diff --git a/.env.example b/.env.example
index d017b40..0548468 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,6 @@
# Postgres credentials — shared by the db container and the API/poller connection string.
POSTGRES_USER=applytrack
-POSTGRES_PASSWORD=applytrack
+POSTGRES_PASSWORD=JanewayDidNothingWrong
POSTGRES_DB=applytrack
# Migration timeout in seconds (default 60). Increase for slower databases / many
@@ -15,7 +15,7 @@ API_PORT=8080
# Optional: override the API connection string directly (e.g. pointing at an
# external Postgres). When unset, the compose default / appsettings.json is used.
-# ConnectionStrings__Postgres=Host=localhost;Port=5432;Database=applytrack;Username=applytrack;Password=applytrack
+# ConnectionStrings__Postgres=Host=localhost;Port=5432;Database=applytrack;Username=applytrack;Password=JanewayDidNothingWrong
# --- Public origin (set this for any real deployment) -----------------------
# Canonical public base URL of this instance. Magic-link login emails are built
@@ -36,7 +36,7 @@ API_PORT=8080
# Optional: override the poller's connection directly (libpq URL). When unset, the
# compose default points it at the bundled db service.
-# DATABASE_URL=postgresql://applytrack:applytrack@localhost:5432/applytrack
+# DATABASE_URL=postgresql://applytrack:JanewayDidNothingWrong@localhost:5432/applytrack
# --- Email / magic-link delivery --------------------------------------------
# SMTP relay for login-link emails. SMTP is the email equivalent of the
diff --git a/README.md b/README.md
index b32f8ae..8d83b9e 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,9 @@ telemetry, no SaaS.
- **It drafts your cover letters.** Point it at any OpenAI-compatible LLM — a local
Ollama/vLLM model (so your résumé never leaves the box, $0 per draft) or a hosted
provider — and generate a letter per application, tailored from your uploaded
- résumé. Keys are yours, encrypted at rest.
+ résumé. Set a reusable multi-line signature once in **Settings · AI**; it is
+ appended exactly to every new or regenerated letter. Download the result as
+ Markdown or PDF. Keys are yours, encrypted at rest.
- **It's genuinely multi-tenant.** Every row is owned by a tenant; every query in
both runtimes unconditionally filters `WHERE tenant_id`. One deployment cleanly
serves many users with hard data isolation.
@@ -184,14 +186,14 @@ All configuration is environment variables (see [`.env.example`](./.env.example)
| Variable | Default | Purpose |
| --- | --- | --- |
-| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | `applytrack` | Postgres credentials, shared by `db`, `api`, and `poller`. |
+| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | `applytrack` / `JanewayDidNothingWrong` / `applytrack` | Postgres credentials, shared by `db`, `api`, and `poller`. Override the password for any deployment exposed beyond local development. |
| `API_PORT` | `8080` | Host port the API publishes (the container always listens on 8080). |
| `DRAIN_INTERVAL` | `60` | Seconds between drains of the on-demand poll queue (the SPA's "Poll now" button). |
| `POLL_INTERVAL` | `3600` | Seconds between full multi-tenant polls. |
| `ConnectionStrings__Postgres` | _(compose default)_ | Override to point the API at an external Postgres. |
| `DATABASE_URL` | _(compose default)_ | Override to point the poller at an external Postgres (libpq URL). |
| `APPLYTRACK_DIR` | `./applications` | Default folder the `import-md` command reads when `--dir` is omitted. |
-| `Llm__BaseUrl` / `Llm__Model` / `Llm__ApiKey` | _(empty)_ | Instance-default cover-letter LLM — any OpenAI-compatible endpoint (a local Ollama/vLLM/LM Studio model or a hosted provider). `ApiKey` is blank for a keyless local model. Each tenant can override these in the UI. See [Cover letters](#cover-letters). |
+| `Llm__BaseUrl` / `Llm__Model` / `Llm__ApiKey` | _(empty)_ | Instance-default cover-letter LLM — any OpenAI-compatible endpoint (a local Ollama/vLLM/LM Studio model or a hosted provider). `ApiKey` is blank for a keyless local model. Each tenant can override these in **Settings · AI**, including a reusable multi-line signature. See [Cover letters](#cover-letters). |
| `APPLYTRACK_SECRETS_KEY` | _(empty)_ | Master key (AES-256-GCM) that encrypts each tenant's **own** stored LLM API key at rest. Leave unset to disable per-tenant keys — the instance default above is still used. |
| `Email__Host` / `Email__Port` / `Email__Username` / `Email__Password` / `Email__From` / `Email__FromName` | `Host` empty, `Port` `587`, `FromName` `OSApplyTrack` | SMTP relay for magic-link login emails. Leave `Email__Host` unset to log links to the console instead of sending (zero email config). Set it to relay through any SMTP provider — a local relay, your mail provider, or a transactional service (Resend/SendGrid/Mailgun/SES). Port 465 = implicit TLS, else STARTTLS; blank username = unauthenticated. Deliverability to Gmail/Outlook needs a relay whose IP has PTR + SPF/DKIM/DMARC. |
@@ -264,8 +266,8 @@ killing the process:
| `GET` | `/api/resume` | The tenant's résumé brief — the only facts the drafter may assert. |
| `PUT` | `/api/resume` | Compatibility endpoint for structured résumé JSON. |
| `POST` | `/api/resume/upload` | Upload a text-based PDF résumé (`multipart/form-data`, `resume` file, max 5 MB) and store the extracted text as the model brief. |
-| `GET` | `/api/llm-settings` | The tenant's endpoint override + the instance default. The API key is **write-only** — never returned, only a `has_api_key` flag. |
-| `PUT` | `/api/llm-settings` | Set `base_url` / `model` / `api_key` (omit `api_key` to leave it untouched, blank to clear it) and `cover_letters_enabled` (omit to keep; `false` disables all drafting for the tenant). |
+| `GET` | `/api/llm-settings` | The tenant's endpoint override, model, reusable `cover_letter_signature`, and the instance default. The API key is **write-only** — never returned, only a `has_api_key` flag. |
+| `PUT` | `/api/llm-settings` | Set `base_url` / `model` / `api_key` / `cover_letter_signature` (omit `api_key` or the signature to leave it untouched; blank clears either) and `cover_letters_enabled` (omit to keep; `false` disables all drafting for the tenant). |
| `DELETE` | `/api/apps/{name}/cover-letter` | Discard a generated letter → `204`. |
### Not in v1
@@ -290,7 +292,7 @@ The schema is migrated by **DbUp** from idempotent `.sql` scripts under
| `seen` | The dedupe ledger — listings already surfaced, so leads don't repeat. |
| `poll_requests` | The on-demand "Poll now" queue the worker drains. |
| `resume_profiles` | Per-tenant résumé brief — the facts the cover-letter drafter feeds the LLM. |
-| `llm_settings` | Per-tenant LLM endpoint override; a tenant's own API key is stored **AES-256-GCM-encrypted** at rest. |
+| `llm_settings` | Per-tenant LLM endpoint, model, cover-letter toggle, and multi-line signature; a tenant's own API key is stored **AES-256-GCM-encrypted** at rest. |
| `cover_letters` | Generated cover letters, one per application (`FK → applications ON DELETE CASCADE`). |
Account deletion relies on `ON DELETE CASCADE` foreign keys (migrations
@@ -335,18 +337,22 @@ résumé you control — provider-agnostic, and built so your data can stay on-p
Groq, …). A local model means **$0 per draft** and the résumé never leaves the box.
- **Operator default + per-tenant override.** The instance sets a default endpoint
via `Llm__BaseUrl` / `Llm__Model` / `Llm__ApiKey`; each tenant can override any
- field in the **Settings · AI** tab (override just the model, keep the URL, etc.).
+ field in the **Settings · AI** tab (override just the model, keep the URL, set
+ the cover-letter toggle, or save a multi-line signature).
- **Your résumé is the only source of truth.** The **Settings · Résumé** tab uploads
a text-based PDF and stores the extracted résumé text as the model brief. The LLM
is told these are the *only* facts it may assert, so it can't invent employers or
metrics.
+- **Your signature is deterministic.** The model is instructed to stop after the
+ body paragraphs. The saved signature is appended server-side exactly as entered,
+ so drafts do not fall back to placeholders such as “The Candidate.”
- **Keys encrypted at rest.** A tenant's own API key is write-only: sealed with
AES-256-GCM under `APPLYTRACK_SECRETS_KEY` and never echoed back. Without that
master key the per-tenant-key path is disabled (the instance default still works).
- **Generate from the application sheet.** Each app gets a **Generate cover letter**
action; the result renders inline with copy / download `.md` or PDF / regenerate /
- discard. Letters are stored per application and are excluded from the export
- snapshot by design.
+ discard. PDF output is generated server-side from the saved letter. Letters are
+ stored per application and are excluded from the export snapshot by design.
## Security & hardening
@@ -381,7 +387,8 @@ OSApplyTrack is built to face the public internet behind a reverse proxy:
cookie's `Secure` flag is set automatically. Don't expose Kestrel directly.
- **Change the default password.** For any deployment reachable beyond `localhost`,
change `POSTGRES_PASSWORD` (and the matching connection string) from the
- `applytrack` default before first boot — the bundled value is local-dev only.
+ bundled development default before first boot — the documented value is not a
+ production secret.
- **Dependency CVE watch.** [`.forgejo/workflows/audit.yml`](./.forgejo/workflows/audit.yml)
runs `dotnet list package --vulnerable --include-transitive` and `pip-audit` on
every push/PR and weekly, failing the build on a known-vulnerable dependency. Run
@@ -411,9 +418,12 @@ OSApplyTrack is built to face the public internet behind a reverse proxy:
## Accessibility
The web interface targets **WCAG 2.2 Level AA**. It provides semantic screen-reader
-navigation, complete keyboard operation, visible focus, reduced-motion and
-high-contrast modes, light/dark/system colors, adjustable text size, and responsive
-reflow. Preferences are stored only in the current browser and apply before sign-in.
+navigation, complete keyboard operation, visible focus, labeled controls, live status
+announcements, reduced-motion and high-contrast modes, light/dark/system colors,
+adjustable 100%/125%/150% text size, comfortable or compact spacing, and responsive
+reflow through 400% browser zoom. The **Settings · Accessibility** panel detects system
+preferences before sign-in and lets each browser override color, contrast, motion,
+text size, and density. Preferences are stored only in the current browser.
Pull requests run Playwright and axe-core checks against login, application, editor,
settings, validation, and responsive workflows. See the
@@ -451,7 +461,7 @@ cd api && dotnet run --project ApplyTrack.Api
# Poller (one-shot poll; needs DATABASE_URL or the POSTGRES_* / PG* env vars)
pip install -e '.[dev]'
-DATABASE_URL=postgresql://applytrack:applytrack@localhost:5432/applytrack applytrack poll
+DATABASE_URL=postgresql://applytrack:JanewayDidNothingWrong@localhost:5432/applytrack applytrack poll
```
**Enable cover-letter drafting (optional).** Drafting stays off until an
@@ -510,8 +520,9 @@ v1 is intentionally focused. Deferred, with clean seams already in place:
- **Link checking.** `/api/apps/{name}/check-link` returns 501 today; the
SSRF-hardened prober already exists in the poller for when it's enabled.
-- **Richer cover-letter output.** The materials engine ships plain-text/Markdown
- letters ([Cover letters](#cover-letters)) with direct Markdown and PDF downloads.
+- **Richer cover-letter output.** The materials engine already ships plain-text/
+ Markdown letters ([Cover letters](#cover-letters)) with direct Markdown and PDF
+ downloads; future work can add richer templates and provider-specific formats.
## Contributing
diff --git a/api/ApplyTrack.Api/ApplyTrack.Api.csproj b/api/ApplyTrack.Api/ApplyTrack.Api.csproj
index 166e835..63e6e78 100644
--- a/api/ApplyTrack.Api/ApplyTrack.Api.csproj
+++ b/api/ApplyTrack.Api/ApplyTrack.Api.csproj
@@ -5,7 +5,7 @@
enable
enable
ApplyTrack.Api
- 1.11.0
+ 1.11.1
Aaron K. Clark
Copyright 2026 Aaron K. Clark
Apache-2.0
diff --git a/api/ApplyTrack.Api/appsettings.json b/api/ApplyTrack.Api/appsettings.json
index 9106bf9..0573245 100644
--- a/api/ApplyTrack.Api/appsettings.json
+++ b/api/ApplyTrack.Api/appsettings.json
@@ -7,6 +7,6 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
- "Postgres": "Host=localhost;Port=5432;Database=applytrack;Username=applytrack;Password=applytrack"
+ "Postgres": "Host=localhost;Port=5432;Database=applytrack;Username=applytrack;Password=JanewayDidNothingWrong"
}
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 091efad..eec2bb9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,7 +10,7 @@ services:
image: postgres:17-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-applytrack}
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-applytrack}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-JanewayDidNothingWrong}
POSTGRES_DB: ${POSTGRES_DB:-applytrack}
ports:
- "5432:5432"
@@ -28,7 +28,7 @@ services:
context: ./api
dockerfile: ApplyTrack.Api/Dockerfile
environment:
- ConnectionStrings__Postgres: "Host=db;Port=5432;Database=${POSTGRES_DB:-applytrack};Username=${POSTGRES_USER:-applytrack};Password=${POSTGRES_PASSWORD:-applytrack}"
+ ConnectionStrings__Postgres: "Host=db;Port=5432;Database=${POSTGRES_DB:-applytrack};Username=${POSTGRES_USER:-applytrack};Password=${POSTGRES_PASSWORD:-JanewayDidNothingWrong}"
ASPNETCORE_URLS: "http://+:8080"
# Cover-letter AI (all optional; see .env.example). Empty defaults leave drafting
# off until a tenant configures their own endpoint in the SPA's AI panel.
@@ -63,7 +63,7 @@ services:
context: .
dockerfile: Dockerfile.poller
environment:
- DATABASE_URL: "postgresql://${POSTGRES_USER:-applytrack}:${POSTGRES_PASSWORD:-applytrack}@db:5432/${POSTGRES_DB:-applytrack}"
+ DATABASE_URL: "postgresql://${POSTGRES_USER:-applytrack}:${POSTGRES_PASSWORD:-JanewayDidNothingWrong}@db:5432/${POSTGRES_DB:-applytrack}"
depends_on:
db:
condition: service_healthy
diff --git a/pyproject.toml b/pyproject.toml
index 43b93fd..22182e7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "applytrack-poller"
-version = "1.11.0"
+version = "1.11.1"
description = "Discovery poller for OSApplyTrack — fetches and scores remote job leads into shared Postgres."
requires-python = ">=3.10"
license = { text = "Apache-2.0" }
diff --git a/src/applytrack/importer.py b/src/applytrack/importer.py
index 91ca6a2..d9f5f31 100644
--- a/src/applytrack/importer.py
+++ b/src/applytrack/importer.py
@@ -86,7 +86,7 @@ def connect(database_url: str | None = None) -> psycopg.Connection:
port=os.environ.get("PGPORT", "5432"),
dbname=os.environ.get("POSTGRES_DB", "applytrack"),
user=os.environ.get("POSTGRES_USER", "applytrack"),
- password=os.environ.get("POSTGRES_PASSWORD", "applytrack"),
+ password=os.environ.get("POSTGRES_PASSWORD", "JanewayDidNothingWrong"),
**options, # type: ignore[arg-type]
)
diff --git a/uv.lock b/uv.lock
index 9a31c7b..a9bb558 100644
--- a/uv.lock
+++ b/uv.lock
@@ -23,7 +23,7 @@ wheels = [
[[package]]
name = "applytrack-poller"
-version = "1.11.0"
+version = "1.11.1"
source = { editable = "." }
dependencies = [
{ name = "defusedxml" },