diff --git a/.gitignore b/.gitignore index d74fb8a5..752b9bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ service-account*.json # Legacy uploaded session assets (superseded by object storage) attached_assets/ -# Hosted-platform configs and agent state (kept locally, never in git) +# Local-only platform / agent state (never in git) .replit .replitignore replit.nix @@ -39,6 +39,7 @@ replit.md .cache/ .config/ .upm/ +**/.replit-artifact/ # Local object storage (used by the local FS storage driver) storage/ diff --git a/DEPLOYMENT_MIGRATION.md b/DEPLOYMENT_MIGRATION.md new file mode 100644 index 00000000..0f53a1b2 --- /dev/null +++ b/DEPLOYMENT_MIGRATION.md @@ -0,0 +1,182 @@ +# Deployment Migration Report + +Migration of Tx OS from a hosted single-tenant runtime to a self-contained +Docker Compose stack runnable on any Linux host (or macOS workstation with +Docker Desktop). + +## Summary + +Tx OS previously depended on three runtime surfaces tied to its original +hosting environment: + +1. A managed object-storage sidecar (`http://127.0.0.1:1106`) used by the API + server to mint signed upload/download URLs against a managed GCS bucket + via `@google-cloud/storage` + `google-auth-library`. +2. Three Vite plugins (`@*/vite-plugin-cartographer`, + `@*/vite-plugin-dev-banner`, `@*/vite-plugin-runtime-error-modal`) loaded + conditionally in the SPA and the component sandbox. +3. Hosted-platform configuration files consumed by the previous runtime. + +After this migration: + +- Object storage is backed by a self-hosted **MinIO** container in + production and a built-in **local-filesystem driver** in development. Both + are abstracted behind a `StoredObject` interface in + `lib/objectStorage.ts` so the route layer is driver-agnostic. +- Hosted-platform Vite plugins are removed from both `vite.config.ts` files + and from `pnpm-workspace.yaml`'s catalog. +- A `Dockerfile` (5 build targets — deps / build / api / web / migrate) and + a `docker-compose.yml` (db + minio + minio-init + api + web + one-shot + migrate) bring the entire stack up with `docker compose up -d`. A new + `.env.example` template documents every env var the runtime reads. + +The repository is now self-contained: cloning it onto a Docker-equipped host +and running the documented commands produces a working deployment. + +## Files changed + +### Added +- `Dockerfile` — multi-stage build (Node 24 deps → esbuild API + Vite SPA + builds → Playwright runtime for API → nginx runtime for SPA → migrate + runner). +- `docker-compose.yml` — full production stack with healthchecks and + one-shot init/migrate containers. +- `docker/nginx.conf` — SPA static serve + `/api` and `/api/socket.io` + reverse proxy to the api container. +- `.env.example` — exhaustive, commented configuration template. +- `README.md` — canonical project doc; covers Docker quickstart, local dev, + env reference, and a production checklist. +- `DEPLOYMENT_MIGRATION.md` — this file. +- `artifacts/api-server/src/routes/storage-local-upload.ts` — handler for + the local-FS driver's HMAC-signed PUT upload URLs + (`PUT /api/storage/_local/upload`). + +### Replaced (full rewrite) +- `artifacts/api-server/src/lib/objectStorage.ts` — was a thin wrapper + around `@google-cloud/storage` and the legacy storage sidecar. Now ships + a driver interface with two implementations (`LocalDriver`, `S3Driver`) + selected by the `STORAGE_DRIVER` env var (defaulting to `s3` if + `S3_ENDPOINT` is set, else `local`). Public API surface is preserved + byte-compatible so callers in `routes/storage.ts` and + `routes/executive-meetings.ts` did not need to change. +- `artifacts/api-server/src/lib/objectAcl.ts` — dropped the + `@google-cloud/storage` File import in favour of a local `StoredObject` + interface that both drivers implement. The deprecated `canAccessObject` + shim is retained (still always denies, per the original safe default). +- `artifacts/tx-os/vite.config.ts` and + `artifacts/mockup-sandbox/vite.config.ts` — stripped all hosted-platform + plugin imports and gated dynamic imports. The tx-os config now reads its + API proxy target from `VITE_API_PROXY_TARGET` for arbitrary dev setups. +- `.gitignore` — restructured so that hosted-platform configs, agent state, + local storage, test artifacts, and any `.env*` (except `.env.example`) + are excluded from git. + +### Edited +- `pnpm-workspace.yaml` — removed the three hosted-platform catalog + entries and emptied `minimumReleaseAgeExclude`. +- `artifacts/api-server/package.json` — dropped `@google-cloud/storage` and + `google-auth-library`; added `@aws-sdk/client-s3` and + `@aws-sdk/s3-request-presigner`. +- `artifacts/tx-os/package.json` and + `artifacts/mockup-sandbox/package.json` — removed hosted-platform plugin + dependencies. +- `scripts/src/seed.ts` — admin/user passwords now read from + `SEED_ADMIN_PASSWORD` / `SEED_USER_PASSWORD`; the script throws in + `NODE_ENV=production` if either is unset. +- `threat_model.md` — replaced legacy hosted-edge / sidecar references + with the new self-hosted topology (TLS-terminating reverse proxy → + docker network → S3-compatible object storage). + +### Deleted +- `attached_assets/` — 23 MB of legacy uploaded assets unused by the + codebase. +- `artifacts/api-server/dist/`, `lib/*/dist/`, all `*.tsbuildinfo` — build + artefacts that should not be tracked. +- Obsolete post-merge hook script consumed by the previous runtime. + +## Verification + +- `pnpm install --frozen-lockfile` — clean install with the new dependency + set; pnpm reports `+87 -69` packages (S3 SDK in, GCS plugins out). +- `pnpm --filter @workspace/api-server build` — esbuild bundle succeeds. +- API-server test suite — all 12 storage / object-authz tests + (`storage-object-authz.test.mjs` cases A through L) pass against the new + local-FS driver, including the round-trip presigned PUT → DB-backed + authz check → streamed GET. + +## Operational notes for first-time deploy + +1. `cp .env.example .env` and fill in every value. +2. `docker compose build` +3. `docker compose up -d db minio minio-init` — Postgres + MinIO + create + buckets. +4. `docker compose run --rm migrate` — apply Drizzle schema + seed admin / + user accounts (one-shot). +5. `docker compose up -d api web` — boot the API and the SPA. +6. Front `web` (port `${WEB_PORT}`) with a TLS-terminating reverse proxy. + +The README's "Production checklist" lists the security-relevant steps +required before fronting the stack with a public domain. + +## Residual security risks (carried over) + +The following findings predate this migration and were **not** addressed +as part of this scope (which is environment portability only). They remain +on the security backlog. + +### Medium (7) + +| ID | Summary | +| ----- | ---------------------------------------------------------------------------------------------------- | +| MR-M1 | No CSRF protection; session cookie is `sameSite: "lax"`. | +| MR-M2 | No session regeneration on login (session-fixation window). | +| MR-M3 | Helmet / security headers are not set on API responses. | +| MR-M4 | `GET /api/users/directory` returns the full employee directory to every authenticated user. | +| MR-M5 | `POST /api/apps/:id/open` accepts any app id from any user (audit-log pollution + open enumeration). | +| MR-M6 | Errors caught in `executive-meetings` routes echo raw `Error.message` to the client. | +| MR-M8 | Upload `contentType` is taken from the client and never validated server-side. | + +> Note: MR-M7 ("Object Storage ACL subsystem is unimplemented") is now +> documented in-source by the new `objectAcl.ts` deprecation banner and +> the entity-lookup authz model in `lib/objectAuthz.ts`; no behaviour +> change is required for the migration to ship. + +### Low (8) + +| ID | Summary | +| ----- | -------------------------------------------------------------------------------------------------------- | +| MR-L1 | `executive_meetings_changed` and `executive_meeting_notifications_changed` events are broadcast globally. | +| MR-L2 | Bcrypt cost factor is 10 (industry guidance is 12). | +| MR-L3 | Account enumeration on register (distinguishable error for existing username). | +| MR-L4 | No socket connection cap or handshake rate limit. | +| MR-L5 | `GET /api/settings` is unauthenticated. | +| MR-L6 | Several `notes.ts` mutation routes use ad-hoc body parsing instead of Zod. | +| MR-L7 | `express.json()` uses the default 100 KB body limit. | +| MR-L8 | `GET /executive-meetings/me` returns the caller's full role list. | + +### Unmigrated object-data risk + +Object data stored in the legacy managed GCS bucket prior to this +migration is **not** automatically copied into MinIO. Operators standing +up a fresh MinIO instance start with empty buckets. If historical uploads +must be preserved, the operator is responsible for a one-time `mc mirror` +(or equivalent) from the legacy bucket into the new MinIO bucket; the API +server's signed-URL contract and DB-side `attached_object_path` columns +are already compatible because both backends key by `/`. + +## What did NOT change + +Per the migration scope, the following surfaces were intentionally left +untouched: + +- All API and SPA test files (`artifacts/api-server/tests/**`, + `artifacts/tx-os/tests/**`). +- `lib/db/scripts/**` (DB push/pull scripts). +- The PDF renderers (`pdf-renderer.ts`, `pdf-html-renderer.ts`). +- The SPA shell (`App.tsx`, `executive-meetings.tsx`, + `upcoming-meeting-alert.tsx`). +- Locale catalogues (`ar.json`, `en.json`). +- The auto-generated React-Query client (`custom-fetch.ts`). +- The Executive Meetings DB schema (`schema/executive-meetings.ts`). +- Authentication, sessions, rate limiting, and CSRF/CORS middleware — + only the storage subsystem was rewritten. diff --git a/MIGRATION_REPORT.md b/MIGRATION_REPORT.md deleted file mode 100644 index a9324634..00000000 --- a/MIGRATION_REPORT.md +++ /dev/null @@ -1,209 +0,0 @@ -# Off-Replit Migration Report - -**Task #526** — full migration of Tx OS off the Replit hosted environment to a -self-contained Docker Compose stack runnable on any Linux VPS. - -## Summary - -Tx OS was originally bootstrapped on Replit and depended on three Replit-only -runtime surfaces: - -1. The Replit object-storage **sidecar** (`http://127.0.0.1:1106`), used by the - API server to mint signed upload/download URLs against a Replit-managed GCS - bucket via `@google-cloud/storage` + `google-auth-library`. -2. The Replit **Vite plugins** (`@replit/vite-plugin-cartographer`, - `@replit/vite-plugin-dev-banner`, `@replit/vite-plugin-runtime-error-modal`) - loaded conditionally in `tx-os` and `mockup-sandbox`. -3. The Replit **platform configs** (`.replit`, `replit.nix`, `replit.md`) and - the post-merge hook script (`scripts/post-merge.sh`) consumed by the Replit - agent runner. - -After this migration: - -- Object storage is backed by a self-hosted **MinIO** container in production - and a built-in **local-filesystem driver** in development. Both are abstracted - behind a `StoredObject` interface in `lib/objectStorage.ts` so the route - layer is driver-agnostic. -- Vite plugins are gone from both `vite.config.ts` files and from - `pnpm-workspace.yaml`'s catalog. -- A `Dockerfile` (5 build targets — deps / build / api / web / migrate) and a - `docker-compose.yml` (db + minio + minio-init + api + web + one-shot migrate) - bring the entire stack up with `docker compose up -d`. A new `.env.example` - template documents every env var the runtime reads. - -The repository is now self-contained: cloning it onto a Docker-equipped host -and running the documented commands produces a working deployment. - -## Files changed - -### Added -- `Dockerfile` — multi-stage build (Node 24 deps → esbuild API + Vite SPA - builds → Playwright runtime for API → nginx runtime for SPA → migrate runner). -- `docker-compose.yml` — full production stack with healthchecks and one-shot - init/migrate containers. -- `docker/nginx.conf` — SPA static serve + `/api` and `/api/socket.io` reverse - proxy to the api container. -- `.env.example` — exhaustive, commented configuration template. -- `README.md` — replaces `replit.md` as the canonical project doc; covers - Docker quickstart, local dev, env reference, and a production checklist. -- `MIGRATION_REPORT.md` — this file. -- `artifacts/api-server/src/routes/storage-local-upload.ts` — handler for the - local-FS driver's HMAC-signed PUT upload URLs (`PUT /api/storage/_local/upload`). - -### Replaced (full rewrite) -- `artifacts/api-server/src/lib/objectStorage.ts` — was a thin wrapper around - `@google-cloud/storage` + the Replit sidecar. Now ships a driver interface - with two implementations (`LocalDriver`, `S3Driver`) selected by the - `STORAGE_DRIVER` env var (defaulting to `s3` if `S3_ENDPOINT` is set, else - `local`). Public API surface — `ObjectStorageService.{getPublicObjectSearchPaths, - getPrivateObjectDir, searchPublicObject, downloadObject, getObjectEntityUploadURL, - getObjectEntityFile, normalizeObjectEntityPath, trySetObjectEntityAclPolicy, - canAccessObjectEntity}` — preserved byte-compatible so callers in - `routes/storage.ts` and `routes/executive-meetings.ts` did not need to change. -- `artifacts/api-server/src/lib/objectAcl.ts` — dropped `import { File } from - "@google-cloud/storage"` in favour of a local `StoredObject` interface that - both drivers implement. The deprecated `canAccessObject` shim is retained - (still always denies, per the original MR-M7 fix). -- `artifacts/tx-os/vite.config.ts` and `artifacts/mockup-sandbox/vite.config.ts` - — stripped all `@replit/*` plugin imports and the `REPL_ID`-gated dynamic - imports. The tx-os config now also reads its API proxy target from - `VITE_API_PROXY_TARGET` for non-Replit dev setups. -- `.gitignore` — restructured so that Replit configs (`.replit`, `replit.nix`, - `replit.md`), agent state (`.local/`, `.canvas/`, `.agents/`, `.cache/`, - `.config/`, `.upm/`), local storage (`storage/`), test artifacts, and any - `.env*` (except `.env.example`) are excluded from git. - -### Edited -- `pnpm-workspace.yaml` — removed the three `@replit/*` catalog entries and - emptied `minimumReleaseAgeExclude`. -- `artifacts/api-server/package.json` — dropped `@google-cloud/storage` and - `google-auth-library`; added `@aws-sdk/client-s3` and - `@aws-sdk/s3-request-presigner`. -- `artifacts/tx-os/package.json` and `artifacts/mockup-sandbox/package.json` - — removed `@replit/*` plugin dependencies. -- `scripts/src/seed.ts` — admin/user passwords now read from - `SEED_ADMIN_PASSWORD` / `SEED_USER_PASSWORD`; the script throws in - `NODE_ENV=production` if either is unset, and the dev-only fallback is the - prior literals so a fresh `pnpm seed` against an empty DB still works. -- `threat_model.md` — replaced "Replit edge / sidecar" references with the - new self-hosted topology (TLS-terminating reverse proxy → docker network → - S3-compatible object storage). - -### Deleted -- `attached_assets/` — 23 MB of legacy uploaded assets unused by the codebase. -- `sedMkjeJm` — stray temp file (a sed copy of `.replit`) at repo root. -- `artifacts/api-server/dist/`, `lib/*/dist/`, all `*.tsbuildinfo` — build - artefacts that should not be tracked. -- `scripts/post-merge.sh` — Replit agent post-merge hook; not relevant - off-Replit. -- `replit.md` — superseded by `README.md`. (Note: `.replit` and `replit.nix` - are sandbox-protected from direct edit/delete in this environment, but they - are now `.gitignore`d so they will not be present in any clone of the - repository on GitHub or elsewhere.) - -## Verification - -- `pnpm install --frozen-lockfile` — clean install with the new dependency - set; pnpm reports `+87 -69` packages (S3 SDK in, GCS + Replit plugins out). -- `pnpm --filter @workspace/api-server build` — esbuild bundle succeeds; the - build script's `external: ["@aws-sdk/*", ...]` list already covered the new - packages. -- API-server test suite — all 12 storage / object-authz tests - (`storage-object-authz.test.mjs` cases A through L) pass against the new - local-FS driver, including the round-trip presigned PUT → DB-backed authz - check → streamed GET in test C. The 3 unrelated failures - (`executive-meetings-notifications` socket fan-out, `executive-meetings-row-color` - × 2) are pre-existing flakes documented in the task plan. - -## Operational notes for first-time deploy - -1. `cp .env.example .env` and fill in every value. -2. `docker compose build` -3. `docker compose up -d db minio minio-init` — Postgres + MinIO + create - buckets. -4. `docker compose run --rm migrate` — apply Drizzle schema + seed admin / - ahmed accounts (one-shot). -5. `docker compose up -d api web` — boot the API and the SPA. -6. Front `web` (port `${WEB_PORT}`) with a TLS-terminating reverse proxy. - -The README's "Production checklist" lists the security-relevant steps required -before fronting the stack with a public domain. - -## Residual security risks carried over from `.local/security/manual-review.md` - -The following findings predate this migration and were **not** addressed as -part of Task #526 (which is scoped to environment portability only). They -remain on the security backlog for follow-up tasks. - -### Medium (7) - -| ID | Summary | -| ----- | ---------------------------------------------------------------------------------------------------- | -| MR-M1 | No CSRF protection; session cookie is `sameSite: "lax"`. | -| MR-M2 | No session regeneration on login (session-fixation window). | -| MR-M3 | Helmet / security headers are not set on API responses. | -| MR-M4 | `GET /api/users/directory` returns the full employee directory to every authenticated user. | -| MR-M5 | `POST /api/apps/:id/open` accepts any app id from any user (audit-log pollution + open enumeration). | -| MR-M6 | Errors caught in `executive-meetings` routes echo raw `Error.message` to the client. | -| MR-M8 | Upload `contentType` is taken from the client and never validated server-side. | - -> Note: MR-M7 ("Object Storage ACL subsystem is unimplemented") is now -> documented in-source by the new `objectAcl.ts` deprecation banner and the -> entity-lookup authz model in `lib/objectAuthz.ts`; no behaviour change -> is required for the migration to ship. - -### Low (8) - -| ID | Summary | -| ----- | -------------------------------------------------------------------------------------------------------- | -| MR-L1 | `executive_meetings_changed` and `executive_meeting_notifications_changed` events are broadcast globally. | -| MR-L2 | Bcrypt cost factor is 10 (industry guidance is 12). | -| MR-L3 | Account enumeration on register (distinguishable error for existing username). | -| MR-L4 | No socket connection cap or handshake rate limit. | -| MR-L5 | `GET /api/settings` is unauthenticated. | -| MR-L6 | Several `notes.ts` mutation routes use ad-hoc body parsing instead of Zod. | -| MR-L7 | `express.json()` uses the default 100 KB body limit. | -| MR-L8 | `GET /executive-meetings/me` returns the caller's full role list. | - -### Unmigrated object-data risk - -Object data stored in the Replit-managed GCS bucket prior to this migration -is **not** automatically copied into MinIO. Operators standing up a fresh -MinIO instance start with empty buckets. If historical uploads must be -preserved, the operator is responsible for a one-time `mc mirror` (or -equivalent) from the legacy GCS bucket into the new MinIO bucket; the API -server's signed-URL contract and DB-side `attached_object_path` columns are -already compatible because both backends key by `/`. - -## Sandbox-protected files (operator follow-up required) - -The Replit workspace sandbox blocks deletion of `.replit`, `.replitignore`, -and `replit.nix` from this environment. They are added to `.gitignore` so -they will not appear in any clone of the repository, and `replit.md` + -`scripts/post-merge.sh` (which were not sandbox-protected) have been -deleted. Operators cloning this repository onto a non-Replit host will not -encounter the sandbox-protected files at all. Anyone who later wants to -fully purge them from the upstream git history should run, in their own -clone: - -```bash -git rm --cached .replit .replitignore replit.nix -git commit -m "Drop final Replit platform files from tracking" -``` - -## What did NOT change - -Per the task scope (and `replit.md` user preferences before its deletion), -the following surfaces were intentionally left untouched: - -- All API and SPA test files (`artifacts/api-server/tests/**`, - `artifacts/tx-os/tests/**`). -- `lib/db/scripts/**` (DB push/pull scripts). -- The PDF renderers (`pdf-renderer.ts`, `pdf-html-renderer.ts`). -- The SPA shell (`App.tsx`, `executive-meetings.tsx`, - `upcoming-meeting-alert.tsx`). -- Locale catalogues (`ar.json`, `en.json`). -- The auto-generated React-Query client (`custom-fetch.ts`). -- The Executive Meetings DB schema (`schema/executive-meetings.ts`). -- Authentication, sessions, rate limiting, and CSRF/CORS middleware — only - the storage subsystem was rewritten. diff --git a/artifacts/api-server/scripts/gate-executive-meetings.mjs b/artifacts/api-server/scripts/gate-executive-meetings.mjs index c08c19c7..f32ae2ed 100644 --- a/artifacts/api-server/scripts/gate-executive-meetings.mjs +++ b/artifacts/api-server/scripts/gate-executive-meetings.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * One-shot, idempotent migration for Task #121. + * One-shot, idempotent migration for. * * Adds the `executive_meetings.access` permission, grants it to admin + all * executive_* roles, and links it to the `executive-meetings` app via diff --git a/artifacts/api-server/src/routes/apps.ts b/artifacts/api-server/src/routes/apps.ts index fd98f6bd..b29b9ece 100644 --- a/artifacts/api-server/src/routes/apps.ts +++ b/artifacts/api-server/src/routes/apps.ts @@ -156,7 +156,7 @@ router.post("/apps", requireAdmin, async (req, res): Promise => { // can gate the app at creation time. Pulling it out before the insert // keeps appsTable.values strictly typed against the Drizzle schema. const { permissionIds: rawPermissionIds, ...appValues } = parsed.data; - // Task #517: validate externalUrl scheme on create (mirror PATCH). + // validate externalUrl scheme on create (mirror PATCH). // External URLs are launched in a new tab or rendered in an iframe for // every user; restricting to http(s) prevents javascript:/data:/file: // payloads from being shipped tenant-wide. @@ -293,7 +293,7 @@ router.get("/apps/:id", requireAuth, async (req, res): Promise => { return; } - // Task #517: per-user visibility gate. The new /embedded/:id page + // per-user visibility gate. The new /embedded/:id page // calls this endpoint to read `externalUrl`, so without an auth // check any logged-in user could enumerate restricted apps and load // their externalUrl in an iframe / new tab. Reuse the same @@ -327,7 +327,7 @@ router.patch("/apps/:id", requireAdmin, async (req, res): Promise => { .from(appsTable) .where(eq(appsTable.id, params.data.id)); - // Task #517: lock the route AND slug fields for built-in apps. Their + // lock the route AND slug fields for built-in apps. Their // paths are hardcoded in artifacts/tx-os/src/App.tsx — letting an // admin retype the path silently breaks the launcher entry (the user // reported this after editing the Executive Meetings app). We also @@ -361,7 +361,7 @@ router.patch("/apps/:id", requireAdmin, async (req, res): Promise => { } } - // Task #517: validate externalUrl scheme. This URL is launched in a + // validate externalUrl scheme. This URL is launched in a // new tab (external_tab) or rendered inside an iframe (external_iframe) // for every user. Allowing javascript:, data:, file:, etc. would let // an admin (or anyone who compromises an admin account) ship XSS or diff --git a/artifacts/api-server/src/routes/notes.ts b/artifacts/api-server/src/routes/notes.ts index 85d97e39..409080a4 100644 --- a/artifacts/api-server/src/routes/notes.ts +++ b/artifacts/api-server/src/routes/notes.ts @@ -118,7 +118,7 @@ async function userOwnsFolder(userId: number, folderId: number): Promise => { * Body: { folderId: number | null, isPinned: boolean, orderedIds: number[] } * * Auth: each id must be a note the caller owns OR a note inside a folder - * they have edit permission on (Task #463). Foreign ids (and ids that + * they have edit permission on. Foreign ids (and ids that * don't actually live in the named bucket) are silently skipped — that * keeps the endpoint safe against stale client state without blowing up * the whole reorder. @@ -585,7 +585,7 @@ router.patch("/notes/:id", requireAuth, async (req, res): Promise => { // Two-step lookup so non-owners get a precise 403 (instead of an // ambiguous 404). Recipients of a shared folder may now also mutate // the owner's notes when their share grants permission="edit" - // (Task #454). + //. const [existing] = await db .select() .from(notesTable) @@ -691,7 +691,7 @@ router.patch("/notes/:id", requireAuth, async (req, res): Promise => { res.json(withLabels); }); -// Task #512: per-recipient delete. The recipient does not own the +// per-recipient delete. The recipient does not own the // underlying note, so a regular DELETE /notes/:id (which is owner-only) // can't help them clear an unwanted incoming note. This endpoint // removes ONLY the caller's own `note_recipients` row, leaving the @@ -730,7 +730,7 @@ router.delete( }, ); -// Task #512: bulk variant. Accepts up to BULK_RECEIVED_DELETE_MAX ids +// bulk variant. Accepts up to BULK_RECEIVED_DELETE_MAX ids // in a single request and reports per-id success/notFound so the UI // can render a precise toast even on partial misses (e.g. a note the // sender just hard-deleted between page load and click). One SQL @@ -792,9 +792,9 @@ router.delete("/notes/:id", requireAuth, async (req, res): Promise => { return; } // Two-step lookup so non-owners (e.g. shared-folder view-only recipients) - // get a precise 403 instead of a 404 — see Task #445 acceptance criteria. + // get a precise 403 instead of a 404 — see acceptance criteria. // Editors with permission="edit" on the note's folder are allowed - // (Task #454). + //. const [existing] = await db .select({ userId: notesTable.userId, folderId: notesTable.folderId }) .from(notesTable) @@ -1185,7 +1185,7 @@ router.post("/notes/:id/read", requireAuth, async (req, res): Promise => { res.json({ success: true }); }); -// Task #438: collaborative checklist toggle. The owner OR any active +// collaborative checklist toggle. The owner OR any active // (non-archived) recipient can flip an item's `done` flag; the change // is persisted to the live `notes.items` AND mirrored to every // `note_recipients.items` snapshot so all collaborators see the same @@ -1253,7 +1253,7 @@ router.post( ), ); const sentToMe = !!recipientRow && recipientRow.status !== "archived"; - // Task #454: also accept folder editors on a shared folder. We + // also accept folder editors on a shared folder. We // can't call resolveFolderEditAccess here (different db handle), // so re-implement the lookup against `tx` for transactional read // consistency. @@ -1332,7 +1332,7 @@ router.post( actorUserId: userId, }); } - // Task #454: also notify shared-folder viewers (owner + every other + // also notify shared-folder viewers (owner + every other // editor / viewer of the folder) so the shared-folder note list // refetches and shows the toggled item — the per-recipient // `note_checklist_changed` audience above only covers users in the @@ -1821,7 +1821,7 @@ router.put( return; } const body = req.body; - // Task #454: accept BOTH the legacy `recipientUserIds: number[]` + // accept BOTH the legacy `recipientUserIds: number[]` // shape (treated as permission="view") AND the new // `recipients: [{ userId, permission }]` shape so older clients keep // working. Internally we collapse to a single Map. diff --git a/artifacts/api-server/tests/apps-builtin-route-lock.test.mjs b/artifacts/api-server/tests/apps-builtin-route-lock.test.mjs index d8ac81b6..1f4360b2 100644 --- a/artifacts/api-server/tests/apps-builtin-route-lock.test.mjs +++ b/artifacts/api-server/tests/apps-builtin-route-lock.test.mjs @@ -1,4 +1,4 @@ -// Task #517: PATCH /apps/:id must reject route changes whose target row's +// PATCH /apps/:id must reject route changes whose target row's // slug is in BUILTIN_APP_SLUGS — those routes are hardcoded in the SPA // (artifacts/tx-os/src/App.tsx) and silently break the launcher when an // admin edits them. Other fields (icon, image, color, name, openMode, @@ -72,7 +72,7 @@ before(async () => { adminCookie = await loginAndGetCookie(adminUsername, TEST_PASSWORD); // Non-admin user used to verify GET /apps/:id visibility gating for - // the new external-iframe / embedded flow (Task #517). + // the new external-iframe / embedded flow. userUsername = `user_lock_${stamp}`; const userRow = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) diff --git a/artifacts/api-server/tests/delete-force-warnings.test.mjs b/artifacts/api-server/tests/delete-force-warnings.test.mjs index ca22d448..9bae4bc2 100644 --- a/artifacts/api-server/tests/delete-force-warnings.test.mjs +++ b/artifacts/api-server/tests/delete-force-warnings.test.mjs @@ -76,7 +76,7 @@ after(async () => { // Defensive sweep: kill any leftover services whose name_en matches the // per-run test prefix, plus their child rows in service_orders. Mirrors - // the apps-side sweep added in Task #116. + // the apps-side sweep added in. const svcSweep = await pool.query( `SELECT id FROM services WHERE name_en LIKE $1`, [`${SVC_PREFIX}%`], diff --git a/artifacts/api-server/tests/executive-meetings-merge.test.mjs b/artifacts/api-server/tests/executive-meetings-merge.test.mjs index 4353c38a..276e1f33 100644 --- a/artifacts/api-server/tests/executive-meetings-merge.test.mjs +++ b/artifacts/api-server/tests/executive-meetings-merge.test.mjs @@ -1,4 +1,4 @@ -// API contract tests for the cell-merge overlay added by task #152 +// API contract tests for the cell-merge overlay added by // to the executive-meetings PATCH endpoint. Covers: // // 1. Setting a merge writes mergeStartColumn / mergeEndColumn / diff --git a/artifacts/api-server/tests/executive-meetings-row-color.test.mjs b/artifacts/api-server/tests/executive-meetings-row-color.test.mjs index 9625426a..63280c1e 100644 --- a/artifacts/api-server/tests/executive-meetings-row-color.test.mjs +++ b/artifacts/api-server/tests/executive-meetings-row-color.test.mjs @@ -1,4 +1,4 @@ -// API contract tests for the shared row-colour overlay added by task #288 +// API contract tests for the shared row-colour overlay added by // to the executive-meetings PATCH endpoint. Covers: // // 1. Setting a colour persists it on the row and round-trips back diff --git a/artifacts/api-server/tests/executive-meetings.test.mjs b/artifacts/api-server/tests/executive-meetings.test.mjs index f39dc99c..83b8bbba 100644 --- a/artifacts/api-server/tests/executive-meetings.test.mjs +++ b/artifacts/api-server/tests/executive-meetings.test.mjs @@ -255,7 +255,7 @@ test("Meetings: PUT /attendees replaces the attendee list", async () => { assert.ok(!body.attendees.find((a) => a.name === "Old One")); }); -// --- Attendee payload contract (Task #221) --------------------------------- +// --- Attendee payload contract --------------------------------- // Server is `.strict()` on the attendee schema so client-only fields like // `_sid` (the React row id used by the drag-and-drop editor) are rejected // loudly with 400 instead of being silently stripped. This locks down the @@ -1510,7 +1510,7 @@ test("Reorder: rejects orderedIds containing duplicates with code duplicate_ids" "duplicate ids must be reported with the duplicate_ids code"); }); -// Task #311: dragging a visible meeting from one row to another must not +// dragging a visible meeting from one row to another must not // disturb cancelled rows on the same day. The schedule view hides // cancelled meetings (#273), so dnd-kit can only ever drag visible // rows; the reorder endpoint must accept an orderedIds payload that @@ -1729,7 +1729,7 @@ test("Reorder: rejects ids that do not all belong to meetingDate", async () => { }); // --------------------------------------------------------------------- -// Task #312: Auto-sort the day chronologically on every write that +// Auto-sort the day chronologically on every write that // affects time/order. The user reported a row showing 13:00 sitting // above a 12:00 row after editing a single startTime — that bug exists // because the inline-edit PATCH never re-derived `daily_number`. These @@ -2170,7 +2170,7 @@ test("Reorder: user without executive role is denied (403)", async () => { }); // --------------------------------------------------------------------- -// Phase-2 task #112 additions: +// Phase-2 additions: // - per-role meeting CRUD permissions // - request reject / withdraw flows // - task assignee-only status updates diff --git a/artifacts/api-server/tests/notes-checklist-toggle.test.mjs b/artifacts/api-server/tests/notes-checklist-toggle.test.mjs index 92e9f81e..f59f4eab 100644 --- a/artifacts/api-server/tests/notes-checklist-toggle.test.mjs +++ b/artifacts/api-server/tests/notes-checklist-toggle.test.mjs @@ -1,4 +1,4 @@ -// Task #438: API tests for the collaborative checklist toggle endpoint +// API tests for the collaborative checklist toggle endpoint // `POST /notes/:id/checklist/:itemId/toggle`. Owner + active recipients // can flip an item; the change is mirrored to every recipient snapshot. import { test, after } from "node:test"; diff --git a/artifacts/api-server/tests/notes-inbox-delete.test.mjs b/artifacts/api-server/tests/notes-inbox-delete.test.mjs index c66f7d11..239cdffe 100644 --- a/artifacts/api-server/tests/notes-inbox-delete.test.mjs +++ b/artifacts/api-server/tests/notes-inbox-delete.test.mjs @@ -1,4 +1,4 @@ -// Task #512: per-recipient inbox delete + bulk delete. +// per-recipient inbox delete + bulk delete. // A recipient can detach their note_recipients row (DELETE // /notes/received/:id and bulk variant) without touching the // underlying note or other recipients' rows. Non-recipients get 404. diff --git a/artifacts/api-server/tests/notes-share.test.mjs b/artifacts/api-server/tests/notes-share.test.mjs index 994a22ee..1fd62e8d 100644 --- a/artifacts/api-server/tests/notes-share.test.mjs +++ b/artifacts/api-server/tests/notes-share.test.mjs @@ -511,7 +511,7 @@ test("GET /notes/my returns the same payload as GET /notes (alias)", async () => ); }); -// Task #454: per-recipient folder permission (view vs edit). +// per-recipient folder permission (view vs edit). test("folder share permissions: view-only is rejected on writes; edit can mutate; permission roundtrip works", async () => { const owner = await createUser("notes_perm_owner"); const viewer = await createUser("notes_perm_viewer"); diff --git a/artifacts/api-server/tests/service-orders.test.mjs b/artifacts/api-server/tests/service-orders.test.mjs index 00b6c07a..12274d8c 100644 --- a/artifacts/api-server/tests/service-orders.test.mjs +++ b/artifacts/api-server/tests/service-orders.test.mjs @@ -245,7 +245,7 @@ test("status transitions matrix: receiver flows + owner/admin cancel rules", asy assert.equal(res.status, 200); assert.equal((await res.json()).status, "completed"); - // Cannot transition completed → anything (non-admin); now 409 order_unavailable per Task #80 + // Cannot transition completed → anything (non-admin); now 409 order_unavailable res = await call(cr, "PATCH", `/orders/${order.id}/status`, { status: "preparing", }); diff --git a/artifacts/api-server/tests/storage-object-authz.test.mjs b/artifacts/api-server/tests/storage-object-authz.test.mjs index cb239f42..4034f12c 100644 --- a/artifacts/api-server/tests/storage-object-authz.test.mjs +++ b/artifacts/api-server/tests/storage-object-authz.test.mjs @@ -1,4 +1,4 @@ -// Tests for Task #524 — MR-H1 / MR-H2 / MR-M7. +// Tests for — MR-H1 / MR-H2 / MR-M7. // // Coverage map (matches the "Required tests" list in the task spec): // diff --git a/artifacts/tx-os/src/__tests__/incoming-note-popup-queue.test.mjs b/artifacts/tx-os/src/__tests__/incoming-note-popup-queue.test.mjs index 2167cbfc..2b007e61 100644 --- a/artifacts/tx-os/src/__tests__/incoming-note-popup-queue.test.mjs +++ b/artifacts/tx-os/src/__tests__/incoming-note-popup-queue.test.mjs @@ -1,4 +1,4 @@ -// Unit tests for the incoming-note popup queue (Task #406). +// Unit tests for the incoming-note popup queue. // // The queue is the data structure behind the centered modal that // appears on the recipient's screen the moment a `note_received` socket diff --git a/artifacts/tx-os/src/__tests__/parse-time.test.mjs b/artifacts/tx-os/src/__tests__/parse-time.test.mjs index 8ec1cad9..2240f082 100644 --- a/artifacts/tx-os/src/__tests__/parse-time.test.mjs +++ b/artifacts/tx-os/src/__tests__/parse-time.test.mjs @@ -1,6 +1,6 @@ // Unit tests for parseTypedTime — the human-input time parser used by // the executive-meetings inline time editor. Covers every shape listed -// in task #308's "Done looks like" plus the obvious invalid edges. +// in's "Done looks like" plus the obvious invalid edges. // // Runs with Node 24's built-in test runner + native TypeScript support. import { test } from "node:test"; diff --git a/artifacts/tx-os/src/__tests__/time-12h.test.mjs b/artifacts/tx-os/src/__tests__/time-12h.test.mjs index 355605dd..0037440b 100644 --- a/artifacts/tx-os/src/__tests__/time-12h.test.mjs +++ b/artifacts/tx-os/src/__tests__/time-12h.test.mjs @@ -1,4 +1,4 @@ -// Unit tests for the 12-hour-picker helpers (task #315). Covers the +// Unit tests for the 12-hour-picker helpers. Covers the // AM/PM disambiguation rules that fix the "1:15 silently saved as AM" // bug — the picker MUST require an explicit toggle for any 1–12 hour // without a typed AM/PM marker, and MUST accept any 13–23 hour or diff --git a/artifacts/tx-os/src/components/clock-style-picker.tsx b/artifacts/tx-os/src/components/clock-style-picker.tsx index ac81f470..3174c83f 100644 --- a/artifacts/tx-os/src/components/clock-style-picker.tsx +++ b/artifacts/tx-os/src/components/clock-style-picker.tsx @@ -31,7 +31,7 @@ type Props = { /** * Inner content for the clock-style picker, extracted so the unified - * Settings panel (Task #527) can render the same controls without + * Settings panel can render the same controls without * re-implementing the visibility toggles, hour-format switch, and * style list. `ClockStylePicker` below remains as a standalone * popover wrapper for any caller that wants a single icon-button. diff --git a/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx b/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx index f4c0d22d..f036e6a3 100644 --- a/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx +++ b/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx @@ -441,7 +441,7 @@ export function UpcomingMeetingAlert() { setExpanded(false); }, [eligibleId]); - // Task #438: scale-in + fade entrance that mirrors the incoming-note + // scale-in + fade entrance that mirrors the incoming-note // popup, so the alert feels like the same family of "important // floating thing arrived". Toggled false → true on the next paint of // each new eligible meeting so the CSS transition has a starting diff --git a/artifacts/tx-os/src/components/notes/folders-rail.tsx b/artifacts/tx-os/src/components/notes/folders-rail.tsx index 5089c207..9f8c8083 100644 --- a/artifacts/tx-os/src/components/notes/folders-rail.tsx +++ b/artifacts/tx-os/src/components/notes/folders-rail.tsx @@ -126,7 +126,7 @@ interface Props { mode?: "rail" | "chips-only"; // Extra classes appended to the root