feat/Update_Controllers_to_Allow_for_Premium_Membership
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# AI privacy and external-processing policy
|
||||
|
||||
Updated: 2026-08-03
|
||||
|
||||
The default execution mode is local-only. External processing of `/cv/*` payloads requires all of:
|
||||
|
||||
1. `Ai:ExternalProcessingEnabled=true` on the backend;
|
||||
2. `EXTERNAL_AI_ENABLED=true` on the AI sidecar;
|
||||
3. a configured external `Ai:ExternalProvider` / `AI_PROVIDER` (`gemini` or `groq`);
|
||||
4. a current Pro/Admin entitlement resolved from the database;
|
||||
5. AI enabled in the user's server-side settings; and
|
||||
6. the user's explicit `ExternalAiProcessingAllowed` opt-in.
|
||||
|
||||
The backend adds `X-Ai-External-Allowed: true` only after that live policy check. The sidecar otherwise routes `/cv/*` to Ollama even when an external provider is configured. `/summarize` always uses the local summarization model. Provider keys remain server-side and are never returned by the settings API.
|
||||
|
||||
`GET/PUT /api/ai/settings` owns the user settings. Disabling AI takes effect on the next protected request and is also rechecked by the current enrichment and queued-CV workers. Existing users migrate with AI enabled to preserve current behaviour; external consent always defaults to false.
|
||||
|
||||
This is the privacy admission foundation, not the final routing system. AI-001/AI-002 must carry an immutable policy snapshot into durable operations, recheck it at execution, record the actual provider/reason, add bounded local-first fallback triggers and minimize each external payload. Background CV work currently fails safe to local because it has no HTTP user context.
|
||||
|
||||
@@ -174,6 +174,8 @@ I missing", "what happened previously". All deterministic, all owned by nothing:
|
||||
| Endpoint | Reads | Owns |
|
||||
|---|---|---|
|
||||
| `GET /{id}/timeline` | `JobEvent` | nothing |
|
||||
| `GET /{id}/interview-prep` | editable `InterviewPrepItem` board | user edits only |
|
||||
| `GET /{id}/interview-prep/brief` | cached generated `InterviewPrepNote` | explicit refresh or attachment-context change |
|
||||
| `GET /{id}/analysis` | `JobApplication.Description` | nothing |
|
||||
| `GET /{id}/match` | `CareerProfile` + the advert | nothing |
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Attachment storage invariants
|
||||
|
||||
Updated: 2026-08-02
|
||||
|
||||
Attachments remain local files under `Data:AttachmentsRoot/<jobId>/` with metadata in `Attachments`. No object-store abstraction or schema migration is used.
|
||||
|
||||
## Invariants
|
||||
|
||||
- Every managed path resolves beneath `Data:AttachmentsRoot` without crossing a symlink/junction.
|
||||
- Stored filenames are generated and stable. Renaming changes only the user-visible `FileName` metadata.
|
||||
- A committed row normally has its final file. A temporary suffix is the only recognized recoverable exception.
|
||||
- Unknown plain files are reported and preserved; reconciliation never guesses that a legacy orphan is safe to delete.
|
||||
- All row lookups remain parent/job tenant-scoped.
|
||||
|
||||
## Durable filesystem states
|
||||
|
||||
| State | Meaning | Startup action |
|
||||
|---|---|---|
|
||||
| `<final>.uploading` and matching DB row | metadata committed; promotion was interrupted | atomically promote to `<final>` |
|
||||
| `<final>.uploading` without DB row | copy/request failed before commit | purge the staging file |
|
||||
| `<final>.deleting` and matching DB row | delete stopped before DB commit | restore to `<final>` |
|
||||
| `<final>.deleting` without DB row | DB deletion committed; purge was interrupted | purge the quarantined file |
|
||||
| plain file without DB row | unknown/legacy orphan | report only |
|
||||
| DB row without final or recognized state | missing bytes | report only |
|
||||
|
||||
Upload validates the complete batch before copying, stages every file, commits all metadata and derived flags in one database transaction, then promotes files. A post-commit promotion failure returns 202 and leaves `.uploading` for startup recovery.
|
||||
|
||||
Delete first atomically renames bytes to `.deleting`, removes metadata and updates flags in one transaction, then purges. A database failure restores the file. A post-commit purge failure returns 202 and leaves a retryable marker.
|
||||
|
||||
## Operations
|
||||
|
||||
Reconciliation runs once after database initialization and before the API begins serving. It logs counts only—never file contents or paths. Nonzero missing, unsafe, unknown-orphan or failure counts require operator review. Persistent suffix-state failures are retried on the next safe service restart.
|
||||
|
||||
Rollback requires draining/reconciling `.uploading` and `.deleting` markers before reverting the application. Do not delete unknown plain files. The same managed-root and quarantine conventions must be reused by account deletion (SEC-009).
|
||||
|
||||
Object storage, content deduplication, periodic multi-replica reconciliation and destructive legacy-orphan cleanup are explicitly deferred until deployment topology or measured volume requires them.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Background worker ownership and activation
|
||||
|
||||
Updated: 2026-08-02
|
||||
|
||||
## Tenant execution contract
|
||||
|
||||
HTTP requests derive `JobTrackerContext.CurrentUserId` from the authenticated request. Hosted services have no HTTP context, so deny-on-null query filters intentionally return no tenant rows.
|
||||
|
||||
`BackgroundTenantRunner` is the only worker bypass for the four job-owner schedulers. It uses `IgnoreQueryFilters` only to enumerate distinct non-empty job owners, then creates a fresh scope per owner and sets `CurrentUserService` before resolving the scoped `JobTrackerContext`. All work queries run through the normal tenant filters. An owner failure is counted and isolated; logs contain worker name, exception type and aggregate counts, not owner IDs or private content. An HTTP context cannot be replaced by a background owner.
|
||||
|
||||
This is a sequential, single-instance foundation. Generic operation leasing now exists in OPS-001A, but notifications, bounded AI handlers and multi-replica scheduling belong to OPS-001B/C and AI-001 and must precede activation that needs them.
|
||||
|
||||
## Hosted-service inventory
|
||||
|
||||
| Service | Tenant behavior | Side effect | Activation |
|
||||
|---|---|---|---|
|
||||
| `RulesHostedService` | owner runner + normal filters/per-user rules | changes job status | `Workers:RulesEnabled=false` by default; keep off until user-visible notification/audit behavior is ready |
|
||||
| `FollowUpReminderHostedService` | owner runner + normal filters | sends email, then marks date | both `Workers:FollowUpRemindersEnabled` and `Email:FollowUpReminders:Enabled`; keep off until persistent notification/idempotency work |
|
||||
| `DailyExportHostedService` | owner runner + normal filters; one hashed-owner atomic file | writes local JSON | both `Workers:DailyExportEnabled` and `Exports:DailyEnabled`; keep off pending retention/operator rollout |
|
||||
| `JobEnrichmentHostedService` | owner runner + normal filters | deterministic tags and AI summary | `Workers:JobEnrichmentEnabled=false`; do not enable before Pro/privacy/provider/queue gates |
|
||||
| `CvProcessingHostedService` | existing explicit run owner on every unfiltered query | user-requested CV parsing/AI | unchanged; persistent run rows recover at startup, process-local wake-up remains single-instance |
|
||||
| `SummarizerProbeHostedService` | tenant-neutral; no user payload | AI-sidecar health probe | existing probe settings; unchanged |
|
||||
| `DatabaseBackupHostedService` | tenant-neutral complete database snapshot | local backup | existing backup settings; unchanged |
|
||||
|
||||
## Rollout and rollback
|
||||
|
||||
Changing old settings alone cannot activate the four repaired workers; the new worker-specific switch must also be true. Enable one worker at a time only after its listed dependencies, fake/synthetic tests, operator monitoring and rollback are ready. Rollback is setting its worker switch to false; do not delete outputs or undo already-applied user-visible changes without a separate reviewed procedure.
|
||||
@@ -256,13 +256,13 @@ erDiagram
|
||||
|
||||
| Controller | Lines | Highlights |
|
||||
|---|---|---|
|
||||
| `JobApplicationsController` | **2313** | **38 endpoints.** CRUD, paging/filter/sort, board, reminders, stats, analytics, history, timeline, status/follow-up PATCH, soft delete/restore, duplicate-check, **plus** the whole AI surface: match-score, candidate-fit, focus-plan, interview-prep, readiness, tailored-CV draft/preview/export/generate, application-drafts, application-package, follow-up drafts + send, ai-metrics. |
|
||||
| `JobApplicationsController` | **2394** | **37 endpoints.** CRUD, paging/filter/sort, board, reminders, stats, analytics, history, status/follow-up PATCH, soft delete/restore, duplicate-check, **plus** the AI surface: match-score, candidate-fit, focus-plan, generated interview-prep brief, readiness, tailored-CV draft/preview/export/generate, application-drafts, application-package, follow-up drafts + send, ai-metrics. The canonical timeline and editable interview board live in their focused controllers. |
|
||||
| `ProfileCvController` | **2249** | CV upload artifacts, extraction runs, structure parsing, reprocess/rebuild/improve, rewrite-section, rewrite-preview, templates, Playwright PDF export, benchmark harness. |
|
||||
| `GmailController` | **1023** | OAuth connect/callback, sync, review queue, import decisions, job matching. |
|
||||
| `AuthController` | **879** | login/register/me/config, Google + Microsoft exchange and link/unlink, avatar, password change/reset, email verification, session cookie + CSRF. |
|
||||
| `AdminSystemController` | 342 | System readiness (DB/Gmail/AI). |
|
||||
| `TwoFactorController` | 341 | TOTP enrol/verify/disable, recovery codes. |
|
||||
| `AttachmentsController` | 245 | Multipart upload to disk, download, rename, delete, purpose/AI-inclusion metadata. |
|
||||
| `AttachmentsController` | 342 | Validated staged upload, managed-root download, metadata-only rename, quarantined delete, purpose/AI-inclusion metadata and restart reconciliation. See `attachment-storage.md`. |
|
||||
| `UsersController` | 229 | Admin user/role management. |
|
||||
| `AdminAuditController` | 219 | Audit trail. |
|
||||
| `CorrespondenceController` | 185 | Per-job messages CRUD. |
|
||||
@@ -294,7 +294,7 @@ erDiagram
|
||||
| `CvProcessingHostedService` + `CvProcessingQueue` | Process-local wake-up queue for CV extraction; queued/running database work is recovered at startup |
|
||||
| `DatabaseBackupHostedService` → `DatabaseBackupRunner` | Automated DB backup (`VACUUM INTO`, server-derived path) |
|
||||
|
||||
Caches and worker coordination are process-local. CV work itself is durable and recovered after restart, but the worker remains a deliberate single-instance design without database leasing.
|
||||
Rules, reminders, daily export and enrichment now enumerate owners explicitly, then re-enter normal tenant-filtered scopes. All four have deny-by-default worker switches and remain inactive until their notification/privacy/entitlement/operations prerequisites are ready; see `docs/architecture/background-workers.md`. Caches and worker coordination are process-local. CV work itself is durable and recovered after restart, but the worker remains a deliberate single-instance design without database leasing.
|
||||
|
||||
---
|
||||
|
||||
@@ -321,7 +321,7 @@ Caches and worker coordination are process-local. CV work itself is durable and
|
||||
|
||||
## 10. Email
|
||||
|
||||
`SmtpEmailSender` + `EmailSettingsResolver`: config from env/appsettings **or** DB-stored `SystemEmailSettings` (admin-editable). Gmail SMTP + app password in prod. Flows: password reset, email verification, follow-up reminders. `App:PublicBaseUrl` builds links.
|
||||
`SmtpEmailSender` + `EmailSettingsResolver`: config from env/appsettings **or** DB-stored `SystemEmailSettings` (admin-editable). Gmail SMTP + app password in prod. Flows: password reset, email verification, follow-up reminders. `App:PublicBaseUrl` is the canonical external origin for generated links, OAuth callbacks, billing redirects, secure cookies and production Host validation.
|
||||
|
||||
Inbound: `GmailOAuthService` (655), `MicrosoftGraphOAuthService` (507), `ImapService` (345, SSRF-guarded).
|
||||
|
||||
|
||||
@@ -5,9 +5,14 @@ Compose. The backend is not published directly; nginx proxies `/api`. `deploy/de
|
||||
configuration, takes and verifies a provider-appropriate backup before replacement, builds/restarts the
|
||||
stack, and performs health checks.
|
||||
|
||||
Production compose enables `Proxy:TrustForwardedHeaders` because nginx is the sole ingress, allowing
|
||||
HTTPS scheme and client-IP rate limits to use one trusted forwarded hop. The development override
|
||||
publishes the API directly and disables forwarded-header trust.
|
||||
Production commands explicitly select `docker-compose.yml`; it publishes no application ports.
|
||||
Traefik reaches frontend/nginx over `jobtracker_shared`, must match the canonical Host exactly, and
|
||||
must replace `X-Forwarded-For` and `X-Forwarded-Proto`. Nginx passes those sanitized values to the
|
||||
backend over the dedicated `WEB_PROXY_SUBNET`; nginx also derives its only application server name
|
||||
from `APP_PUBLIC_BASE_URL` and rejects unknown Hosts except its liveness endpoint. The backend trusts
|
||||
only the dedicated CIDR and one forwarded hop. Local development explicitly adds
|
||||
`docker-compose.dev.yml`, which publishes ports 3000/5202, uses the localhost origin, and disables
|
||||
forwarded-header trust.
|
||||
|
||||
Each service rotates local Docker logs at 10 MB and retains three files. Add a central sink only if
|
||||
cross-host search or longer retention becomes necessary.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Durable operation state
|
||||
|
||||
Updated: 2026-08-02
|
||||
|
||||
`UserOperations` is the shared persistence foundation for long-running CV, Strategy Snapshot and later AI work. It is not a workflow engine and carries no raw CV, email, prompt, job description or private note. Producers store only bounded task/policy fields plus an opaque subject reference.
|
||||
|
||||
## State and ownership
|
||||
|
||||
Allowed application states are `queued`, `running`, `waiting_for_retry`, `waiting_for_external_fallback`, `succeeded`, `failed` and `cancelled`. Every row has an owner query filter and a unique `(OwnerUserId, TaskType, IdempotencyKey)` index, so duplicate clicks for one user return the same operation while another user may use the same key safely.
|
||||
|
||||
Workers claim from a neutral scope with one conditional database update, receive the owner ID and lease token, then must re-enter that owner scope before heartbeat/completion/failure. Owner mutations use normal query filters plus the unguessable lease token. Claiming from an owner/HTTP scope and completing from a neutral scope are refused.
|
||||
|
||||
Expired leases become retryable until `MaxAttempts`; the final expiry fails. Cancellation is immediate before execution and cooperative while running; an expired cancelled lease converges to `cancelled`. Queued deadlines fail closed. Retry delay, attempts, leases, field lengths and progress percentages are bounded.
|
||||
|
||||
## Schema ownership
|
||||
|
||||
`20260802224646_AddUserOperations` is EF-owned and intentionally absent from `StartupInitializationExtensions`. Its `Up` branches by provider: native SQLite DDL from the model and explicit bounded MariaDB `varchar`/`char`, `datetime(6)` and `int` DDL. Common indexes are generated by the active provider. This incrementally reduces the dual-ownership risk from JT-019 while preserving clean MariaDB types.
|
||||
|
||||
Rollback requires stopping operation producers/workers, draining or explicitly cancelling active rows, retaining any referenced results, then applying the migration `Down`. Rolling an old application version against a database that still contains this additive table is safe; dropping it loses operation history and must not be done casually.
|
||||
|
||||
Terminal notifications are described in `notifications.md`. Authenticated owner APIs expose bounded list/detail/cancel/retry state under `/api/operations`; DTOs omit idempotency keys, leases, provider/model fields, failure text and result references.
|
||||
|
||||
`AiOperationAdmission` now provides the shared AI producer boundary: it rechecks live Pro/AI settings, snapshots `local_only` or `external_allowed`, applies per-user/global capacity, assigns a deadline and returns the stable `/api/operations/{id}` status URL. It stores only subject type/ID, never raw CV/email/prompt text. The current process-local admission semaphore is correct for the documented single-backend deployment; multi-replica rollout requires a database capacity reservation.
|
||||
|
||||
`AiOperationWorker` claims only registered task types by priority, enters the explicit owner scope, rechecks entitlement/privacy/cancellation, runs one inference by default, heartbeats the lease, enforces a timeout, classifies bounded retry/permanent failure and commits the existing terminal notification. `Workers:AiOperationsEnabled` defaults false and no production feature handler is registered yet. AI-003/004 add the Strategy/CV handlers and 202 producer endpoints; AI-002 adds provider/model concurrency, circuit and actual-provider provenance.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Persistent operation notifications
|
||||
|
||||
Updated: 2026-08-02
|
||||
|
||||
`UserNotifications` provides durable, owner-scoped visibility for terminal `UserOperations`. It is not an email outbox: no SMTP or provider delivery is triggered by this record.
|
||||
|
||||
Each operation may have one current notification, enforced by a unique nullable `OperationId`. Success, permanent failure and cancellation update the operation and insert a generic notification in one relational database transaction. Retryable failures do not notify. Manually retrying a failed or cancelled operation removes its previous terminal notification so its next terminal outcome can create one. Duplicate terminal calls make no change.
|
||||
|
||||
Notification text never includes operation inputs, provider diagnostics, failure messages, CV/email/job content or result data. Read and dismiss mutations use the normal owner query filter; dismissed notifications are excluded from lists and unread counts. Authenticated APIs expose list/unread/read/dismiss under `/api/notifications`.
|
||||
|
||||
The frontend `/operations` page polls only while mounted, shows loading/empty/error/progress/cancellation/retry states and dispatches a local refresh event after notification mutations. The application shell polls only the unread count every 60 seconds; the reminders badge remains separate. The bell links to `/operations` and has an accessible name.
|
||||
|
||||
Migration `20260802225941_AddUserNotifications` is EF-owned and absent from startup reconciliation. SQLite uses native scaffolded types; MariaDB uses bounded `char`/`varchar` and `datetime(6)` columns with a foreign key that sets `OperationId` null if operation history is deleted.
|
||||
|
||||
Rollback requires stopping producers/workers, retaining any required notification evidence elsewhere, then applying the migration `Down`. An older application can run with this additive table still present, which is the safer application rollback. Dropping the table loses notification state.
|
||||
Reference in New Issue
Block a user