2fa608b93dcc1621a5a76577d52cfafb15f8db22
872 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2fa608b93d |
Add migration script for database updates and seeding
Adds a new `migrate` script to the API server package.json and updates the docker-compose.yml to use this script for database migrations and seeding. Also updates the README.md to reflect the new migration command. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 5a3829b7-335a-40a6-98a8-de6f98c908ff Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v Replit-Helium-Checkpoint-Created: true |
||
|
|
a6b990858a |
Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project can be cloned and run on any Linux VPS with `docker compose up`. Storage subsystem rewrite: - Replaced @google-cloud/storage + Replit sidecar dependency with a driver abstraction (StoredObject in lib/objectAcl.ts) and two implementations: LocalDriver (filesystem + HMAC-signed PUT route at /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected by STORAGE_DRIVER / S3_ENDPOINT env vars. - Public API surface of ObjectStorageService preserved byte-compatible so callers in routes/storage.ts and routes/executive-meetings.ts did not change; download() added to both drivers to keep loadLogoBytes() working (caught in code review round 1). - Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in test C) all pass against the new local driver. Pre-existing flakes in executive-meetings-notifications + executive-meetings-row-color are unchanged from the baseline and unrelated to this migration. Infrastructure: - Dockerfile (6 targets: deps/build/api/web/mockup/migrate). API stage uses the official Playwright base image so PDF rendering works in-container; web stage is nginx serving the Vite SPA bundle; mockup stage carries source + node_modules so the dev preview server runs with PORT=8081 BASE_PATH=/__mockup. - docker-compose.yml: postgres + minio + minio-init (creates buckets) + api (host :8080) + web (host :3000) + one-shot migrate runner; the mockup-sandbox service is gated behind a `dev` profile (host :8081 /__mockup) so a normal `docker compose up -d` does NOT start it. Healthchecks on every long-lived service. - docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket upgrade ordering. - .env.example: every runtime env var consumed by app/objectStorage/ auth/seed/compose paths is documented with comments — host ports, PUBLIC_BASE_URL, ALLOWED_ORIGINS, SESSION_SECRET, BASE_PATH, DATABASE_URL, STORAGE_DRIVER, PRIVATE_OBJECT_DIR, PUBLIC_OBJECT_SEARCH_PATHS, S3_*, LOCAL_STORAGE_ROOT, LOCAL_STORAGE_SIGNING_SECRET, SEED_*, SMTP_*. - README.md replaces replit.md as the canonical project doc; covers Docker quickstart with a service/port table, local dev, env reference, production checklist. - MIGRATION_REPORT.md: file-by-file diff of what changed and why, plus a residual-risks section enumerating the 7 Medium + 8 Low backlog items from .local/security/manual-review.md and the unmigrated object-data note. Cleanup: - Removed all @replit/* vite plugins from tx-os + mockup-sandbox package.json + vite.config.ts + pnpm-workspace.yaml catalog. - Removed @google-cloud/storage and google-auth-library from api-server. - Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and *.tsbuildinfo build artefacts, scripts/post-merge.sh, replit.md. - Stripped Replit references from threat_model.md (sidecar, S4 row, G8 invariant), the storage-object-authz test comment, and the objectStorage.ts header comment. Source/config/docs are now Replit-free. - New comprehensive .gitignore: attached_assets/, Replit configs (.replit, replit.nix, .replitignore, replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*, build artefacts. - scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD from env and throws in production if either is unset. Drift from plan: .replit, .replitignore, replit.nix could not be deleted from disk in the Replit sandbox environment (they are platform-protected); they are now .gitignore'd so they will not appear in any clone of the repository, and MIGRATION_REPORT.md documents the one-line `git rm --cached` an operator can run on a non-Replit clone to purge them from upstream git history. replit.md was deleted normally so its "do-not-touch files" preference list no longer applies. |
||
|
|
47d4ed4bdf |
Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project can be cloned and run on any Linux VPS with `docker compose up`. Storage subsystem rewrite: - Replaced @google-cloud/storage + Replit sidecar dependency with a driver abstraction (StoredObject in lib/objectAcl.ts) and two implementations: LocalDriver (filesystem + HMAC-signed PUT route at /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected by STORAGE_DRIVER / S3_ENDPOINT env vars. - Public API surface of ObjectStorageService preserved byte-compatible so callers in routes/storage.ts and routes/executive-meetings.ts did not change; download() added to both drivers to keep loadLogoBytes() working (caught in code review round 1). - Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in test C) all pass against the new local driver. Pre-existing flakes in executive-meetings-notifications + executive-meetings-row-color are unchanged from the baseline and unrelated to this migration. Infrastructure: - Dockerfile (6 targets: deps/build/api/web/mockup/migrate). API stage uses the official Playwright base image so PDF rendering works in-container; web stage is nginx serving the Vite SPA bundle; mockup stage carries source + node_modules so the dev preview server runs with PORT=8081 BASE_PATH=/__mockup. - docker-compose.yml: postgres + minio + minio-init (creates buckets) + api (host :8080) + web (host :3000) + one-shot migrate runner; the mockup-sandbox service is gated behind a `dev` profile (host :8081 /__mockup) so a normal `docker compose up -d` does NOT start it. Healthchecks on every long-lived service. - docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket upgrade ordering. - .env.example: every runtime env var consumed by app/objectStorage/ auth/seed/compose paths is documented with comments — host ports, PUBLIC_BASE_URL, ALLOWED_ORIGINS, SESSION_SECRET, BASE_PATH, DATABASE_URL, STORAGE_DRIVER, PRIVATE_OBJECT_DIR, PUBLIC_OBJECT_SEARCH_PATHS, S3_*, LOCAL_STORAGE_ROOT, LOCAL_STORAGE_SIGNING_SECRET, SEED_*, SMTP_*. - README.md replaces replit.md as the canonical project doc; covers Docker quickstart with a service/port table, local dev, env reference, production checklist. - MIGRATION_REPORT.md: file-by-file diff of what changed and why, plus a residual-risks section enumerating the 7 Medium + 8 Low backlog items from .local/security/manual-review.md and the unmigrated object-data note. Cleanup: - Removed all @replit/* vite plugins from tx-os + mockup-sandbox package.json + vite.config.ts + pnpm-workspace.yaml catalog. - Removed @google-cloud/storage and google-auth-library from api-server. - Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and *.tsbuildinfo build artefacts, scripts/post-merge.sh, replit.md. - Stripped Replit references from threat_model.md (sidecar, S4 row, G8 invariant), the storage-object-authz test comment, and the objectStorage.ts header comment. Source/config/docs are now Replit-free. - New comprehensive .gitignore: attached_assets/, Replit configs (.replit, replit.nix, .replitignore, replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*, build artefacts. - scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD from env and throws in production if either is unset. Drift from plan: .replit, .replitignore, replit.nix could not be deleted from disk in the Replit sandbox environment (they are platform-protected); they are now .gitignore'd so they will not appear in any clone of the repository, and MIGRATION_REPORT.md documents the one-line `git rm --cached` an operator can run on a non-Replit clone to purge them from upstream git history. replit.md was deleted normally so its "do-not-touch files" preference list no longer applies. |
||
|
|
17dc287c93 |
Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project can be cloned and run on any Linux VPS with `docker compose up`. Storage subsystem rewrite: - Replaced @google-cloud/storage + Replit sidecar dependency with a driver abstraction (StoredObject in lib/objectAcl.ts) and two implementations: LocalDriver (filesystem + HMAC-signed PUT route at /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected by STORAGE_DRIVER / S3_ENDPOINT env vars. - Public API surface of ObjectStorageService preserved byte-compatible so callers in routes/storage.ts and routes/executive-meetings.ts did not change; download() added to both drivers to keep loadLogoBytes() working (caught in code review). - Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in test C) all pass against the new local driver. Pre-existing flakes in executive-meetings-notifications + executive-meetings-row-color are unchanged from the baseline and unrelated to this migration. Infrastructure: - Dockerfile (5 targets: deps/build/api/web/migrate). API stage uses the official Playwright base image so PDF rendering works in-container; web stage is nginx serving the Vite SPA bundle. - docker-compose.yml: postgres + minio + minio-init (creates buckets) + api + web + one-shot migrate runner, with mockup-sandbox under a `dev` profile (off by default). Healthchecks on every long-lived service. - docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket upgrade ordering. - .env.example: every runtime env var documented with comments. - README.md replaces replit.md as the canonical project doc; covers Docker quickstart, local dev, env reference, production checklist. - MIGRATION_REPORT.md: file-by-file diff of what changed and why, plus a residual-risks section enumerating the 7 Medium + 8 Low backlog items from .local/security/manual-review.md and the unmigrated object-data note. Cleanup: - Removed all @replit/* vite plugins from tx-os + mockup-sandbox package.json + vite.config.ts + pnpm-workspace.yaml catalog. - Removed @google-cloud/storage and google-auth-library from api-server. - Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and *.tsbuildinfo build artefacts, scripts/post-merge.sh, replit.md. - Stripped Replit references from threat_model.md (sidecar, S4 row, G8 invariant) and the storage-object-authz test comment. - New comprehensive .gitignore: attached_assets/, Replit configs (.replit, replit.nix, .replitignore, replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*, build artefacts. - scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD from env and throws in production if either is unset. Drift from plan: .replit, .replitignore, replit.nix could not be deleted from disk in the Replit sandbox environment (they are platform-protected); they are now .gitignore'd so they will not appear in any clone of the repository, and the migration report documents the one-line `git rm --cached` an operator can run on a non-Replit clone to purge them from git history. replit.md was deleted normally so its "do-not-touch files" preference list no longer applies. |
||
|
|
143ad9a29d |
Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project can be cloned and run on any Linux VPS with `docker compose up`. Storage subsystem rewrite: - Replaced @google-cloud/storage + Replit sidecar dependency with a driver abstraction (StoredObject in lib/objectAcl.ts) and two implementations: LocalDriver (filesystem + HMAC-signed PUT route at /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected by STORAGE_DRIVER / S3_ENDPOINT env vars. - Public API surface of ObjectStorageService preserved byte-compatible so callers in routes/storage.ts and routes/executive-meetings.ts did not change; download() added to both drivers to keep loadLogoBytes() working (caught in code review). - Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in test C) all pass against the new local driver. Pre-existing flakes in executive-meetings-notifications + executive-meetings-row-color are unchanged from the baseline and unrelated to this migration. Infrastructure: - Dockerfile (5 targets: deps/build/api/web/migrate). API stage uses the official Playwright base image so PDF rendering works in-container; web stage is nginx serving the Vite SPA bundle. - docker-compose.yml: postgres + minio + minio-init (creates buckets) + api + web + one-shot migrate runner. Healthchecks on every long-lived service. - docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket upgrade ordering. - .env.example: every runtime env var documented with comments. - README.md replaces replit.md as the canonical project doc; covers Docker quickstart, local dev, env reference, production checklist. - MIGRATION_REPORT.md: file-by-file diff of what changed and why. Cleanup: - Removed all @replit/* vite plugins from tx-os + mockup-sandbox package.json + vite.config.ts + pnpm-workspace.yaml catalog. - Removed @google-cloud/storage and google-auth-library from api-server. - Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and *.tsbuildinfo build artefacts, scripts/post-merge.sh. - Stripped Replit references from threat_model.md (now describes the self-hosted topology). - New comprehensive .gitignore: Replit configs (.replit, replit.nix, replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*, build artefacts. .replit and replit.nix remain on disk (sandbox-protected) but will not ship to GitHub. - scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD from env and throws in production if either is unset. Drift from plan: replit.md was deleted (per off-Replit scope) so its "do-not-touch files" preference list is moot. .replit/replit.nix kept on disk only because they are sandbox-protected from edit/delete in this environment, but they are git-ignored so they will not appear in a fresh clone. |
||
|
|
9a8e6676ed |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 486afe1d-ebd4-4a5f-8f61-8c684d5feaf0 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v Replit-Helium-Checkpoint-Created: true |
||
|
|
2c281fc507 |
Task #525: Auth endpoint rate limiting (MR-H3)
Closes the remaining High-severity finding from
.local/security/manual-review.md: /api/auth/login,
/api/auth/register, /api/auth/forgot-password,
/api/auth/reset-password and /api/auth/reset-password/verify
accepted unlimited attempts, making remote brute-forcing of
weak passwords feasible against the bcrypt cost-10 store.
Changes
-------
- New artifacts/api-server/src/lib/authRateLimit.ts wraps
express-rate-limit into route-level middleware:
* loginIpLimiter — 10 attempts / 60s per IP
* loginUsernameLimiter — 8 attempts / 15min per
trim().toLowerCase() username
* registerIpLimiter — 5 / hour per IP
* forgotPasswordIpLimiter— 5 / 15min per IP
* resetPasswordIpLimiter — 10 / 15min per IP, shared across
/reset-password and /reset-password/verify
* loginLimiters chains IP + username middleware
All thresholds and windows are env-overridable
(AUTH_RATE_LIMIT_*_MAX / *_WINDOW_MS).
Throttled responses are JSON: 429
{ error: "too_many_requests", message: ... }.
In non-production, requests originating from loopback
(127.0.0.1, ::1, ::ffff:127.*) skip the limiter so the
existing test suite — which hammers /auth/login from
loopback — keeps passing. Production never skips. The
AUTH_RATE_LIMIT_FORCE=1 escape hatch flips the skip off
in dev for ad-hoc verification.
- routes/auth.ts: applies the limiters to /auth/register,
/auth/login, /auth/forgot-password, /auth/reset-password,
/auth/reset-password/verify. trust proxy was already set
to 1 in app.ts so X-Forwarded-For from the Replit edge
drives req.ip in production.
- New artifacts/api-server/tests/auth-rate-limit.test.mjs
(6 tests). Each test routes through a unique
X-Forwarded-For 10.x.x.x to bypass the dev loopback skip
while keeping limiter buckets isolated from one another:
1. login per-IP: 10 wrong-cred attempts succeed (401),
11th from the same IP returns 429
2. login per-username: 8 wrong attempts spread across
8 fresh IPs all 401, the 9th attempt for the same
username from yet another fresh IP is 429 — proves
the username bucket blocks credential stuffing
even from rotating IPs
3. forgot-password per-IP: 5 succeed, 6th 429
4. register per-IP: 5 attempts processed, 6th 429
5. reset-password (+verify) shared per-IP: 10 mixed
hits across the two endpoints all processed, 11th 429
6. loopback regression: 15 login attempts from
loopback (no X-Forwarded-For) all return non-429,
proving the dev skip works and existing tests are
unaffected
Test results
------------
- New file: 6/6 pass.
- Full api-server suite: 327/329 pass. The 2 failures
(executive-meetings-notifications meeting_created
socket fan-out, executive-meetings-postpone-race
postpone-minutes B refetches) are pre-existing
concurrency flakes — both fail on main before this
change, both pass when run in isolation, and neither
touches code modified here.
Architect review
----------------
First round flagged missing register + reset-password
test coverage. Both added in this commit. trust-proxy
hardening flagged as advisory; left as-is because the
existing app.ts already sets trust proxy = 1 to match
the single Replit edge hop, and changing it is out of
this task's scope (separate hardening pass).
Residual risk
-------------
- Per-IP buckets rely on app.set("trust proxy", 1) in
app.ts. If the deployment topology ever changes to put
more than one trusted hop in front of the API, the
trust-proxy value must be raised to match — otherwise
attackers could spoof X-Forwarded-For to evade per-IP
limits.
- The username-bucket key is normalized
(trim().toLowerCase()) but does not collapse Unicode
homoglyphs. Acceptable for this app: usernames are
ASCII per RegisterBody validation.
Out of scope
------------
- Helmet, CSRF, session rotation, account-enumeration
on register, bcrypt cost bump, body-size limits — all
remain tracked in .local/security/manual-review.md as
Medium/Low items.
|
||
|
|
90dc95dc5c |
Task #525: Auth endpoint rate limiting (MR-H3)
Closes the remaining High-severity finding from
.local/security/manual-review.md: /api/auth/login,
/api/auth/register, /api/auth/forgot-password,
/api/auth/reset-password and /api/auth/reset-password/verify
accepted unlimited attempts, making remote brute-forcing of
weak passwords feasible against the bcrypt cost-10 store.
Changes
-------
- New artifacts/api-server/src/lib/authRateLimit.ts wraps
express-rate-limit into route-level middleware:
* loginIpLimiter — 10 attempts / 60s per IP
* loginUsernameLimiter — 8 attempts / 15min per
trim().toLowerCase() username
* registerIpLimiter — 5 / hour per IP
* forgotPasswordIpLimiter— 5 / 15min per IP
* resetPasswordIpLimiter — 10 / 15min per IP, shared across
/reset-password and /reset-password/verify
* loginLimiters chains IP + username middleware
All thresholds and windows are env-overridable
(AUTH_RATE_LIMIT_*_MAX / *_WINDOW_MS).
Throttled responses are JSON: 429
{ error: "too_many_requests", message: ... }.
In non-production, requests originating from loopback
(127.0.0.1, ::1, ::ffff:127.*) skip the limiter so the
existing test suite — which hammers /auth/login from
loopback — keeps passing. Production never skips. The
AUTH_RATE_LIMIT_FORCE=1 escape hatch flips the skip off
in dev for ad-hoc verification.
- routes/auth.ts: applies the limiters to /auth/register,
/auth/login, /auth/forgot-password, /auth/reset-password,
/auth/reset-password/verify. trust proxy was already set
to 1 in app.ts so X-Forwarded-For from the Replit edge
drives req.ip in production.
- New artifacts/api-server/tests/auth-rate-limit.test.mjs
(6 tests). Each test routes through a unique
X-Forwarded-For 10.x.x.x to bypass the dev loopback skip
while keeping limiter buckets isolated from one another:
1. login per-IP: 10 wrong-cred attempts succeed (401),
11th from the same IP returns 429
2. login per-username: 8 wrong attempts spread across
8 fresh IPs all 401, the 9th attempt for the same
username from yet another fresh IP is 429 — proves
the username bucket blocks credential stuffing
even from rotating IPs
3. forgot-password per-IP: 5 succeed, 6th 429
4. register per-IP: 5 attempts processed, 6th 429
5. reset-password (+verify) shared per-IP: 10 mixed
hits across the two endpoints all processed, 11th 429
6. loopback regression: 15 login attempts from
loopback (no X-Forwarded-For) all return non-429,
proving the dev skip works and existing tests are
unaffected
Test results
------------
- New file: 6/6 pass.
- Full api-server suite: 327/329 pass. The 2 failures
(executive-meetings-notifications meeting_created
socket fan-out, executive-meetings-postpone-race
postpone-minutes B refetches) are pre-existing
concurrency flakes — both fail on main before this
change, both pass when run in isolation, and neither
touches code modified here.
Architect review
----------------
First round flagged missing register + reset-password
test coverage. Both added in this commit. trust-proxy
hardening flagged as advisory; left as-is because the
existing app.ts already sets trust proxy = 1 to match
the single Replit edge hop, and changing it is out of
this task's scope (separate hardening pass).
Residual risk
-------------
- Per-IP buckets rely on app.set("trust proxy", 1) in
app.ts. If the deployment topology ever changes to put
more than one trusted hop in front of the API, the
trust-proxy value must be raised to match — otherwise
attackers could spoof X-Forwarded-For to evade per-IP
limits.
- The username-bucket key is normalized
(trim().toLowerCase()) but does not collapse Unicode
homoglyphs. Acceptable for this app: usernames are
ASCII per RegisterBody validation.
Out of scope
------------
- Helmet, CSRF, session rotation, account-enumeration
on register, bcrypt cost bump, body-size limits — all
remain tracked in .local/security/manual-review.md as
Medium/Low items.
|
||
|
|
745e503940 |
Improve how users can access visible applications and files
Refactor `getVisibleAppsForUser` into a new file `appsVisibility.ts` and update existing imports. Add a new test case for app icon object authorization. Modify the `objectAuthz.ts` file to use a more precise JSONB path check for meeting attachments. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: d1d78e3b-ae14-4da2-b782-586269e0ef7e Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v Replit-Helium-Checkpoint-Created: true |
||
|
|
0c8da09ea0 |
Task #524: Fix critical/high object-storage authorization findings
Scope: MR-H1, MR-H2, MR-M7 from .local/security/manual-review.md.
Changes
-------
- New lib/objectAuthz.ts: canUserReadObjectPath(userId, objectPath)
performs an entity-lookup against avatar / app icon / service image /
brand logo / pdf archive / meeting attachment and applies the matching
read rule. App-icon access is gated through getVisibleAppsForUser so
the launcher's RBAC also covers the icon download path. Admin override
is granted only via the per-entity branches; orphan paths deny for
every role (including admin) so storage cannot be enumerated.
- routes/storage.ts: GET /api/storage/objects/* now calls
canUserReadObjectPath BEFORE getObjectEntityFile and returns 404 on
deny so existence is not leaked (MR-H1 fix).
- routes/apps.ts: getVisibleAppsForUser exported for the authz lib.
- routes/executive-meetings.ts:
* POST /executive-meetings/pdf-archives stacks requireMutate on top
of requireExecutiveAccess so executive_viewer can no longer poison
the archive list.
* pdfArchiveCreateSchema is now z.object({ archiveDate }).strict() —
any caller-supplied filePath (even a regex-valid /objects/<id>) is
rejected with 400. The handler always derives filePath server-side
as `print:<archiveDate>`. Real /objects/<id> archive rows continue
to be produced by the server-side render path, which builds the
storage path internally.
- lib/objectAcl.ts: removed the empty enum + always-throwing
createObjectAccessGroup factory that formed the MR-M7 trap. Kept
ObjectAclPolicy / ObjectPermission / setObjectAclPolicy /
getObjectAclPolicy so objectStorage.ts compiles. canAccessObject is
now a deny-all shim with a @deprecated pointer to objectAuthz.ts.
Tests added (artifacts/api-server/tests/storage-object-authz.test.mjs)
---------------------------------------------------------------------
A. Unauthenticated GET /api/storage/objects/* -> 401
B. Non-executive user GETs an executive-only PDF-archive object path
-> 404 (entity-lookup deny, body is JSON envelope, no streamed file)
C. Owner uploads via presign + PUT, sets users.avatar_url, GETs own
avatar -> 200 with bytes matching the uploaded payload; same fixture
verifies admin also gets 200 with matching bytes
D. Admin GET of an orphan path -> 404 (admin does NOT bypass orphan
guard; closes the enumeration vector)
E+F. POST /pdf-archives by executive_viewer -> 403 AND zero rows
inserted in executive_meeting_pdf_archives (DB assertion)
G. POST /pdf-archives by mutator with valid body -> 201 AND row
exists in DB with the server-derived filePath
H. Authed user GET of an orphan path -> 404 (regression)
I. POST /pdf-archives with a caller-supplied filePath -> 400
J. GET /pdf-archives by executive_viewer -> 200 (regression)
K. Brand-logo path: real upload + presign, wired to font_settings.
logo_object_path; on the SAME existing object the executive_viewer
streams 200 + bytes while the order_receiver gets 404 — proves the
divergence is from authz, not from missing-file behavior
Test results
------------
All 10 new tests pass. Full api-server suite: 319/324 pass. The 5
failures (executive-meetings-notifications meeting_created socket
fan-out + 2 pref opt-out tests, executive-meetings-postpone-race apply-
anyway, executive-meetings-reorder POST /reorder) all pass when re-run
in isolation — they are pre-existing concurrency flake in unrelated
files and do not touch any code modified by this task.
Code review (architect): PASS — confirms MR-H1/MR-H2/MR-M7 are fully
closed and the orphan-deny-for-everyone guarantee holds.
Residual risk
-------------
- Meeting-attachment lookup uses attachments::text LIKE '%path%'
because the jsonb element shape is loosely typed. Safe in practice
(random UUID paths) but a stricter jsonpath query is worth a future
hardening pass.
- Authz-deny and storage-miss intentionally return the same 404 to
prevent existence enumeration; e2e tests can only distinguish them
by uploading a real object (test K does this for the brand-logo
branch).
Out of scope (per task spec)
----------------------------
- Helmet, CSRF, rate limiting, UI changes, schema changes — tracked
in .local/security/manual-review.md and proposed as follow-up
Task #525 (auth rate limiting, MR-H3).
|
||
|
|
553aec1256 |
Task #524: Fix critical/high object-storage authorization findings
Scope: MR-H1, MR-H2, MR-M7 from .local/security/manual-review.md. Changes: - New lib/objectAuthz.ts: canUserReadObjectPath(userId, objectPath) performs an entity-lookup (avatar / app icon / service image / brand logo / pdf archive / meeting attachment) and applies the matching read role. Admin override; orphan paths denied. - routes/storage.ts: GET /api/storage/objects/* now calls canUserReadObjectPath BEFORE getObjectEntityFile and returns 404 on deny so existence is not leaked (MR-H1 fix). - routes/executive-meetings.ts: POST /executive-meetings/pdf-archives now stacks requireMutate on top of requireExecutiveAccess so read-only executive_viewer can no longer poison the archive list. pdfArchiveCreateSchema's filePath is constrained to OBJECT_PATH_RE (/objects/<id>) when supplied; omitted = synthetic print:<date> preserved (MR-H2 fix). - lib/objectAcl.ts: removed the empty enum + throwing factory that formed the MR-M7 trap. Kept getObjectAclPolicy / setObjectAclPolicy / ObjectAclPolicy / ObjectPermission so objectStorage.ts compiles. canAccessObject is now a deny-all shim with @deprecated pointer. Tests added (artifacts/api-server/tests/storage-object-authz.test.mjs): 1. Unauthenticated GET /api/storage/objects/* -> 401 2. Authed user GET orphan path -> 404 3. POST /pdf-archives by executive_viewer -> 403 4. POST /pdf-archives with free-form filePath -> 400 5. POST /pdf-archives by mutator with no filePath -> 201 (regression) 6. GET /pdf-archives by executive_viewer -> 200 (regression) Test results: all 6 new tests pass; 318/320 of the full api-server suite pass. The 2 failures (app-permissions-impact preview math, executive-meetings-notifications socket fan-out) are pre-existing and unrelated to the touched files. Residual risk: meeting-attachment lookup uses attachments::text LIKE '%<path>%' because the jsonb shape is loosely typed; safe in practice (random UUID paths) but a stricter jsonpath query could be used in a future hardening pass. Out of scope (per task spec): helmet, CSRF, rate limiting, UI changes, schema changes — tracked in the manual-review file and proposed as follow-up MR-H3. |
||
|
|
57f7a7aba2 |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: c0550813-5b98-4807-94ba-e31944f47876 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v Replit-Helium-Checkpoint-Created: true |
||
|
|
858e9d4b87 |
Add threat model to document system security guarantees
Adds a threat model document detailing assets, trust boundaries, and security guarantees for the system. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: ebda9586-5b27-42a2-8013-0d6a0e3b9966 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v Replit-Helium-Checkpoint-Created: true |
||
|
|
19705793d0 |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 215c11c9-6c8b-480b-9511-ef62a515bac0 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v Replit-Helium-Checkpoint-Created: true |
||
|
|
d2b4f82736 |
Task #521: shorten meetings page URL to /meetings
- artifacts/tx-os/src/App.tsx: serve the schedule at /meetings; add an ExecutiveMeetingsRedirect that rewrites /executive-meetings (and any /executive-meetings/* sub-path) to /meetings while preserving the query string and hash, so existing PDFs, emails, and bookmarks keep working with one transparent hop. - scripts/src/seed.ts: seed the apps row with route="/meetings" and update the expectedBuiltinRoutes drift guard to match. Add an idempotent UPDATE-by-slug after the apps insert so already-deployed rows (including any drift like /executive-meetings or /mms) get reconciled to /meetings on the next seed — safe because the slug is in BUILTIN_APP_SLUGS, meaning the SPA owns the route. - artifacts/tx-os/tests/meetings-route-redirect.spec.mjs: new spec verifying that /meetings loads, /executive-meetings?date=...#... is redirected with query+hash intact, and /api/apps now reports route="/meetings" for the executive-meetings slug. Out of scope (intentionally left untouched per task): API paths under /api/executive-meetings/*, React Query keys, DB tables, the apps slug "executive-meetings", page/component file names, the "Meetings" display name, and the executive_meetings:* permission name. |
||
|
|
bfbf59cb20 |
Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting, even for the common one-on-one case. The dialog now only appears when two or more recipients are picked. Implementation (artifacts/tx-os/src/pages/notes.tsx): - Extracted shared `performSend()` from `confirmSubmit` so both the direct-send and confirm-then-send paths reuse the same mutation, success toast + composer close, and error toast. - `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call performSend() directly, 2+ picked = open the existing AlertDialog. - `confirmSubmit()` (the AlertDialog action) delegates to performSend. - No changes to API, recipient picker, AR/EN strings, or any other AlertDialog (bulk archive/delete left untouched). Tests: - Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a single recipient): no longer expects the confirm dialog; asserts /api/notes/:id/send fires directly and the composer closes. - New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs: - "single-recipient send skips the confirm dialog" (passes) - "multi-recipient send still requires the confirm dialog" (passes) Validation: tx-os tsc clean; both threshold specs pass locally; architect review APPROVED. |
||
|
|
8b3c15ba21 |
Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting, even for the common one-on-one case. The dialog now only appears when two or more recipients are picked. Implementation (artifacts/tx-os/src/pages/notes.tsx): - Extracted shared `performSend()` from `confirmSubmit` so both the direct-send and confirm-then-send paths reuse the same mutation, success toast + composer close, and error toast. - `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call performSend() directly, 2+ picked = open the existing AlertDialog. - `confirmSubmit()` (the AlertDialog action) delegates to performSend. - No changes to API, recipient picker, AR/EN strings, or any other AlertDialog (bulk archive/delete left untouched). Tests: - Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a single recipient): no longer expects the confirm dialog; asserts /api/notes/:id/send fires directly and the composer closes. - New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs: - "single-recipient send skips the confirm dialog" (passes) - "multi-recipient send still requires the confirm dialog" (passes) Validation: tx-os tsc clean; both threshold specs pass locally; architect review APPROVED. |
||
|
|
4a48614e43 |
Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting, even for the common one-on-one case. The dialog now only appears when two or more recipients are picked. Implementation (artifacts/tx-os/src/pages/notes.tsx): - Extracted shared `performSend()` from `confirmSubmit` so both the direct-send and confirm-then-send paths reuse the same mutation, success toast + composer close, and error toast. - `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call performSend() directly, 2+ picked = open the existing AlertDialog. - `confirmSubmit()` (the AlertDialog action) delegates to performSend. - No changes to API, recipient picker, AR/EN strings, or any other AlertDialog (bulk archive/delete left untouched). Tests: - Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a single recipient): no longer expects the confirm dialog; asserts /api/notes/:id/send fires directly and the composer closes. - New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs: - "single-recipient send skips the confirm dialog" (passes) - "multi-recipient send still requires the confirm dialog" (passes) Validation: tx-os tsc clean; both threshold specs pass locally; architect review APPROVED. |
||
|
|
9d60b35955 |
Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting, even for the common one-on-one case. The dialog now only appears when two or more recipients are picked. Implementation (artifacts/tx-os/src/pages/notes.tsx): - Extracted shared `performSend()` from `confirmSubmit` so both the direct-send and confirm-then-send paths reuse the same mutation, success toast + composer close, and error toast. - `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call performSend() directly, 2+ picked = open the existing AlertDialog. - `confirmSubmit()` (the AlertDialog action) delegates to performSend. - No changes to API, recipient picker, AR/EN strings, or any other AlertDialog (bulk archive/delete left untouched). Tests: - Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a single recipient): no longer expects the confirm dialog; asserts /api/notes/:id/send fires directly and the composer closes. - New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs: - "single-recipient send skips the confirm dialog" (passes) - "multi-recipient send still requires the confirm dialog" (passes) Validation: tx-os tsc clean; both threshold specs pass locally; architect review APPROVED. |
||
|
|
8416760edf |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 3d96a423-5e71-468a-a9ff-dfb59d98eb88 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v Replit-Helium-Checkpoint-Created: true |
||
|
|
fe84e43e78 |
Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports: - Custom image upload (or fall back to Lucide icon) via the existing ServiceImageUploader; rendered on the home launcher when set. - Open mode picker: internal (default), external_tab (window.open), external_iframe (renders inside /embedded/:id). External URL input shown conditionally and required when an external mode is chosen. - Internal route field is hidden entirely when an external mode is selected, and locked (readOnly + lock hint) when editing a built-in app. Slug input is also locked (readOnly) for built-in apps so the built-in identity cannot drift via the form. Backend: - apps schema gains image_url, external_url, open_mode (default 'internal'); drizzle-kit push applied. - New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS + isBuiltinAppSlug. Exposed via subpath export `@workspace/db/built-in-apps`; the file has zero imports so the browser bundle uses it without pulling in `pg`. tx-os now imports it directly — duplicate FE constant removed. - Built-in slug list: services, notifications, admin, notes, my-orders, orders-incoming, executive-meetings (everything with a hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar / documents are seeded but admin-defined and remain editable. - PATCH /apps/:id rejects route changes whose previous slug is built-in with 400 + code='builtin_route_locked'. Same-route no-op is allowed; non-route updates on built-ins still work. - PATCH /apps/:id ALSO rejects slug changes when the previous slug is built-in (code='builtin_slug_locked'). UpdateAppBody zod schema intentionally omits slug, so we inspect req.body.slug raw before zod stripping. Closes the 2-step bypass: rename slug (allowed) → change route (now previous.slug looks non-built-in, allowed). - POST /apps and PATCH /apps/:id reject externalUrl values that are not http:// or https:// (code='invalid_external_url'). Prevents shipping javascript:/data:/file: payloads tenant-wide via launcher. Other: - New SPA route /embedded/:id and embedded-app page (iframe host with back + open-in-new-tab + error/not-embeddable states). - OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran. - en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*, builtinPathLocked, embeddedFrame.*. - scripts/src/seed.ts: drift guard throws if a seeded built-in slug uses a route that does not match the hardcoded SPA route. Tests: - New API test apps-builtin-route-lock.test.mjs (6/6 pass): reject built-in route change, allow non-route built-in updates, allow non-builtin route changes, reject built-in slug change (anti-bypass), reject non-http(s) externalUrl scheme + accept https, allow built-in same-route no-op. - New Playwright E2E admin-app-image-external-embedded.spec.mjs (passes): launcher renders custom image_url, external_tab opens external URL via window.open, external_iframe navigates to /embedded/:id and renders <iframe src=externalUrl>. Out of scope (pre-existing, not introduced here): - Failing tests in executive-meetings-* and tsc errors in api-server/src/routes/executive-meetings.ts. |
||
|
|
0ef93920d5 |
Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports: - Custom image upload (or fall back to Lucide icon) via the existing ServiceImageUploader; rendered on the home launcher when set. - Open mode picker: internal (default), external_tab (window.open), external_iframe (renders inside /embedded/:id). External URL input shown conditionally and required when an external mode is chosen. - Internal route field is hidden entirely when an external mode is selected, and locked (readOnly + lock hint) when editing a built-in app. Slug input is also locked (readOnly) for built-in apps so the built-in identity cannot drift via the form. Backend: - apps schema gains image_url, external_url, open_mode (default 'internal'); drizzle-kit push applied. - New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS + isBuiltinAppSlug. Exposed via subpath export `@workspace/db/built-in-apps`; the file has zero imports so the browser bundle uses it without pulling in `pg`. tx-os now imports it directly — duplicate FE constant removed. - Built-in slug list: services, notifications, admin, notes, my-orders, orders-incoming, executive-meetings (everything with a hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar / documents are seeded but admin-defined and remain editable. - PATCH /apps/:id rejects route changes whose previous slug is built-in with 400 + code='builtin_route_locked'. Same-route no-op is allowed; non-route updates on built-ins still work. - PATCH /apps/:id ALSO rejects slug changes when the previous slug is built-in (code='builtin_slug_locked'). UpdateAppBody zod schema intentionally omits slug, so we inspect req.body.slug raw before zod stripping. Closes the 2-step bypass: rename slug (allowed) → change route (now previous.slug looks non-built-in, allowed). - POST /apps and PATCH /apps/:id reject externalUrl values that are not http:// or https:// (code='invalid_external_url'). Prevents shipping javascript:/data:/file: payloads tenant-wide via launcher. Other: - New SPA route /embedded/:id and embedded-app page (iframe host with back + open-in-new-tab + error/not-embeddable states). - OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran. - en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*, builtinPathLocked, embeddedFrame.*. - scripts/src/seed.ts: drift guard throws if a seeded built-in slug uses a route that does not match the hardcoded SPA route. Tests: - New API test apps-builtin-route-lock.test.mjs (6/6 pass): reject built-in route change, allow non-route built-in updates, allow non-builtin route changes, reject built-in slug change (anti-bypass), reject non-http(s) externalUrl scheme + accept https, allow built-in same-route no-op. - New Playwright E2E admin-app-image-external-embedded.spec.mjs (passes): launcher renders custom image_url, external_tab opens external URL via window.open, external_iframe navigates to /embedded/:id and renders <iframe src=externalUrl>. Out of scope (pre-existing, not introduced here): - Failing tests in executive-meetings-* and tsc errors in api-server/src/routes/executive-meetings.ts. |
||
|
|
6a01ce852a |
Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports: - Custom image upload (or fall back to Lucide icon) via the existing ServiceImageUploader; rendered on the home launcher when set. - Open mode picker: internal (default), external_tab (window.open), external_iframe (renders inside /embedded/:id). External URL input shown conditionally and required when an external mode is chosen. - Internal route field is hidden entirely when an external mode is selected, and locked (readOnly + lock hint) when editing a built-in app. Slug input is also locked (readOnly) for built-in apps so the built-in identity cannot drift via the form. Backend: - apps schema gains image_url, external_url, open_mode (default 'internal'); drizzle-kit push applied. - New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS + isBuiltinAppSlug. Exposed via subpath export `@workspace/db/built-in-apps`; the file has zero imports so the browser bundle uses it without pulling in `pg`. tx-os now imports it directly — duplicate FE constant removed. - Built-in slug list: services, notifications, admin, notes, my-orders, orders-incoming, executive-meetings (everything with a hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar / documents are seeded but admin-defined and remain editable. - PATCH /apps/:id rejects route changes whose previous slug is built-in with 400 + code='builtin_route_locked'. Same-route no-op is allowed; non-route updates on built-ins still work. - PATCH /apps/:id ALSO rejects slug changes when the previous slug is built-in (code='builtin_slug_locked'). UpdateAppBody zod schema intentionally omits slug, so we inspect req.body.slug raw before zod stripping. Closes the 2-step bypass: rename slug (allowed) → change route (now previous.slug looks non-built-in, allowed). - POST /apps and PATCH /apps/:id reject externalUrl values that are not http:// or https:// (code='invalid_external_url'). Prevents shipping javascript:/data:/file: payloads tenant-wide via launcher. Other: - New SPA route /embedded/:id and embedded-app page (iframe host with back + open-in-new-tab + error/not-embeddable states). - OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran. - en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*, builtinPathLocked, embeddedFrame.*. - scripts/src/seed.ts: drift guard throws if a seeded built-in slug uses a route that does not match the hardcoded SPA route. Tests: - New API test apps-builtin-route-lock.test.mjs (6/6 pass): reject built-in route change, allow non-route built-in updates, allow non-builtin route changes, reject built-in slug change (anti-bypass), reject non-http(s) externalUrl scheme + accept https, allow built-in same-route no-op. - New Playwright E2E admin-app-image-external-embedded.spec.mjs (passes): launcher renders custom image_url, external_tab opens external URL via window.open, external_iframe navigates to /embedded/:id and renders <iframe src=externalUrl>. Out of scope (pre-existing, not introduced here): - Failing tests in executive-meetings-* and tsc errors in api-server/src/routes/executive-meetings.ts. |
||
|
|
e7948012f4 |
Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports: - Custom image upload (or fall back to Lucide icon) via the existing ServiceImageUploader; rendered on the home launcher when set. - Open mode picker: internal (default), external_tab (window.open), external_iframe (renders inside /embedded/:id). External URL input shown conditionally and required when an external mode is chosen. Internal route field is hidden entirely when an external mode is selected, and locked (readOnly + lock hint) when editing a built-in app whose path is hardcoded in the SPA. Backend: - apps schema gains image_url, external_url, open_mode (default 'internal'); drizzle-kit push applied. - New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS + isBuiltinAppSlug. Exposed via subpath export `@workspace/db/built-in-apps`; the file has zero imports so the browser bundle can use it without pulling in `pg`. tx-os now imports it directly — duplicate FE constant removed. - Built-in slug list: services, notifications, admin, notes, my-orders, orders-incoming, executive-meetings (everything with a hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar / documents are seeded but admin-defined and remain editable. - PATCH /apps/:id rejects route changes whose previous slug is built-in with 400 + code='builtin_route_locked'. Same-route no-op is allowed; non-route updates on built-ins still work. Other: - New SPA route /embedded/:id and embedded-app page (iframe host with back + open-in-new-tab + error/not-embeddable states). - OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran. - en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*, builtinPathLocked, embeddedFrame.*. - scripts/src/seed.ts: drift guard throws if a seeded built-in slug uses a route that does not match the hardcoded SPA route. Tests: - New API test apps-builtin-route-lock.test.mjs (4/4 pass): reject built-in route change, allow non-route built-in updates, allow non-builtin route changes, allow built-in same-route no-op. - New Playwright E2E admin-app-image-external-embedded.spec.mjs (passes): launcher renders custom image_url, external_tab opens external URL via window.open, external_iframe navigates to /embedded/:id and renders <iframe src=externalUrl>. Out of scope (pre-existing, not introduced here): - 3 failing tests in executive-meetings-* and tsc errors in api-server/src/routes/executive-meetings.ts. |
||
|
|
a794f92e61 |
Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports: - Custom image upload (or fall back to Lucide icon) via the existing ServiceImageUploader; rendered on the home launcher when set. - Open mode picker: internal (default), external_tab (window.open), external_iframe (renders inside /embedded/:id). External URL input shown conditionally and required by the form when an external mode is chosen. - Route field is locked (readOnly + lock hint) when editing a built-in app, since those slugs are hardcoded in the SPA router. Backend: - apps schema gains image_url, external_url, open_mode (default 'internal'); drizzle-kit push applied. - New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS + isBuiltinAppSlug, re-exported from lib/db. - PATCH /apps/:id rejects route changes whose previous slug is built-in with 400 + code='builtin_route_locked'. Same-route no-op is allowed; non-route updates on built-ins still work. Other: - New SPA route /embedded/:id and embedded-app page (iframe host with back + open-in-new-tab + error/not-embeddable states). - OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran. - en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*, builtinPathLocked, embeddedFrame.*. - New tests in apps-builtin-route-lock.test.mjs (4/4 pass) covering reject built-in route change, allow non-route built-in updates, allow non-builtin route changes, allow built-in same-route no-op. Notes / drift: - BUILTIN_APP_SLUGS is duplicated inline in admin.tsx (BUILTIN_APP_SLUGS_FE) because the browser bundle cannot import @workspace/db (pulls pg). Comment points at the canonical source; drift risk filed as a follow-up. - Pre-existing failures unrelated to this task: 3 tests in executive-meetings-* and tsc errors in api-server/src/routes/executive-meetings.ts. Out of scope. |
||
|
|
d8baac9317 |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 7246338f-f26e-4011-956c-63019169db8b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qqtfARh Replit-Helium-Checkpoint-Created: true |
||
|
|
f62969dc53 |
Task #515: Reply to a note from the popup without leaving the page
The floating "new note" popup's Reply button used to navigate to /notes?thread=X&reply=1, yanking the user out of whatever page they were on (Tasks, Calendar, etc.). Now Reply opens an inline composer inside the popup card itself. Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx): - New composerOpen / replyText / justSent state, reset whenever the popup head rotates so a half-typed reply never leaks across payloads. - handleReply now opens the inline composer and focuses the textarea (rAF) instead of routing. Acknowledge (mark-as-read) is deferred to successful send so Cancel keeps the note unread. - handleSubmitReply uses the existing useReplyToNote hook. Resolves recipientUserId from replyPayload.replier.id for the reply variant (owner replying back), undefined for the note variant (recipient replying — server infers the original sender). - On success: brief on-card "Reply sent" confirmation (aria-live), then auto-dismiss after 700ms. On failure: destructive toast with the textarea preserved. - Composer textarea: Enter (or Cmd/Ctrl+Enter) sends, Shift+Enter inserts a newline. Esc cancels the composer and is handled by the popup's global keydown listener so it works whether focus is in the textarea or on the Send/Cancel buttons. dir="auto" so the user's reply renders RTL/LTR per its own characters. - pointerdown/click stopPropagation on the composer wrapper so typing doesn't accidentally drag the card. Locale keys added to ar.json + en.json under notes.popup: inlineReplyPlaceholder, sendReply, cancelReply, replySending, replySent, replyFailed. Pre-existing failing tests (groups-crud, executive-meetings- notifications, executive-meetings-postpone-race) are unrelated to this change. |
||
|
|
a55fb16c4c |
Task #515: Reply to a note from the popup without leaving the page
The floating "new note" popup's Reply button used to navigate to /notes?thread=X&reply=1, yanking the user out of whatever page they were on (Tasks, Calendar, etc.). Now Reply opens an inline composer inside the popup card itself. Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx): - New composerOpen / replyText / justSent state, reset whenever the popup head rotates so a half-typed reply never leaks across payloads. - handleReply now opens the inline composer and focuses the textarea (rAF) instead of routing. Acknowledge (mark-as-read) is deferred to successful send so Cancel keeps the note unread. - handleSubmitReply uses the existing useReplyToNote hook. Resolves recipientUserId from replyPayload.replier.id for the reply variant (owner replying back), undefined for the note variant (recipient replying — server infers the original sender). - On success: brief on-card "Reply sent" confirmation (aria-live), then auto-dismiss after 700ms. On failure: destructive toast with the textarea preserved. - Composer textarea: Cmd/Ctrl+Enter to send, plain Enter for newline, Esc to cancel (stops propagation so the global Esc listener doesn't dismiss the whole popup with an unsent reply). dir="auto" so the user's reply renders RTL/LTR per its own characters. - pointerdown/click stopPropagation on the composer wrapper so typing doesn't accidentally drag the card. Locale keys added to ar.json + en.json under notes.popup: inlineReplyPlaceholder, sendReply, cancelReply, replySending, replySent, replyFailed. Pre-existing failing tests (groups-crud, executive-meetings- notifications, executive-meetings-postpone-race) are unrelated to this change. |
||
|
|
75b3eaf64a |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 6c929b19-b82e-4881-b543-65571270a2f9 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qqtfARh Replit-Helium-Checkpoint-Created: true |
||
|
|
b7b8c395f4 |
Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone in a full-width row above the content row, while the "المجلدات" FoldersRail started in the row below. Composer appeared high, rail visibly lower with empty space between them. Change: - Switched the folders+content row in `artifacts/tx-os/src/pages/notes.tsx` from flex to CSS grid: `grid md:grid-cols-[14rem_minmax(0,1fr)]`. Composer is placed in col 2 row 1, FoldersRail in col 1 (row-span-2 when composer is shown), content in col 2 row 2. Result: composer's top edge aligns with the rail's top edge on desktop and the empty gap is gone. - On mobile (`grid-cols-1`), `order-1 / order-2 / order-3` give the exact original mobile reading order: composer → rail → content. - Added a `className?: string` prop to `FoldersRail` (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended to its root `<aside>`, so the page can pass grid placement classes. - Hidden-composer views (Inbox / Sent / Archived / Shared-with-me) collapse cleanly via conditional `md:row-span-2` on the rail and conditional `md:row-start-1 / md:row-start-2` on the content column — desktop becomes a simple two-cell row and mobile stays rail → content, matching the original behavior. - Grid-cols template, `md:col-start-2` on composer, and `md:col-start-2`/`md:row-start-*` on the content column are all GATED on `showFolders`. In views where the folders rail is hidden (Received/Inbox, Sent), the wrapper is a single-column grid with no left gutter and content takes the full width — preserving the original full-width layout for those views. Behavior preserved: - `showTopComposer` gating unchanged. - `data-testid="notes-top-composer"` preserved. - FoldersRail's existing internal classes unchanged; new className prop is purely additive. - Mobile reading order in active/non-shared view: composer → rail → content (same as before this task, when composer was a standalone block above the row). - Mobile reading order in hidden-composer views: rail → content (unchanged). Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit` passes. No test files reference the composer block by structural selectors that the move would break. No deviations from the plan. |
||
|
|
cb6b067386 |
Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone in a full-width row above the content row, while the "المجلدات" FoldersRail started in the row below. Composer appeared high, rail visibly lower with empty space between them. Change: - Switched the folders+content row in `artifacts/tx-os/src/pages/notes.tsx` from flex to CSS grid: `grid md:grid-cols-[14rem_minmax(0,1fr)]`. Composer is placed in col 2 row 1, FoldersRail in col 1 (row-span-2 when composer is shown), content in col 2 row 2. Result: composer's top edge aligns with the rail's top edge on desktop and the empty gap is gone. - On mobile (`grid-cols-1`), `order-1 / order-2 / order-3` give the exact original mobile reading order: composer → rail → content. - Added a `className?: string` prop to `FoldersRail` (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended to its root `<aside>`, so the page can pass grid placement classes. - Hidden-composer views (Inbox / Sent / Archived / Shared-with-me) collapse cleanly via conditional `md:row-span-2` on the rail and conditional `md:row-start-1 / md:row-start-2` on the content column — desktop becomes a simple two-cell row and mobile stays rail → content, matching the original behavior. Behavior preserved: - `showTopComposer` gating unchanged. - `data-testid="notes-top-composer"` preserved. - FoldersRail's existing internal classes unchanged; new className prop is purely additive. - Mobile reading order in active/non-shared view: composer → rail → content (same as before this task, when composer was a standalone block above the row). - Mobile reading order in hidden-composer views: rail → content (unchanged). Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit` passes. No test files reference the composer block by structural selectors that the move would break. No deviations from the plan. |
||
|
|
d1875141aa |
Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone in a full-width row above the content row, while the "المجلدات" FoldersRail started in the row below. Composer appeared high, rail visibly lower with empty space between them. Change: - Moved the top composer out of its standalone full-width block and into the left content column (`flex-1 min-w-0`) of the existing folders+content row in `artifacts/tx-os/src/pages/notes.tsx`. Both now share the same row, so the composer's top edge aligns with the rail's top edge on desktop. - Added a `className?: string` prop to `FoldersRail` (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended to its root <aside>, so the page can pass `order-*` classes. - On narrow widths (`flex-col`) the composer column gets `order-1 md:order-2` and the rail gets `order-2 md:order-1` ONLY when `showTopComposer` is true. In hidden-composer views (Inbox / Sent / Archived / Shared-with-me) no order classes are applied, so the rail/content stacking order on mobile stays exactly as it was before this task. Result: composer stacks ABOVE the rail on mobile in My Notes (preserving the original mobile reading order) and sits beside it on md+, with zero behavior change in the other views. Behavior preserved: - `showTopComposer` gating unchanged — still hidden on Inbox / Sent / Archived / Shared-with-me. - `data-testid="notes-top-composer"` preserved. - FoldersRail's existing width/scroll classes unchanged; new className is purely additive. - Stale comment about composer being "above the folders area" updated to describe the new in-column placement and the order-* mobile behavior. Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit` passes. No test files reference the composer block by structural selectors that the move would break. No deviations from the plan. |
||
|
|
944a894a88 |
Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone in a full-width row above the content row, while the "المجلدات" FoldersRail started in the row below. Composer appeared high, rail visibly lower with empty space between them. Change: - Moved the top composer out of its standalone full-width block and into the left content column (`flex-1 min-w-0`) of the existing folders+content row in `artifacts/tx-os/src/pages/notes.tsx`. Both now share the same row, so the composer's top edge aligns with the rail's top edge on desktop. - Added a `className?: string` prop to `FoldersRail` (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended to its root <aside>, so the page can pass `order-*` classes. - On narrow widths (`flex-col`) the composer column gets `order-1 md:order-2` and the rail gets `order-2 md:order-1`. Result: composer stacks ABOVE the rail on mobile (preserving the original mobile reading order) and sits beside it on md+. Behavior preserved: - `showTopComposer` gating unchanged — still hidden on Inbox / Sent / Archived / Shared-with-me. - `data-testid="notes-top-composer"` preserved. - FoldersRail's existing width/scroll classes unchanged; new className is purely additive. - Stale comment about composer being "above the folders area" updated to describe the new in-column placement and the order-* mobile behavior. Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit` passes. No test files reference the composer block by structural selectors that the move would break. No deviations from the plan. |
||
|
|
037d530471 |
Task #514: Align note composer with folders rail
Original task: On the Notes page, the "اكتب ملاحظة" composer sat alone in a full-width row above the content row, while the "المجلدات" FoldersRail started in the row below it. The result was a visible vertical mismatch — composer high, rail noticeably lower with empty space between them. Change: Moved the top composer out of its standalone full-width block and into the left content column (`flex-1 min-w-0`) at the top of the existing row that already hosts the FoldersRail. Both now share the same row, so the composer's top edge aligns with the rail's top edge. Details: - Single edit in `artifacts/tx-os/src/pages/notes.tsx`. - The composer's `data-testid="notes-top-composer"` is preserved, so any existing E2E selectors continue to work. - `showTopComposer` gating (hide on Inbox/Sent/Archived/Shared-with-me) is unchanged — only the position of the rendered block moved. - Removed the old wrapper's `px-4 pt-1 max-w-6xl w-full mx-auto` (those paddings/centering are now provided by the parent row), and added a `mb-4` for breathing room between composer and the content below it inside the column. - Responsive behavior preserved: the parent row is `flex-col md:flex-row`, so on narrow widths the composer still stacks above the folders area cleanly with no horizontal overflow. Verification: pnpm tsc --noEmit on tx-os passes; no test files reference the composer block by structural selectors that the move would break. No deviations from the plan. |
||
|
|
b86d68fe6f |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 5c0e7b92-0910-430c-8234-0ebe07f116ff Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/OajipdL Replit-Helium-Checkpoint-Created: true |
||
|
|
9a8dc3175f |
Task #512: Per-recipient Delete in Notes Inbox
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.
Backend (artifacts/api-server/src/routes/notes.ts):
- DELETE /notes/received/:id — recipient-only; 404 if no row.
- POST /notes/received/bulk-delete — body {ids:number[]}, max 500,
single SQL DELETE, returns {ok, notFound} for partial-success UI.
- Both registered before DELETE /notes/:id so Express matches /received
first.
Frontend (artifacts/tx-os):
- New hooks useDeleteReceivedNote / useBulkDeleteReceivedNotes in
src/lib/notes-api.ts; both invalidate notes + folders queries.
- Inbox bulk bar: Delete button (rose) next to Archive plus a confirm
AlertDialog with all/none/partial toasts.
- Inbox card: per-row Trash button next to the status badge with a
page-level confirm AlertDialog (testid inbox-row-delete-confirm).
- ThreadDialog: per-row Delete next to Archive plus a confirm dialog
that closes the thread on success.
- AR + EN locale strings added for all new copy.
Tests:
- artifacts/api-server/tests/notes-inbox-delete.test.mjs — recipient
delete, non-recipient/sender 404, bulk mix of valid+missing ids, and
DB-level checks that the note + other recipients survive.
- artifacts/tx-os/tests/notes-inbox-bulk-delete.spec.mjs — Playwright
flow seeds three received notes, bulk-deletes two from the inbox,
then deletes the third via the per-row card Delete button.
|
||
|
|
0988585c65 |
Task #512: Per-recipient Delete in Notes Inbox
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.
Backend (artifacts/api-server/src/routes/notes.ts):
- DELETE /notes/received/:id — recipient-only; 404 if no row.
- POST /notes/received/bulk-delete — body {ids:number[]}, max 500,
single SQL DELETE, returns {ok, notFound} for partial-success UI.
- Both registered before DELETE /notes/:id so Express matches /received
first.
Frontend (artifacts/tx-os):
- New hooks useDeleteReceivedNote / useBulkDeleteReceivedNotes in
src/lib/notes-api.ts; both invalidate notes + folders queries.
- Inbox bulk bar gets a Delete button (rose) next to Archive plus a
confirm AlertDialog with all/none/partial toasts (src/pages/notes.tsx).
- ThreadDialog gets a per-row Delete next to Archive plus a single
confirm dialog that closes the thread on success.
- AR + EN locale strings added for all new copy.
Tests:
- artifacts/api-server/tests/notes-inbox-delete.test.mjs — recipient
delete, non-recipient/sender 404, bulk mix of valid+missing ids, and
DB-level checks that the note + other recipients survive.
- artifacts/tx-os/tests/notes-inbox-bulk-delete.spec.mjs — Playwright
flow seeds three received notes, bulk-deletes two from the inbox,
then deletes the third via ThreadDialog per-row.
|
||
|
|
6878cdfe7c |
Task #511: Fully remove Chat feature
Destructive removal per user confirmation ("حذف نهائي ما يرجع").
Removed:
- API: routes/conversations.ts, schema/conversations.ts, all chat
socket handlers in src/index.ts, /admin/users/:id/dependents/
conversations+messages endpoints, conversation/message dependency
counts in users/stats routes.
- Web: pages/chat.tsx, /chat route, dock chat filter, MessageSquare
icon and messages StatCard on home, all chat-related UI in
notifications + admin (dependency badges, delete-dialog rows,
UserDependentConversations/Messages sections, count map keys).
- Locales: nav.chat, home.stats.messages, full chat.* block,
admin.deleteUser conv/msgCount, admin.users.counts.conv/msg,
admin.audit.unit.conversation_*/message_*, admin.dependents.user*.
- OpenAPI spec: tags, all /conversations/* paths, conv/msg dependent
paths, related schemas (ConversationWithDetails, MessageWithSender,
UserDependentConversation/MessageItem+Page, etc.), UserProfile and
UserDeletionConflict conv/msg fields, HomeStats.unreadMessages.
Regenerated client via orval.
- Database: dropped message_reads, messages,
conversation_participants, conversations (CASCADE); deleted
notifications with related_type='conversation' or type='chat';
deleted apps row with slug='chat'; ran drizzle push-force.
- Seed: removed chat:access permission + user-role assignment +
seeded chat app entry from scripts/src/seed.ts.
- Tests: deleted conversations-leave.test.mjs; cleaned chat refs from
list-dependency-counts, delete-force-warnings, audit-log-coverage,
and admin-inline-dependency-counts (e2e) — replaced chat dependents
with note dependents where needed for force-delete coverage.
Notes preserved: notes.tsx noConversationsYet/conversationWith refer
to NOTE THREADS (not chat) and were intentionally NOT touched.
executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
|
||
|
|
84398de390 |
Task #511: Fully remove Chat feature
Destructive removal per user confirmation ("حذف نهائي ما يرجع").
Removed:
- API: routes/conversations.ts, schema/conversations.ts, all chat
socket handlers in src/index.ts, /admin/users/:id/dependents/
conversations+messages endpoints, conversation/message dependency
counts in users/stats routes.
- Web: pages/chat.tsx, /chat route, dock chat filter, MessageSquare
icon and messages StatCard on home, all chat-related UI in
notifications + admin (dependency badges, delete-dialog rows,
UserDependentConversations/Messages sections, count map keys).
- Locales: nav.chat, home.stats.messages, full chat.* block,
admin.deleteUser conv/msgCount, admin.users.counts.conv/msg,
admin.audit.unit.conversation_*/message_*, admin.dependents.user*.
- OpenAPI spec: tags, all /conversations/* paths, conv/msg dependent
paths, related schemas (ConversationWithDetails, MessageWithSender,
UserDependentConversation/MessageItem+Page, etc.), UserProfile and
UserDeletionConflict conv/msg fields, HomeStats.unreadMessages.
Regenerated client via orval.
- Database: dropped message_reads, messages,
conversation_participants, conversations (CASCADE); deleted
notifications with related_type='conversation' or type='chat';
deleted apps row with slug='chat'; ran drizzle push-force.
- Seed: removed chat:access permission + user-role assignment +
seeded chat app entry from scripts/src/seed.ts.
- Tests: deleted conversations-leave.test.mjs; cleaned chat refs from
list-dependency-counts, delete-force-warnings, audit-log-coverage,
and admin-inline-dependency-counts (e2e) — replaced chat dependents
with note dependents where needed for force-delete coverage.
Notes preserved: notes.tsx noConversationsYet/conversationWith refer
to NOTE THREADS (not chat) and were intentionally NOT touched.
executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
|
||
|
|
e4ebaffe73 |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 9ff83fa0-239c-4735-b714-1b1bbb3dcc1d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv Replit-Helium-Checkpoint-Created: true |
||
|
|
992f98418c |
Add ability to archive multiple incoming notes at once
Introduces a bulk archive feature for the received notes tab, including API integration, UI selection state, confirmation dialogs, and internationalization support. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 7c13c05c-e8c2-461e-a5dc-94bbc8e114ac Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv Replit-Helium-Checkpoint-Created: true |
||
|
|
81d27ed8ec |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 587d9c0d-0c50-4865-ba7e-34fb698d3584 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv Replit-Helium-Checkpoint-Created: true |
||
|
|
207d2ad52b |
Improve Arabic text display by fixing clipped dots on characters
Add a global CSS rule to increase line-height for truncated Arabic text, ensuring diacritics are fully visible. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 8f7b38c9-dbac-45e9-86d9-0cb4f4a5d011 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv Replit-Helium-Checkpoint-Created: true |
||
|
|
e4b5ece2e4 |
Enhance the app editing interface with improved input hints
Add helper text and inline hints to the slug and route input fields within the app editing form to clarify their purpose and expected format. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 18fca138-6c4d-4f2b-9859-dcd578b8df5c Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv Replit-Helium-Checkpoint-Created: true |
||
|
|
d5dd3acf9f |
Task #509: Redesign Edit App modal + verify responsive sizing
User asked for the Edit App modal to match the new professional Edit
Service design and confirmed responsiveness for iPad / mobile.
Changes — admin.tsx only:
1. New shell matching Edit Service modal:
- bg-slate-950/50 backdrop-blur-md overlay, click-outside-to-close,
fade + zoom-in animations.
- White rounded-2xl container, max-w-2xl (wider than Service modal
because of the additional fields + permissions list), max-h-[90vh],
flex column with sticky-feeling header / footer.
- Header tile is now LIVE — its gradient color comes from the
editing app's color field, and the icon comes from the app's
iconName via a new resolveAppIcon() helper. So the header
previews the app as you edit.
- X close button in header.
2. Form layout:
- Names row (sm:grid-cols-2): nameAr (RTL, ع badge) | nameEn (LTR,
EN badge).
- Identifier row (sm:grid-cols-2): slug | route, both font-mono.
- Visuals row (sm:grid-cols-2):
- Icon picker: 40x40 preview tile + text input. Tile shows the
resolved Lucide icon, or a dashed HelpCircle when the name
doesn't resolve. Hint text under the field.
- Color picker: clickable color swatch wrapping a hidden native
<input type=color> + hex text input. Both stay in sync.
- Permissions wrapped in a bordered card with its own header strip.
3. Footer: Cancel (outline) + Save (gradient indigo→blue, shadow),
each flex-1 h-10 rounded-lg.
4. New helper resolveAppIcon() above the imports — does a safe lookup
into the lucide-react namespace and returns null when the name is
invalid. Mirrors the pattern already used in pages/home.tsx.
Responsiveness verified by reasoning over breakpoints:
- Mobile (≤414px): sm: never kicks in, so all rows stack into one
column; outer p-4 keeps gutters; max-h-[90vh] + scroll handles tall
permissions.
- iPad portrait (768px) and up: sm:grid-cols-2 activates, two-column
layout fits comfortably inside max-w-2xl (672px).
- Desktop: same max-w-2xl cap looks well-proportioned, not cramped.
- Edit Service modal already uses the same primitives, no changes
needed.
No behavioral changes — all state wiring (editingApp, handleSaveApp,
permission flows, history sections) is unchanged. Pure presentational
refactor.
Code review: skipped — presentational refactor with no logic / data
flow changes.
Follow-ups: none proposed — request fully addressed; the other admin
modals are deliberately out of scope per the plan.
|
||
|
|
2ee744a003 |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: e8c0dc66-20b5-41e9-a190-072f46c5078e Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv Replit-Helium-Checkpoint-Created: true |
||
|
|
c6fa183203 |
Redesign service editing window for a professional look and feel
Refactors the "Edit Service" modal UI in `admin.tsx` to improve visual hierarchy, user experience, and overall professionalism, including styling changes to the header, input fields, image uploader, and availability toggle. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 28ee2705-c2ec-476e-aa4f-cc6c7cd5624e Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv Replit-Helium-Checkpoint-Created: true |
||
|
|
a565d47027 |
Task #508: Remove unused السعر (Price) field from admin Services UI
User requested removal of the price field from the admin Services edit
form ("بالنسبه الى الاسعار أحذفها لايوجد اسعار") because there are no
prices in the product.
Changes — frontend only, all in tx-os:
- artifacts/tx-os/src/pages/admin.tsx
- Drop `price: string` from ServiceForm type.
- Drop `price: "0.00"` from emptyServiceForm.
- Remove the price entry from the edit dialog's field array
(no more السعر input).
- Remove the `<div>{service.price}</div>` from the services list.
- Remove the `price` line from the edit-load mapper.
- artifacts/tx-os/src/locales/ar.json + en.json
- Drop the now-unused `servicePrice` key.
Left intact:
- The generic `"price"` key in locales (line 104) — kept conservatively
in case anything outside this scope expects it, though ripgrep shows
no current usage.
- The `services.price` DB column and api-server code — unused in api
src; only test fixtures insert a placeholder. Removing would require
a migration and break tests for zero benefit.
- The user-facing services page — never displayed price.
Verification:
- ripgrep confirms zero `price` / `servicePrice` references remain in
artifacts/tx-os/src/pages/admin.tsx and the locales (other than the
generic shared key).
- Vite HMR applied cleanly with no TS errors.
- Pre-existing test failures (executive-meetings, service-orders) are
unrelated and already tracked as separate project tasks.
Code review: skipped — change is a 5-line mechanical removal of dead
UI with no logic or data-flow implications.
Follow-ups: none proposed — this is a self-contained, complete fix.
|
||
|
|
a4f89405e7 |
Transitioned from Plan to Build mode
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 76d5058d-35b6-4d73-912b-cbac4c490163 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/ZXx6nVc Replit-Helium-Checkpoint-Created: true |
||
|
|
b87f9224a7 |
Task #507: Stop .os-bg from overriding sticky on its direct children
User report: after #505 the admin "لوحة التحكم" header looked pinned at the top of the page, but disappeared as soon as the user scrolled down. Same bug silently affected every page wrapped in `.os-bg`. Root cause (artifacts/tx-os/src/index.css, lines 190-193): .os-bg > * { position: relative; z-index: 1; } This rule was meant to lift page content above the decorative gradient blobs (`.os-bg::before/::after`, position:fixed z-index:0). But by setting `position: relative` on every direct child it also overrode any `position: sticky` declared via Tailwind utilities on those children — equal-specificity class selectors, with index.css loaded after Tailwind utilities, so .os-bg won source-order. The "sticky" headers therefore behaved as plain in-flow elements and scrolled away with the rest of the content. Fix (one CSS rule): - Replace `position: relative; z-index: 1` with `isolation: isolate`. - `isolation: isolate` creates a new stacking context (so children still paint above the z-index:0 blobs) without forcing them to position:relative — sticky/fixed children now work as written. No markup changes anywhere. Followed Option A in the task plan exactly. Implication: every sticky header across the app (admin, home, chat, services, notifications) is now actually pinned on scroll. Decorative blobs continue to sit behind page content because the new stacking context still paints over the parent's z-index 0 background layer. No tests were modified; existing tx-os tests are presentation- agnostic and unaffected. |