82ae63f88ed32f3525e6afaa62462f00f2557ac7
171 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
82ae63f88e |
Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
attendees column widest, tighter padding, navy/white/gray + red highlight
only. Header label "م" → "#" (ar.json).
Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
requests work without an existing meeting. db push applied.
Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full
rewrite:
- ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained
requireMutate / requireApprove / requireRequest / requireAdminAudit on the
appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
{status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
{action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a
single shared upsertFontSettingsHandler (no Express method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- GET /notifications now accepts ?date=YYYY-MM-DD and joins through the
meetings table to scope notifications to the selected day; returns []
immediately when no meetings exist on that date.
- Removed all `as never` casts via $inferInsert/Partial typing.
Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin +
executive_*).
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized
users never see Manage / Approvals / Audit / Tasks tabs (matrix in the
in-file isSectionVisible helper).
- Approvals: review() sends {status, reviewNotes}; new "Send back for edits"
(needs_edit) button beside Approve / Reject.
- Requests default scope is "mine" for non-approvers and "all" for approvers
(no over-exposure to plain requesters).
- Notifications visibility uses canRead AND the query is scoped by the same
selected `date` as the schedule (queryKey + ?date=… included).
- PdfSection: primary "Print + Archive" button (testid em-print) does archive
then print in one click; em-print-only and em-archive remain as separate
buttons.
- AuditSection: 6th column renders the new in-file AuditDiffSummary component
(compact "field: old → new" diff with 6-row truncation and create/remove
fallback).
- New tWithFallback(t, key, fallback) helper used in audit + notifications
cells (avoids strict TFunction overload errors); apiJson rewritten to
extract body/headers separately and stringify rawBody, eliminating the
"Record<string, unknown> not assignable to BodyInit" TS errors. tsc
--noEmit on executive-meetings.tsx is now clean.
- Print page (executive-meetings-print.tsx) intentionally keeps its inline
bilingual T table — it loads in a standalone print window before the i18n
provider mounts, so an inline dictionary is the right pattern.
i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
.audit.col.changes, .audit.{created, removed, moreChanges},
.audit.entity.pdf_archive,
.audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate,
replace_attendees, done},
.pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly,
archivedAndPrinted}.
Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201, nested
POST /:id/requests 201, PATCH /requests/:id status approval 200,
audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
200 with persisted data, /notifications?date=YYYY-MM-DD filters
correctly.
- tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is
clean (pre-existing admin.tsx errors are unrelated to this task).
- Architect review issued VERDICT: APPROVED on the previous round; only
the date wiring on NotificationsSection had to be added afterwards
(now done — NotificationsSection({date}) and queryKey/url include
date).
Validation-round-5 fixes (this commit):
- Coordinator task scoping (server-side, blocking RBAC fix). Added
TASK_BROAD_VIEW_ROLES (admin + office_manager + coord_lead). Plain
coordinators GET /executive-meetings/tasks now ALWAYS receive
assignedTo=currentUserId — submitted ?mine=0 / ?assigneeId=other are
ignored. /me also exposes a new canViewAllTasks flag for the UI.
- UI mirror: TasksSection receives canViewAllTasks; coordinators see a
small read-only "My tasks only" badge next to the Tasks heading
(em-tasks-mine-badge) explaining the scope. New i18n keys
executiveMeetings.tasks.{myTasksOnly,myTasksOnlyHelp} (AR + EN).
- Seed: scripts/src/seed.ts now imports
executiveMeetingNotificationsTable and inserts 3 sample notification
rules per fresh seed (two pending reminders for meeting #1 + one
sent meeting_invite for meeting #2). Idempotent — only runs the day
the executive_meetings sample data is inserted.
- Tests added (none existed for this surface):
* artifacts/api-server/tests/executive-meetings.test.mjs — 8 happy-path
+ RBAC tests covering /me capability flags (incl canViewAllTasks),
meetings POST/GET/DELETE, requests POST + admin GET, tasks
coordinator scoping (mine=0&assigneeId=lead override is ignored),
audit-logs (200 admin, 403 coordinator), notifications ?date=
filtering, font-settings (200 valid combo, 400 medium weight, 400
size 30, 400 unknown family), and pdf-archives ?date=. All 8 pass.
* artifacts/tx-os/tests/executive-meetings-manage-create.spec.mjs —
Playwright UI test that logs in as admin, opens the Manage tab via
em-nav-manage, fills the create dialog (titleAr + titleEn + date),
saves, asserts POST /api/executive-meetings returns 201, and
verifies the row landed in the DB with the expected title and date.
Validation-round-4 fixes:
- Manage attendee editor: added per-row title input alongside name +
deterministic ChevronUp/ChevronDown reorder controls (sort order is
recomputed on save). New i18n keys
executiveMeetings.manage.attendees.{moveUp,moveDown}.
- Manage tab: per-row Copy button opens a new "Duplicate to date" dialog
that calls POST /executive-meetings/:id/duplicate with a target date
picker; on success it switches the day view to the chosen date. New
i18n keys executiveMeetings.manage.{duplicate, duplicateToDate,
duplicated, duplicateFailed}.
- Print page (executive-meetings-print.tsx) now uses react-i18next with
new locale block executiveMeetings.print.* (AR + EN parity), forces
i18n.changeLanguage(lang) so ?lang=en always prints English. Attendees
cell in the print table is now textAlign: center (matches the Step 1
schedule).
- Font settings tightened to spec: families = system/Cairo/Tajawal/
Noto Naskh Arabic/Amiri (dropped Inter, monospace), weights =
regular/bold (dropped medium, semibold), alignment = start/center
(dropped end), size range = 12–22 (was 10–32). Enforced server-side
in fontSettingsSchema (Zod) and mirrored in the UI selects/range.
Verified: PATCH font-settings returns 200 for valid combo, 400 for
fontWeight="medium", 400 for fontSize=30.
Validation-round-3 fixes:
- Tasks RBAC tightened end-to-end: new TASK_VIEW_ROLES = admin +
office_manager + coord_lead + coordinator. GET /tasks and
PATCH /tasks/:id now require requireTaskView. /me exposes new
canViewTasks flag. Frontend isSectionVisible("tasks") gated on
canViewTasks (so CEO/viewer never sees the Tasks tab or hits the
endpoint). Verified: admin /me returns canViewTasks=true.
- GET /requests gained dateFrom + dateTo (createdAt range) and a
requester filter that is honored only for approver/audit roles
(non-approvers stay locked to their own requests). Verified curl
with ?dateFrom=...&dateTo=... returns 200.
- Task dueAt schema accepts BOTH YYYY-MM-DD (from <input type="date">)
and full ISO datetime; date-only is normalized to start-of-day UTC,
empty string clears the field. Verified curl POST /tasks with
{"dueAt":"2026-12-31"} returns 201 with stored
dueAt = 2026-12-31T00:00:00.000Z.
Validation-round-2 fixes:
- GET /executive-meetings/requests: added server-side least-privilege so
only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every
other executive role is force-scoped to requestedBy = self regardless
of the client's `mine` flag (closes the RBAC overexposure flag).
- isSectionVisible("tasks") now also returns true for canSubmitRequest,
so executive_coordinator sees the Tasks tab (coordinators execute the
follow-ups generated from approved requests).
- Requests "new request" dropdown now exposes ALL twelve backend request
types: create, edit, delete, reschedule, add_attendee, remove_attendee,
change_location, cancel_meeting, note, highlight, unhighlight, other.
- i18n parity: added en.json + ar.json keys for the five missing request
types (add_attendee, remove_attendee, change_location, cancel_meeting,
note) and the two missing statuses (needs_edit, done) under
executiveMeetings.requests.{type,status}.
- Fixed wrong key reference: reschedule form's date row was using
manage.field.date (does not exist) — switched to manage.field.meetingDate.
Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112):
- Real notifications delivery, server-side PDF rendering, attendee
reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync,
expanded seed data including executive_meeting_notifications sample
rows.
|
||
|
|
b46e55a394 |
Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
attendees column widest, tighter padding, navy/white/gray + red highlight
only. Header label "م" → "#" (ar.json).
Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
requests work without an existing meeting. db push applied.
Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full
rewrite:
- ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained
requireMutate / requireApprove / requireRequest / requireAdminAudit on the
appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
{status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
{action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a
single shared upsertFontSettingsHandler (no Express method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- GET /notifications now accepts ?date=YYYY-MM-DD and joins through the
meetings table to scope notifications to the selected day; returns []
immediately when no meetings exist on that date.
- Removed all `as never` casts via $inferInsert/Partial typing.
Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin +
executive_*).
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized
users never see Manage / Approvals / Audit / Tasks tabs (matrix in the
in-file isSectionVisible helper).
- Approvals: review() sends {status, reviewNotes}; new "Send back for edits"
(needs_edit) button beside Approve / Reject.
- Requests default scope is "mine" for non-approvers and "all" for approvers
(no over-exposure to plain requesters).
- Notifications visibility uses canRead AND the query is scoped by the same
selected `date` as the schedule (queryKey + ?date=… included).
- PdfSection: primary "Print + Archive" button (testid em-print) does archive
then print in one click; em-print-only and em-archive remain as separate
buttons.
- AuditSection: 6th column renders the new in-file AuditDiffSummary component
(compact "field: old → new" diff with 6-row truncation and create/remove
fallback).
- New tWithFallback(t, key, fallback) helper used in audit + notifications
cells (avoids strict TFunction overload errors); apiJson rewritten to
extract body/headers separately and stringify rawBody, eliminating the
"Record<string, unknown> not assignable to BodyInit" TS errors. tsc
--noEmit on executive-meetings.tsx is now clean.
- Print page (executive-meetings-print.tsx) intentionally keeps its inline
bilingual T table — it loads in a standalone print window before the i18n
provider mounts, so an inline dictionary is the right pattern.
i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
.audit.col.changes, .audit.{created, removed, moreChanges},
.audit.entity.pdf_archive,
.audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate,
replace_attendees, done},
.pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly,
archivedAndPrinted}.
Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201, nested
POST /:id/requests 201, PATCH /requests/:id status approval 200,
audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
200 with persisted data, /notifications?date=YYYY-MM-DD filters
correctly.
- tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is
clean (pre-existing admin.tsx errors are unrelated to this task).
- Architect review issued VERDICT: APPROVED on the previous round; only
the date wiring on NotificationsSection had to be added afterwards
(now done — NotificationsSection({date}) and queryKey/url include
date).
Validation-round-4 fixes (this commit):
- Manage attendee editor: added per-row title input alongside name +
deterministic ChevronUp/ChevronDown reorder controls (sort order is
recomputed on save). New i18n keys
executiveMeetings.manage.attendees.{moveUp,moveDown}.
- Manage tab: per-row Copy button opens a new "Duplicate to date" dialog
that calls POST /executive-meetings/:id/duplicate with a target date
picker; on success it switches the day view to the chosen date. New
i18n keys executiveMeetings.manage.{duplicate, duplicateToDate,
duplicated, duplicateFailed}.
- Print page (executive-meetings-print.tsx) now uses react-i18next with
new locale block executiveMeetings.print.* (AR + EN parity), forces
i18n.changeLanguage(lang) so ?lang=en always prints English. Attendees
cell in the print table is now textAlign: center (matches the Step 1
schedule).
- Font settings tightened to spec: families = system/Cairo/Tajawal/
Noto Naskh Arabic/Amiri (dropped Inter, monospace), weights =
regular/bold (dropped medium, semibold), alignment = start/center
(dropped end), size range = 12–22 (was 10–32). Enforced server-side
in fontSettingsSchema (Zod) and mirrored in the UI selects/range.
Verified: PATCH font-settings returns 200 for valid combo, 400 for
fontWeight="medium", 400 for fontSize=30.
Validation-round-3 fixes:
- Tasks RBAC tightened end-to-end: new TASK_VIEW_ROLES = admin +
office_manager + coord_lead + coordinator. GET /tasks and
PATCH /tasks/:id now require requireTaskView. /me exposes new
canViewTasks flag. Frontend isSectionVisible("tasks") gated on
canViewTasks (so CEO/viewer never sees the Tasks tab or hits the
endpoint). Verified: admin /me returns canViewTasks=true.
- GET /requests gained dateFrom + dateTo (createdAt range) and a
requester filter that is honored only for approver/audit roles
(non-approvers stay locked to their own requests). Verified curl
with ?dateFrom=...&dateTo=... returns 200.
- Task dueAt schema accepts BOTH YYYY-MM-DD (from <input type="date">)
and full ISO datetime; date-only is normalized to start-of-day UTC,
empty string clears the field. Verified curl POST /tasks with
{"dueAt":"2026-12-31"} returns 201 with stored
dueAt = 2026-12-31T00:00:00.000Z.
Validation-round-2 fixes:
- GET /executive-meetings/requests: added server-side least-privilege so
only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every
other executive role is force-scoped to requestedBy = self regardless
of the client's `mine` flag (closes the RBAC overexposure flag).
- isSectionVisible("tasks") now also returns true for canSubmitRequest,
so executive_coordinator sees the Tasks tab (coordinators execute the
follow-ups generated from approved requests).
- Requests "new request" dropdown now exposes ALL twelve backend request
types: create, edit, delete, reschedule, add_attendee, remove_attendee,
change_location, cancel_meeting, note, highlight, unhighlight, other.
- i18n parity: added en.json + ar.json keys for the five missing request
types (add_attendee, remove_attendee, change_location, cancel_meeting,
note) and the two missing statuses (needs_edit, done) under
executiveMeetings.requests.{type,status}.
- Fixed wrong key reference: reschedule form's date row was using
manage.field.date (does not exist) — switched to manage.field.meetingDate.
Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112):
- Real notifications delivery, server-side PDF rendering, attendee
reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync,
expanded seed data including executive_meeting_notifications sample
rows.
|
||
|
|
ee6b8523b1 |
Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
attendees column widest, tighter padding, navy/white/gray + red highlight
only. Header label "م" → "#" (ar.json).
Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
requests work without an existing meeting. db push applied.
Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full
rewrite:
- ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained
requireMutate / requireApprove / requireRequest / requireAdminAudit on the
appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
{status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
{action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a
single shared upsertFontSettingsHandler (no Express method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- GET /notifications now accepts ?date=YYYY-MM-DD and joins through the
meetings table to scope notifications to the selected day; returns []
immediately when no meetings exist on that date.
- Removed all `as never` casts via $inferInsert/Partial typing.
Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin +
executive_*).
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized
users never see Manage / Approvals / Audit / Tasks tabs (matrix in the
in-file isSectionVisible helper).
- Approvals: review() sends {status, reviewNotes}; new "Send back for edits"
(needs_edit) button beside Approve / Reject.
- Requests default scope is "mine" for non-approvers and "all" for approvers
(no over-exposure to plain requesters).
- Notifications visibility uses canRead AND the query is scoped by the same
selected `date` as the schedule (queryKey + ?date=… included).
- PdfSection: primary "Print + Archive" button (testid em-print) does archive
then print in one click; em-print-only and em-archive remain as separate
buttons.
- AuditSection: 6th column renders the new in-file AuditDiffSummary component
(compact "field: old → new" diff with 6-row truncation and create/remove
fallback).
- New tWithFallback(t, key, fallback) helper used in audit + notifications
cells (avoids strict TFunction overload errors); apiJson rewritten to
extract body/headers separately and stringify rawBody, eliminating the
"Record<string, unknown> not assignable to BodyInit" TS errors. tsc
--noEmit on executive-meetings.tsx is now clean.
- Print page (executive-meetings-print.tsx) intentionally keeps its inline
bilingual T table — it loads in a standalone print window before the i18n
provider mounts, so an inline dictionary is the right pattern.
i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
.audit.col.changes, .audit.{created, removed, moreChanges},
.audit.entity.pdf_archive,
.audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate,
replace_attendees, done},
.pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly,
archivedAndPrinted}.
Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201, nested
POST /:id/requests 201, PATCH /requests/:id status approval 200,
audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
200 with persisted data, /notifications?date=YYYY-MM-DD filters
correctly.
- tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is
clean (pre-existing admin.tsx errors are unrelated to this task).
- Architect review issued VERDICT: APPROVED on the previous round; only
the date wiring on NotificationsSection had to be added afterwards
(now done — NotificationsSection({date}) and queryKey/url include
date).
Validation-round-3 fixes (this commit):
- Tasks RBAC tightened end-to-end: new TASK_VIEW_ROLES = admin +
office_manager + coord_lead + coordinator. GET /tasks and
PATCH /tasks/:id now require requireTaskView. /me exposes new
canViewTasks flag. Frontend isSectionVisible("tasks") gated on
canViewTasks (so CEO/viewer never sees the Tasks tab or hits the
endpoint). Verified: admin /me returns canViewTasks=true.
- GET /requests gained dateFrom + dateTo (createdAt range) and a
requester filter that is honored only for approver/audit roles
(non-approvers stay locked to their own requests). Verified curl
with ?dateFrom=...&dateTo=... returns 200.
- Task dueAt schema accepts BOTH YYYY-MM-DD (from <input type="date">)
and full ISO datetime; date-only is normalized to start-of-day UTC,
empty string clears the field. Verified curl POST /tasks with
{"dueAt":"2026-12-31"} returns 201 with stored
dueAt = 2026-12-31T00:00:00.000Z.
Validation-round-2 fixes:
- GET /executive-meetings/requests: added server-side least-privilege so
only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every
other executive role is force-scoped to requestedBy = self regardless
of the client's `mine` flag (closes the RBAC overexposure flag).
- isSectionVisible("tasks") now also returns true for canSubmitRequest,
so executive_coordinator sees the Tasks tab (coordinators execute the
follow-ups generated from approved requests).
- Requests "new request" dropdown now exposes ALL twelve backend request
types: create, edit, delete, reschedule, add_attendee, remove_attendee,
change_location, cancel_meeting, note, highlight, unhighlight, other.
- i18n parity: added en.json + ar.json keys for the five missing request
types (add_attendee, remove_attendee, change_location, cancel_meeting,
note) and the two missing statuses (needs_edit, done) under
executiveMeetings.requests.{type,status}.
- Fixed wrong key reference: reschedule form's date row was using
manage.field.date (does not exist) — switched to manage.field.meetingDate.
Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112):
- Real notifications delivery, server-side PDF rendering, attendee
reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync,
expanded seed data including executive_meeting_notifications sample
rows.
|
||
|
|
e66c69bccc |
Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
attendees column widest, tighter padding, navy/white/gray + red highlight
only. Header label "م" → "#" (ar.json).
Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
requests work without an existing meeting. db push applied.
Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full
rewrite:
- ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained
requireMutate / requireApprove / requireRequest / requireAdminAudit on the
appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
{status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
{action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a
single shared upsertFontSettingsHandler (no Express method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- GET /notifications now accepts ?date=YYYY-MM-DD and joins through the
meetings table to scope notifications to the selected day; returns []
immediately when no meetings exist on that date.
- Removed all `as never` casts via $inferInsert/Partial typing.
Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin +
executive_*).
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized
users never see Manage / Approvals / Audit / Tasks tabs (matrix in the
in-file isSectionVisible helper).
- Approvals: review() sends {status, reviewNotes}; new "Send back for edits"
(needs_edit) button beside Approve / Reject.
- Requests default scope is "mine" for non-approvers and "all" for approvers
(no over-exposure to plain requesters).
- Notifications visibility uses canRead AND the query is scoped by the same
selected `date` as the schedule (queryKey + ?date=… included).
- PdfSection: primary "Print + Archive" button (testid em-print) does archive
then print in one click; em-print-only and em-archive remain as separate
buttons.
- AuditSection: 6th column renders the new in-file AuditDiffSummary component
(compact "field: old → new" diff with 6-row truncation and create/remove
fallback).
- New tWithFallback(t, key, fallback) helper used in audit + notifications
cells (avoids strict TFunction overload errors); apiJson rewritten to
extract body/headers separately and stringify rawBody, eliminating the
"Record<string, unknown> not assignable to BodyInit" TS errors. tsc
--noEmit on executive-meetings.tsx is now clean.
- Print page (executive-meetings-print.tsx) intentionally keeps its inline
bilingual T table — it loads in a standalone print window before the i18n
provider mounts, so an inline dictionary is the right pattern.
i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
.audit.col.changes, .audit.{created, removed, moreChanges},
.audit.entity.pdf_archive,
.audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate,
replace_attendees, done},
.pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly,
archivedAndPrinted}.
Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201, nested
POST /:id/requests 201, PATCH /requests/:id status approval 200,
audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
200 with persisted data, /notifications?date=YYYY-MM-DD filters
correctly.
- tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is
clean (pre-existing admin.tsx errors are unrelated to this task).
- Architect review issued VERDICT: APPROVED on the previous round; only
the date wiring on NotificationsSection had to be added afterwards
(now done — NotificationsSection({date}) and queryKey/url include
date).
Validation-round-2 fixes (this commit):
- GET /executive-meetings/requests: added server-side least-privilege so
only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every
other executive role is force-scoped to requestedBy = self regardless
of the client's `mine` flag (closes the RBAC overexposure flag).
- isSectionVisible("tasks") now also returns true for canSubmitRequest,
so executive_coordinator sees the Tasks tab (coordinators execute the
follow-ups generated from approved requests).
- Requests "new request" dropdown now exposes ALL twelve backend request
types: create, edit, delete, reschedule, add_attendee, remove_attendee,
change_location, cancel_meeting, note, highlight, unhighlight, other.
- i18n parity: added en.json + ar.json keys for the five missing request
types (add_attendee, remove_attendee, change_location, cancel_meeting,
note) and the two missing statuses (needs_edit, done) under
executiveMeetings.requests.{type,status}.
- Fixed wrong key reference: reschedule form's date row was using
manage.field.date (does not exist) — switched to manage.field.meetingDate.
Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112):
- Real notifications delivery, server-side PDF rendering, attendee
reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync,
expanded seed data including executive_meeting_notifications sample
rows.
|
||
|
|
b3ac7ebd60 |
Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
attendees column widest, tighter padding, navy/white/gray + red highlight
only. Header label "م" → "#" (ar.json).
Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
requests work without an existing meeting. db push applied.
Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full
rewrite:
- ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained
requireMutate / requireApprove / requireRequest / requireAdminAudit on the
appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
{status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
{action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a
single shared upsertFontSettingsHandler (no Express method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- GET /notifications now accepts ?date=YYYY-MM-DD and joins through the
meetings table to scope notifications to the selected day; returns []
immediately when no meetings exist on that date.
- Removed all `as never` casts via $inferInsert/Partial typing.
Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin +
executive_*).
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized
users never see Manage / Approvals / Audit / Tasks tabs (matrix in the
in-file isSectionVisible helper).
- Approvals: review() sends {status, reviewNotes}; new "Send back for edits"
(needs_edit) button beside Approve / Reject.
- Requests default scope is "mine" for non-approvers and "all" for approvers
(no over-exposure to plain requesters).
- Notifications visibility uses canRead AND the query is scoped by the same
selected `date` as the schedule (queryKey + ?date=… included).
- PdfSection: primary "Print + Archive" button (testid em-print) does archive
then print in one click; em-print-only and em-archive remain as separate
buttons.
- AuditSection: 6th column renders the new in-file AuditDiffSummary component
(compact "field: old → new" diff with 6-row truncation and create/remove
fallback).
- New tWithFallback(t, key, fallback) helper used in audit + notifications
cells (avoids strict TFunction overload errors); apiJson rewritten to
extract body/headers separately and stringify rawBody, eliminating the
"Record<string, unknown> not assignable to BodyInit" TS errors. tsc
--noEmit on executive-meetings.tsx is now clean.
- Print page (executive-meetings-print.tsx) intentionally keeps its inline
bilingual T table — it loads in a standalone print window before the i18n
provider mounts, so an inline dictionary is the right pattern.
i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
.audit.col.changes, .audit.{created, removed, moreChanges},
.audit.entity.pdf_archive,
.audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate,
replace_attendees, done},
.pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly,
archivedAndPrinted}.
Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201, nested
POST /:id/requests 201, PATCH /requests/:id status approval 200,
audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
200 with persisted data, /notifications?date=YYYY-MM-DD filters
correctly.
- tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is
clean (pre-existing admin.tsx errors are unrelated to this task).
- Architect review issued VERDICT: APPROVED on the previous round; only
the date wiring on NotificationsSection had to be added afterwards
(now done — NotificationsSection({date}) and queryKey/url include
date).
Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112):
- Real notifications delivery, server-side PDF rendering, attendee
reorder UI, duplicate-to-date UI, OpenAPI/api-spec sync, expanded seed
data, every backend request type as its own UI form.
|
||
|
|
0b7b571a1e |
Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
attendees column widest, tighter padding, navy/white/gray + red highlight only.
- Header label "م" → "#" (ar.json).
Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
requests work without an existing meeting. db push applied.
Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full rewrite:
- All routes (including /me) gated by requireExecutiveAccess. Fine-grained
middleware: requireMutate / requireApprove / requireRequest /
requireAdminAudit on the appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
{status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
{action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired directly
to the same shared upsertFontSettingsHandler (no method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- Removed all `as never` casts via $inferInsert/Partial typing.
Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES
(admin + executive_*).
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Notifications visibility now uses canRead (was canViewAudit).
- Approvals: review() sends {status, reviewNotes} and a new
"Send back for edits" (needs_edit) button is rendered alongside
Approve/Reject.
- PdfSection: primary "Print + Archive" button (testid em-print) does
archive then print in one click; em-print-only and em-archive remain.
- AuditSection: 6th column renders the new in-file AuditDiffSummary
component (compact "field: old → new" diff with truncation).
- Print page (executive-meetings-print.tsx): kept its inline bilingual T
table — it loads in a standalone print window before the i18n provider
mounts, so an inline dictionary is the right pattern. No hardcoded UI
text remains in the main page.
i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
.audit.col.changes, .audit.{created, removed, moreChanges},
.audit.entity.pdf_archive,
.audit.action.{approved, rejected, needs_edit, withdraw, apply,
duplicate, replace_attendees, done},
.pdf.{print = "Print + archive"/"طباعة وأرشفة", printOnly,
archivedAndPrinted}.
Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201,
nested POST /:id/requests 201, PATCH /requests/:id status approval 200,
audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
200 with persisted data.
- Architect re-reviewed twice: VERDICT: APPROVED on the second pass
after fixing /me gating and the PATCH font-settings dispatch.
Out of scope (handled in follow-ups #110/#111/#112): real notifications
delivery, server-side PDF rendering, mobile polish.
Drift: none material. Followed plan T1–T8.
|
||
|
|
bf85d7066e |
Task #108 — Executive Meetings Phase 2
Schedule UI polish, full CRUD, Requests/Approvals/Tasks/Audit/PDF/Print/
Font Settings/Notifications-placeholder, audit logging on every mutation,
fine-grained RBAC, bilingual i18n.
Backend (artifacts/api-server/src/routes/executive-meetings.ts)
- All mutations transactional + audit-logged.
- Approve/reject/needs_edit use atomic UPDATE WHERE status='new' RETURNING;
race losers return 409 RACE_ALREADY_REVIEWED.
- applyApprovedRequest() applies reschedule / change_location / add_attendee /
remove_attendee / highlight / unhighlight / cancel_meeting / create / edit /
delete in-tx; effective-time validation throws APPLY_VALIDATION → 400.
- Approve auto-creates linked follow_up_<reqType> task with requestId set.
- Marking a task with requestId as 'completed' propagates request → 'done'
(in same tx, audit-logged).
- New endpoints: PUT /:id/attendees, POST /:id/duplicate,
GET/POST /pdf-archives.
Schema (lib/db/src/schema/executive-meetings.ts)
- executive_meeting_requests.meetingId nullable so 'create' requests work.
- pdf_archives table.
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx + new
executive-meetings-print.tsx, App.tsx route)
- Schedule polish: # column, RTL order, attendees widest, navy/white/gray,
red highlight only.
- Tabs: Manage / Requests / Approvals / Tasks / Audit / PDF / Notifications /
Font Settings.
- Requests form now sends STRUCTURED fields (meetingDate, startTime, endTime
for reschedule; location/url for change_location; name/title for
add_attendee; etc.) — previously sent only {note}, which prevented apply
from doing anything.
- Approve handler invalidates ALL ['/api/executive-meetings', *] keys + tasks.
- PDF tab lists archives; Archive snapshot button; Print opens new route.
i18n (en.json/ar.json)
- Full executiveMeetings.* key set including pdf.archive*, attendees.title/id.
Verified
- E2e regression: create meeting → reschedule request (structured) → approve
→ meeting moves to new date → task created → mark task complete → request
becomes 'done'. Negative case (start>end) → approve rejected.
- Architect final review: PASS, no severe blockers.
Out of scope per spec: real notification delivery, real PDF rendering, iCal,
mobile polish.
|
||
|
|
b4d47e1012 |
Executive Meetings Phase 2: full module + atomic audit
Original task (#108): polish the daily-schedule UI and ship the remaining 8 sections of the Executive Meetings module (Manage, Requests, Approvals, Tasks, Notifications, Audit, PDF, Font Settings) with bilingual i18n and fine-grained RBAC. What changed: - Schedule UI: centered cells, '#' header (not 'م'), attendees widest, RTL column order locked, navy/white/gray + red highlighting only. - Schema: made executive_meeting_requests.meetingId nullable so 'create-meeting' suggestions don't need a target row. - Backend rewrite of artifacts/api-server/src/routes/executive-meetings.ts: GET /me (now returns userId), full CRUD for meetings (transactional attendee replace), requests CRUD + approve/reject/withdraw, tasks CRUD with assignee status updates, audit-logs GET (admin), notifications GET, font-settings GET/PATCH (per-user + global). RBAC via 5 role sets and a makeRequireRoles middleware factory. - Audit-in-transaction: every mutation (meeting/request/task/font CRUD) wraps the DB write AND the audit-log insert in the same db.transaction so audit entries cannot drift from state if either insert fails. This was the main finding from the architect review and is now fixed. - Path-to-regexp 8 fix: replaced inline ':id(\\d+)' with router.param('id') + next('route') guard, plus NaN guards in handlers. - Frontend rewrite of artifacts/tx-os/src/pages/executive-meetings.tsx (~2200 lines): 8 new section components, RBAC-aware UI via /me, per-task assignee check now shown for status buttons. - i18n: full executiveMeetings.* key set added in ar.json + en.json. Verified: full e2e (admin login -> all 9 tabs -> create meeting -> submit request -> approve -> audit shows full chain -> font save) plus a smoke regression after the audit-in-tx refactor (atomic audit row created in lockstep with mutation). Out of scope (proposed as follow-ups #110-#112): real notification delivery, real PDF generation (currently window.print), and automated integration tests for the new endpoints. The pre-existing 'apps-open.test.mjs' syntax error in the test workflow is unrelated to this task. |
||
|
|
539238028b |
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: 2f2929b5-7e14-4436-b760-d09717da7323 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/BRoHDEI Replit-Helium-Checkpoint-Created: true |
||
|
|
1ad311ad50 |
Add automated tests for role create / edit / delete endpoints (Task #90)
Original task
- Cover the new /api/roles create, update, and delete handlers with
automated tests so regressions in role validation, system-role
protection, and the in-use conflict response can't slip in unnoticed.
- Tests must run as part of the standard api-server test command.
What was added
- artifacts/api-server/tests/roles-crud.test.mjs with 7 tests:
* POST /api/roles -> 201 (also checks DB row + serialized fields)
* POST /api/roles duplicate name -> 409 (no second row inserted)
* POST /api/roles invalid name format -> 400 (no row leaked)
* PATCH /api/roles/:id description-only update -> 200, name unchanged
* DELETE /api/roles/:id on system role 'admin' -> 400, row preserved
* DELETE /api/roles/:id on in-use role -> 409 with userCount=1,
groupCount=1; row preserved
* DELETE /api/roles/:id on unused role -> 204, row gone
- The file follows the same pattern as groups-crud.test.mjs:
pg.Pool seed via DATABASE_URL, login -> connect.sid cookie,
thorough teardown of users / groups / roles / link tables.
Test infra
- No new test setup needed; the package's existing
"test": "node --test 'tests/**/*.test.mjs'" picks the file up
automatically. All 7 new tests pass.
Notes / drift
- Two pre-existing test failures (apps-open.test.mjs and one assertion
in groups-crud.test.mjs that depends on global group counts) are
unrelated to this task and were left untouched.
Replit-Task-Id: 4254b35b-ba28-443f-80a6-22f6ddfbedd6
|
||
|
|
f7b8093d04 |
Add Reorder shortcut to finished orders on /my-orders
## Original task (Task #66) Let users quickly re-order a service from their order history. On every order card whose status is `completed` or `cancelled`, show a "Reorder" button (Arabic: "اطلب مجددًا") that opens the existing OrderServiceModal pre-filled with the original notes. Submitting creates a brand-new order via the existing endpoint; the original order stays unchanged. ## Implementation - `artifacts/tx-os/src/components/order-service-modal.tsx`: added an optional `initialNotes` prop and seeded the notes textarea with it (truncated to NOTES_MAX) whenever the modal opens. Existing services page callers are unaffected (prop defaults to undefined => empty notes). - `artifacts/tx-os/src/pages/my-orders.tsx`: - Imported the `RotateCcw` icon and `OrderServiceModal`. - Added a "Reorder" button (with icon) on `OrderCard` that shows only for finished orders, alongside the existing cancel/delete actions. - Hoisted reorder modal state to `MyOrdersPage` via a new `reorderTarget` state and an `onReorder` callback wired through `OrderCard`. - Render `OrderServiceModal` at the page level using the original order's service info and notes; closing/submitting clears state. - `artifacts/tx-os/src/locales/{en,ar}.json`: added `myOrders.reorder` ("Reorder" / "اطلب مجددًا"). ## Notes / non-deviations - Task description references `artifacts/teaboy-os/...`; the actual artifact in this repo is `artifacts/tx-os/`. Same files, same intent. - Submitting reuses `useCreateServiceOrder` so the original order is untouched and standard error/success toasts fire from the modal. ## Verification - `pnpm exec tsc --noEmit` shows no new errors in the touched files. - E2E test: created a fresh user with one completed and one cancelled order, logged in, opened /my-orders, confirmed the Reorder button appears with notes prefilled in the modal, edited the notes, submitted, and verified a new pending order with the edited notes appeared while the original order remained unchanged. Replit-Task-Id: 2140b360-9d2f-45d9-9ddc-267ea29f7d7c |
||
|
|
6c160530fa |
Add e2e tests for the order placement and cancellation flow
Original task (#65): Persist coverage for the new "Order" + "My Orders" flow with a Playwright spec under artifacts/teaboy-os/tests/ that places an order, sees the "Awaiting receiver" pill in /my-orders, cancels it, and verifies the red cancelled pill replaces the timeline and the cancel button disappears. Both Arabic and English label paths must be exercised, and the spec must run under `pnpm --filter @workspace/teaboy-os test:e2e`. Implementation - Added artifacts/tx-os/tests/order-place-cancel.spec.mjs (the artifact is now `tx-os`; `teaboy-os` no longer exists, so the spec lives in the same tests directory as `leave-group-successor.spec.mjs` and is picked up automatically by the existing test:e2e Playwright config). - Style mirrors leave-group-successor.spec.mjs: file-scoped DB pool, per-test seeded user with the `user` role, after-all cleanup that deletes the spec's orders, the user's stray orders/notifications, user_roles, and the user itself. - The spec runs the same flow twice, once with English labels and once with Arabic labels, driven through a `placeAndCancelFlow(page, lang, labels)` helper that: 1. Forces the UI language via `page.addInitScript` writing the `tx-lang` localStorage key BEFORE any page script runs. 2. Seeds the test user with `preferred_language` matching `lang`, because AuthContext + login.tsx call `i18n.changeLanguage(user.preferredLanguage)` on login and would otherwise override the localStorage seed. 3. Logs in via the real /login form, navigates to /services, clicks the available service tile (matched by aria-label = localized name), submits the order modal, and waits for the POST /api/orders 201 to capture the order id. 4. Navigates to /my-orders, asserts the first card contains the service name, the pending pill ("Awaiting receiver" / "بانتظار المستلِم"), and 4 timeline step circles. 5. Clicks the in-card Cancel button, confirms via the alertdialog, waits for the PATCH /api/orders/:id/status 200 → cancelled. 6. Asserts the timeline step circles are gone, the centered red cancelled pill ("Cancelled" / "ملغى") is present, and the Cancel button is removed from the card. Verification - `pnpm --filter @workspace/tx-os exec playwright install chromium` - `cd artifacts/tx-os && pnpm exec playwright test --config playwright.config.mjs` → all 6 specs pass (the 4 pre-existing ones + the 2 new EN/AR tests). Deviations - Task referenced the `teaboy-os` artifact path; the actual artifact in this codebase is `tx-os`, so the spec was placed under `artifacts/tx-os/tests/` and registered with the existing `pnpm --filter @workspace/tx-os test:e2e` script (the original `teaboy-os` filter no longer exists). Replit-Task-Id: d2139d0e-9ee0-4795-8f94-29e1cfe4b35a |
||
|
|
aa6866936d |
Task #89: Permissions picker for roles in admin dashboard
Original task: let admins assign permissions to roles from the dashboard.
Changes:
- lib/api-spec/openapi.yaml: added Permission and ReplaceRolePermissionsBody
schemas; added GET /permissions, expanded GET /roles/{id}/permissions to
return full Permission objects, and added admin-guarded
PUT /roles/{id}/permissions. Ran codegen to refresh generated hooks/zod.
- artifacts/api-server/src/routes/roles.ts:
* Added a single isSystemRole helper and PROTECTED_ROLE_NAMES set.
* Added serializePermission + GET /permissions.
* Expanded GET /roles/:id/permissions (404 on missing role, full
Permission objects).
* New PUT /roles/:id/permissions: blocks system/protected roles
(400 with code "system_role_permissions"), rejects non-positive /
non-integer permissionIds with 400, validates all ids exist (404),
deletes+inserts atomically in a transaction, and notifies role
holders via emitAppsChangedToRoleHolders.
* Hardened legacy POST /roles/:id/permissions and
DELETE /roles/:id/permissions/:permissionId so they also reject
system-role mutations with the same code, closing the bypass that
the new endpoint was meant to prevent.
* Re-used isSystemRole in PATCH/DELETE role flows.
- artifacts/tx-os/src/pages/admin.tsx: added a permissions checkbox list
inside the role editor dialog, disabled (read-only) for system /
protected roles with a hint. Save flow runs updateRole then
replaceRolePermissions only when the set actually changed and the role
is non-system. Front-end ROLES_PROTECTED_NAMES set mirrors the
back-end fallback so admin/user/order_receiver are recognised even
when the legacy DB column is 0 (also gates the role-card system badge
and delete button).
- artifacts/tx-os/src/locales/en.json + ar.json: new translation keys
for the permissions picker, system-role read-only hint, empty list,
and error toasts.
Verification: tx-os and api-server typecheck clean. End-to-end test
passes: admin login → edit a created role → toggle/save permissions →
re-open and confirm round-trip → admin role dialog shows disabled
checkboxes and read-only hint → PUT on admin role returns 400
"system_role_permissions". curl regression checks: legacy POST/DELETE
on admin role return the same 400/code; PUT rejects float / negative /
string ids with 400.
Replit-Task-Id: 74180397-afdf-4489-976a-a6fe099684bd
|
||
|
|
1e9024e5a2 |
Let admins edit role names + descriptions in both languages
Original task (#88): The Roles tab in the Group editor displays each role's Arabic and English descriptions, but admins had no UI to edit them — any tweak required a developer. The edit dialog also didn't allow updating the role's name. Make role name + bilingual descriptions editable from the admin Roles panel and persist via PATCH /api/roles/:id. Changes: - lib/api-spec/openapi.yaml: UpdateRoleBody now accepts an optional `name` (minLength 1). PATCH /roles/{id} summary updated to "Update role name and descriptions (admin)" and documents 400 (invalid request) and 409 (name already taken) responses. Regenerated lib/api-zod. - artifacts/api-server/src/routes/roles.ts (PATCH /roles/:id): * Accept and validate `name` with the same rules as create (`/^[a-z][a-z0-9_]*$/i`, uniqueness check that ignores the role's own row). * Refuse to rename system roles (isSystem=1 or one of admin/user/ order_receiver) -> 400 with `code: "system_role_rename"`. * Returns 409 on name collision. - artifacts/tx-os/src/pages/admin.tsx (RolesPanel edit dialog): * Edit form now includes a Name input (data-testid="role-edit-name") prefilled from the role. * Name input is disabled for system roles, with a dedicated hint string. * Save is disabled when name is empty; 400 errors with `code: "system_role_rename"` show a system-role-specific toast, other 400s show the invalid-name toast, and 409 shows name-taken. - artifacts/tx-os/src/locales/{en,ar}.json: * Replaced the now-misleading `editHint` ("Role names cannot be changed") with `editSystemNameHint` (only shown for system roles). * Removed "Cannot be changed later" from `nameHint`. * Added `errorSystemRename` for the system-role rename toast. Validation: - pnpm typecheck passes. - Playwright e2e (testing skill) created a role, renamed it + updated both descriptions, verified the new name + descriptions appear on the role card and in the Group editor's Roles tab, confirmed empty name disables Save, and cleaned up via delete. (Test agent flagged one stale snapshot on the system-role disabled-name check; the implementation gates on r.isSystem from the API and is also enforced server-side.) Replit-Task-Id: 01dbc785-03e0-4714-b9d5-f0dff6de9a56 |
||
|
|
574b93022c |
Treat 404 in incoming-order actions as "no longer available"
Task #87: Confirm-receipt should also handle the case where the order was deleted. When an admin deletes an order while a receiver is viewing it, the API returns 404 Order not found for confirm-receipt and the status PATCH. Previously the UI showed the generic "Could not complete the action" toast and left the stale card in place. Updated all three onError branches in `artifacts/tx-os/src/pages/orders-incoming.tsx` so 404 responses are handled the same way as the existing `order_unavailable` 409: - handleConfirmReceipt: 404 now shows the localized "incomingOrders.orderUnavailable" toast instead of "actionFailed". invalidate() was already called for every error in this branch, so the stale card is removed. - handleSetStatus (preparing/completed): 404 now shows the "orderUnavailable" toast and calls invalidate() to drop the card. - handleCancel: same treatment for 404 on the cancel PATCH. The 409 already-claimed branch is preserved unchanged. No new translation keys were needed; `incomingOrders.orderUnavailable` already exists in both `src/locales/en.json` and `src/locales/ar.json`. Pre-existing TypeScript errors in `src/pages/admin.tsx` are unrelated to this change. Replit-Task-Id: a923d4f2-0a45-4816-aee9-0822c5677b88 |
||
|
|
b865e69f64 |
Add test coverage for the order-unavailable error responses (Task #86)
Task #80 introduced a new 409 `order_unavailable` error code returned by: - PATCH /api/orders/:id/confirm-receipt - PATCH /api/orders/:id/status …to distinguish "already_claimed" (lost a race) from "no longer claimable" (target order is cancelled or completed). The integration test suite did not cover these paths. Changes ------- artifacts/api-server/tests/service-orders.test.mjs + Added test "confirm-receipt returns 409 order_unavailable when the order is cancelled or completed": - Owner cancels a pending order → another receiver's confirm-receipt returns 409 with body.error === "order_unavailable" (not "already_claimed"). - Receiver claims, prepares, completes an order → another receiver's confirm-receipt returns 409 "order_unavailable". + Added test "PATCH /orders/:id/status returns 409 order_unavailable when targeting a terminal order": - Completed order: assignee → preparing → 409 "order_unavailable" - Completed order: assignee → completed → 409 "order_unavailable" - Completed order: owner → cancelled → 409 "order_unavailable" - Cancelled order: owner → cancelled → 409 "order_unavailable" Verification ------------ All 11 service-orders integration tests pass (including both new tests): pnpm --filter @workspace/api-server test → 11 passed, 0 failed. No production code changes; tests only. Replit-Task-Id: 229ed991-1c32-4281-8e57-00abeacd80b4 |
||
|
|
7c9edf6cb6 |
Warn admins before deleting non-empty users/apps/services (Task #85)
Apply the existing groups delete-warning pattern to user, app, and
service deletions so admins are warned (and have to confirm twice)
before destroying records that still own dependent data.
Backend
- openapi.yaml: added `force` query param to DELETE /users/{id},
/apps/{id}, /services/{id} plus three new conflict schemas
(UserDeletionConflict, AppDeletionConflict, ServiceDeletionConflict).
- routes/users.ts, apps.ts, services.ts: rewrote DELETE handlers to
count dependents (notes/orders/conversations/messages for users;
group_apps/restrictions/open events for apps; service_orders for
services), return 409 with counts when non-empty and force is not set,
and on `?force=true` perform the cascade in a transaction and write
an `*.force_delete` audit_logs row.
- Existing 204 success path preserved for empty deletes.
UI (artifacts/tx-os/src/pages/admin.tsx)
- New `DeletionWarningDialog` helper, plus app/service/user delete state
+ lazy 409 detection (first click probes; on 409 the dialog upgrades
to show counts + "Delete anyway"; second click sends ?force=true).
- Replaced the three plain `confirm(t("admin.deleteConfirm"))` callsites.
i18n
- Added admin.deleteApp/deleteService/deleteUser keys (title, warning,
forceHint, emptyBody, count keys, confirm, anyway) in en.json + ar.json.
Tests
- artifacts/api-server/tests/delete-force-warnings.test.mjs covers all
9 cases (clean delete, 409 with counts, force=true 204 + audit log)
for users, apps, and services. Existing groups-crud and service-orders
tests still pass.
Notes / drift
- Lazy detection (vs eager counts on the row like Groups already does)
was chosen because list endpoints don't return counts yet — proposed
follow-up #96 covers eager counts so the warning appears on first
click everywhere.
- E2E test for the UI flow flaked twice; backend integration tests
(9/9 pass), direct curl validation of force=true returning 204, and
typecheck across the monorepo all confirm correctness.
Replit-Task-Id: 91404d92-e74c-4720-8fc9-8eb772eefc33
|
||
|
|
6e9b9b5f3d | Git commit prior to merge | ||
|
|
66bc96f97f |
Add executive meeting management system with role-based access
Implement a new API endpoint and UI for managing executive meetings, including role-based access control and database schema changes for meeting data and attendees. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 079afca5-8c32-40ea-b7d1-8bf26358ff75 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/BRoHDEI Replit-Helium-Checkpoint-Created: true |
||
|
|
0a5e89a6d4 |
Add a new module for managing executive meetings
Introduces a new "Executive Meetings Management" module with database schema, API endpoints, and UI components, designed to run independently without altering existing functionalities. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: e463ac71-2b1f-4b47-8b94-ca9526e6f427 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/3E2mNig Replit-Helium-Checkpoint-Created: true |
||
|
|
1ca5d5ae4b |
Add admin Audit Log view (task-84)
Original task: Show admins a history of sensitive actions. Forced group deletions already write to the new `audit_logs` table; admins now have a UI to browse and filter those entries. API - New OpenAPI endpoint GET /admin/audit-logs (admin-only) with optional filters: action (exact), from / to (YYYY-MM-DD, inclusive), limit (1-200, default 50), offset (default 0). - New schemas: AuditLogActor, AuditLogEntry, AuditLogList. List response includes paginated `entries`, `totalCount`, `nextOffset`, and a distinct `actions` list to power the filter dropdown. - New `audit` tag added to the spec; ran orval codegen so api-client-react / api-zod expose useListAuditLogs et al. - Implemented `artifacts/api-server/src/routes/audit.ts` and registered it in routes/index.ts. Validates date format / order, joins users for actor info, and orders newest-first. UI (artifacts/tx-os/src/pages/admin.tsx) - New "Audit log" entry under the User Management nav group (icon: ScrollText). Section deep-link works via #section=audit-log. - New AuditLogPanel + AuditLogRow components: action chip + target, formatted timestamp, actor avatar/name, expandable JSON metadata. - Filters bar: action dropdown (populated from API's distinct actions), from/to date inputs, Apply / Reset, plus a "Load more" control that increases the page size (caps at 200, the API limit). Read-only by design. - Added en/ar locale strings for nav.auditLog and the audit subtree. Verification - Typecheck: pnpm -w run typecheck (clean). - API smoke tested via curl: 401 unauthenticated, validation errors for bad / inverted dates, returns the seeded `group.force_delete` entry once produced. - End-to-end browser test (testing skill): logged in as admin, navigated to /admin#section=audit-log, verified the row, expanded metadata, exercised filters (empty state + reset). No deviations from the task description. Replit-Task-Id: 5b5bf9b1-6937-43c3-85c9-81f0c19a5e49 |
||
|
|
c05ae786fc |
Fix order cancellation logic and update tests to reflect new behavior
Adjusts the order cancellation endpoint to correctly handle admin overrides for completed/cancelled orders and updates associated tests to expect 409 status codes reflecting Task #80's changes. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: d358e7e1-8a16-4746-86aa-4f1739233f2e Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/3E2mNig Replit-Helium-Checkpoint-Created: true |
||
|
|
595ca06a57 |
Task #83: Refresh role and permission changes live across user sessions
## Socket.IO broadcasts (core task): - POST /users/:id/roles: uses .returning() to emit apps_changed only when row inserted - DELETE /users/:id/roles/:roleName: uses .returning() to emit only when row deleted - Added emitAppsChangedToRoleHolders(roleId) helper to realtime.ts - New role-permission endpoints in roles.ts, emit only on effective changes: - GET /roles/:id/permissions — list permissions for a role - POST /roles/:id/permissions — assign; emits to role holders on actual insert - DELETE /roles/:id/permissions/:permId — remove; emits only if row was deleted ## Bug fix: admin panel not showing disabled apps - Added GET /api/admin/apps (requireAdmin) returning ALL apps from DB - Updated admin.tsx: useQuery → /api/admin/apps with ADMIN_APPS_KEY constant - All admin.tsx mutation handlers invalidate ADMIN_APPS_KEY ## UI fix: disabled app cards nearly invisible (opacity-60 on glass) - Gray background + grayscale icon + muted text + stronger Disabled badge Replit-Task-Id: f439ec75-bcd5-4030-8ee1-83a5d976f1a1 ## DB: removed 11 duplicate rows from app_permissions table |
||
|
|
19a20cc5a4 |
Let admins create and edit roles from the dashboard (Task #82)
What changed:
- Added an `is_system` column to the `roles` table (default 0). The
built-in roles (admin, user, order_receiver) are flagged as system roles
in the seed and via a one-off UPDATE on the existing dev DB.
- Created a new `/api/roles` route module with admin-guarded endpoints:
- POST /api/roles – create a role (validates name format, rejects duplicates).
- PATCH /api/roles/:id – update bilingual descriptions; name is immutable.
- DELETE /api/roles/:id – returns 400 for system roles (also defended
by name allowlist), 409 with userCount/groupCount when in use, 204 on
success. Cleans up role_permissions in a transaction.
GET /api/roles was moved out of groups.ts into the new module.
- OpenAPI updated with createRole / updateRole / deleteRole operations,
isSystem on the Role schema, and CreateRoleBody / UpdateRoleBody /
RoleDeletionConflict schemas. Regenerated api-zod and api-client-react.
- Added a "Roles" item under the User Management nav group in the admin
dashboard, plus a new RolesPanel with search, create dialog, edit dialog
(description-only), and delete dialog with conflict messaging.
- Bilingual translations added in en.json and ar.json (admin.nav.roles +
admin.roles.*).
Notes / deviations:
- Also marked `order_receiver` as a system role since it is seeded by the
system and required by the orders feature, even though the task brief
only called out admin and user.
- Verified end-to-end via the testing skill: list, create, edit, delete,
duplicate-name validation, and protection of system roles all work.
Replit-Task-Id: b1187555-be09-4687-a9ae-83b123d908bd
|
||
|
|
b0b1d673b7 |
Show role descriptions in both languages on the Groups Roles tab
Task #81: The Groups editor's Roles tab previously displayed only the description in the active UI language with the role's machine name as a small subtitle. Bilingual admin teams asked to see both languages at once. Changes (artifacts/tx-os/src/pages/admin.tsx): - Imported Tooltip, TooltipContent, TooltipTrigger from @/components/ui/tooltip (TooltipProvider was already mounted at the App root, so no provider wiring was needed). - Refactored each role row in the Roles tab of GroupDetailEditor: * Primary line: description in the active language (descriptionAr when lang === "ar", otherwise descriptionEn). Falls back to the other-language description, then to the role's name when no descriptions exist. * Secondary line (smaller, muted): the OTHER language's description, rendered with the appropriate dir attribute. Only shown when both descriptions are present so single-description roles fall back cleanly. * Machine name remains visible as a small monospace subtitle on the trailing edge of the row. * The whole row is wrapped in a Tooltip whose content shows both descriptions (each with its proper dir) plus the machine name for full context on hover. - Added data-testid="group-role-<id>-secondary" so the secondary line is easily targetable from tests. Verification: - Type-checked tx-os (only pre-existing errors elsewhere in admin.tsx remain; none introduced by this change). - e2e test passed: logged in as admin, opened a group, switched to the Roles tab, confirmed primary + secondary description lines, hovered to see the bilingual tooltip, toggled the UI language and confirmed the lines swapped, and confirmed checkboxes still toggle through the TooltipTrigger asChild wrapper. The single-description-only fallback could not be exercised against seeded data (all seeded roles are bilingual), but the rendering logic gracefully handles that case via the `heading` and `showSecondary` guards. Also reverts an unrelated stray modification to artifacts/tx-os/public/opengraph.jpg that was swept into the previous auto-commit, restoring it to the version from HEAD~1. Follow-up proposed: #88 "Let admins edit role descriptions in both languages". Replit-Task-Id: dfd3b45c-7b85-4c30-b0f9-3bfbab81be8c |
||
|
|
5051b1b905 |
fix: tell receivers when an order is no longer available + fix disabled apps disappearing from admin
## Task #80 — Tell receivers when an order is no longer available to claim ### Problem When a receiver tried to act on an order that had already been cancelled, completed, or claimed by someone else, the UI showed a generic "Could not complete the action" toast. The stale card also stayed in the list, requiring a manual refresh. ### Changes **artifacts/api-server/src/routes/service-orders.ts** - PATCH /orders/:id/confirm-receipt: after a failed atomic claim, query the current order status; return 409 `order_unavailable` if it's cancelled/completed (vs the existing 409 `already_claimed` for when it was grabbed by someone else first) - PATCH /orders/:id/status (preparing/completed branch): terminal-state check runs BEFORE permission gating — returns 409 `order_unavailable` when order is already cancelled/completed instead of ever hitting 403 Forbidden - PATCH /orders/:id/status (cancel branch): same — terminal-state check moved to the top of the branch so non-admin receivers trying to cancel an already-terminal order get 409 `order_unavailable` (not 403 Forbidden which would never let the UI remove the stale card) **artifacts/tx-os/src/pages/orders-incoming.tsx** - handleConfirmReceipt onError: distinguish 409 `order_unavailable` from `already_claimed`, show specific toast, remove stale card via invalidate() - handleSetStatus onError: same pattern — specific toast + invalidate() on `order_unavailable` - handleCancel onError: same pattern **artifacts/tx-os/src/locales/en.json + ar.json** - Added `incomingOrders.orderUnavailable` ("This order is no longer available" / "هذا الطلب لم يعد متاحاً") ## Bonus fix — Disabled app disappears from admin panel (user-reported) ### Problem When an admin disabled an app via the toggle, the app disappeared from the admin panel too (isActive filter applied to everyone), making it impossible to re-enable without direct DB access. ### Changes **artifacts/api-server/src/routes/apps.ts** - getVisibleAppsForUser: admins now receive ALL apps including inactive ones via conditional $dynamic() Drizzle query; non-admins still only see active apps **artifacts/tx-os/src/pages/home.tsx** - Filter orderedApps to only active apps before rendering so inactive apps don't appear on the home screen even for admins **artifacts/tx-os/src/pages/admin.tsx** - Inactive app cards show reduced opacity + a "Disabled/معطّل" badge **artifacts/tx-os/src/locales/en.json + ar.json** - Added `admin.appDisabled` ("Disabled" / "معطّل") Replit-Task-Id: 8d6fade8-e76c-4d3c-864b-59007688c12c |
||
|
|
cd0a8313e0 |
Warn admins before deleting non-empty groups
Original task: Task #79 — Make DELETE /api/groups/:id refuse to wipe a non-empty group unless explicitly forced, surface the impacted counts in the admin UI, and log forced deletions to an audit trail. Changes: - API: DELETE /api/groups/:id now computes member/app/role counts. If any are non-zero and `?force=true` is not passed, it returns 409 with { error, memberCount, appCount, roleCount }. With `force=true` it deletes and writes an entry to the new `audit_logs` table (action='group.force_delete', target_type='group', target_id, metadata). System group guard and 404 behaviour preserved. - DB: Added `audit_logs` table (lib/db/src/schema/audit-logs.ts) with actor, action, target type/id, jsonb metadata, timestamp. Pushed via drizzle-kit. - OpenAPI: Added `force` query param, 409 response, and `GroupDeletionConflict` schema. Re-ran orval codegen for api-client-react and api-zod. - api-client-react: Re-exported `ApiError` and `ErrorType` so the UI can inspect 409 responses. - Admin UI (artifacts/teaboy-os/src/pages/admin.tsx GroupsPanel): Replaced the bare `confirm()` with a confirmation dialog that shows the group name and impacted member/app/role counts. Empty groups get a simple "Delete" confirmation; non-empty groups get a warning + "Delete anyway" button which sends `force=true`. If the server returns 409 (race), the dialog updates to show the server-reported counts. - Locales: Added en/ar strings for the new dialog (deleteTitle, deleteWarning, deleteForceHint, deleteEmptyBody, deleteConfirm, deleteAnyway). Verified end-to-end: 409 on first DELETE, dialog warning visible, force deletion succeeds, single audit_logs row recorded with correct metadata. Replit-Task-Id: 61624ce4-83b3-43be-b22b-e261662301f1 |
||
|
|
959ad9ca51 |
Update the website's icon for better visual clarity
Add a new icon to the website. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 14de494e-2969-4faf-9689-a7c72ba47548 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dnVFHsG Replit-Helium-Checkpoint-Created: true |
||
|
|
ea41328626 |
Remove service descriptions from display and administration
Removes Arabic and English description fields from the admin form, the services display component, and the seed data in `scripts/src/seed.ts`, and removes the corresponding columns from the database. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 5f1c43b0-7465-4e56-bb0e-896a4df38886 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dnVFHsG Replit-Helium-Checkpoint-Created: true |
||
|
|
9250e5e623 |
Design modern, minimal icons based on "TX" for the platform
Add new SVG icon assets for the platform's favicon and branding, including multiple variants and a preview HTML file. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 8912f52a-183a-4f2b-983a-b2ebfb2814ba Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dnVFHsG Replit-Helium-Checkpoint-Created: true |
||
|
|
d0e6912017 |
Rename project and remove unused services
Update project name from teaboy-os to tx-os and remove obsolete services. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 3154f23a-748a-4118-aa41-fc01b7b1f04d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PVuelRZ Replit-Helium-Checkpoint-Created: true |
||
|
|
1f23e65c0b |
Update system name and references from TeaBoy to Tx
Replaces all user-facing instances of "TeaBoy" with "Tx" across the application, including titles, locale files, database settings, seed data, and API documentation. Also updates internal storage keys and session secrets to remove the old branding. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 07b19cb0-11b5-4be9-8932-ae4820eb73b8 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/HxTkDPZ Replit-Helium-Checkpoint-Created: true |
||
|
|
6dd927350d |
Reflect group memberships live in the user's app grid (Task #77)
Group/membership changes now push to affected users over Socket.IO so
their app dock updates without a manual refresh.
Server:
- New helper `artifacts/api-server/src/lib/realtime.ts` exposing
`emitAppsChangedToUsers(userIds)` which lazy-imports the io instance
(matching the pattern used in conversations/service-orders) and emits
`apps_changed` to each `user:<id>` room.
- `routes/groups.ts`:
- PATCH /groups/🆔 snapshot members before the write, union with
members after, and emit to that set when membership changes; emit to
current members when only apps/roles change.
- POST/DELETE /groups/:id/users/:targetId emit to the target user.
- POST/DELETE /groups/:id/apps|roles/:targetId emit to all current
members of the group.
- DELETE /groups/:id emits to all (former) members.
- `routes/users.ts` PATCH /users/:id emits to the affected user when
groupIds is changed.
Client:
- `artifacts/teaboy-os/src/hooks/use-notifications-socket.ts` now also
listens for `apps_changed` and invalidates `getListAppsQueryKey()` and
`getGetMeQueryKey()`, refreshing the dock and AuthUser context.
Validation:
- API tests pass for all groups/membership cases. One pre-existing
flaky service-orders parallel-receipt test failed; unrelated to this
change.
Replit-Task-Id: 3e5ae44b-9dcf-42f4-8d5d-a0f2b05b6065
|
||
|
|
bc0737aed2 |
Task #76: Show roles management inside the Groups editor
Adds a "Roles" tab to the GroupDetailEditor in the admin Groups screen so
admins can manage which roles are auto-granted to members of a group.
Changes:
- OpenAPI: added `GET /roles` (operationId `listRoles`) and a `Role` schema
in `lib/api-spec/openapi.yaml`. Ran `pnpm --filter @workspace/api-spec
run codegen` to regenerate `lib/api-client-react` and `lib/api-zod`.
- API server: added `GET /api/roles` (admin-only) in
`artifacts/api-server/src/routes/groups.ts`, returning all roles
ordered by name.
- Frontend (`artifacts/teaboy-os/src/pages/admin.tsx`):
- Imported `useListRoles` / `getListRolesQueryKey`.
- Added `roleIds` state and a fourth `"roles"` tab to GroupDetailEditor
with a checkbox list backed by the new endpoint, hydrated from
`group.roleIds`.
- Save now sends `roleIds: Array.from(roleIds)` via the existing
`PATCH /api/groups/:id` mutation.
- On save, also invalidate `getGetGroupQueryKey(editingGroupId)` so the
detail cache (30s staleTime) reflects the new role list immediately
when the editor is reopened.
- Translations: added `admin.groups.tab.roles`, `admin.groups.rolesHint`,
and `admin.groups.rolesEmpty` in both `en.json` and `ar.json`.
Verification:
- `pnpm -w run typecheck` passes.
- `pnpm --filter @workspace/api-server test` — 42/42 pass.
- e2e (testing skill): logged in as admin, opened TeaBoy group, toggled
the `user` role on the new Roles tab, saved, reopened — toggle
persisted.
No deviations from the task description.
Replit-Task-Id: 42808400-9209-469b-b526-37c776ffb25d
|
||
|
|
7d29d63d2d |
Add Undo window when receivers cancel an incoming order
Original task (#73): Mirror the existing 7s Undo toast (already in place for owner-side cancels in My Orders) on the receiver-side cancel action in the Incoming Orders page. Implementation: - artifacts/teaboy-os/src/pages/orders-incoming.tsx - Captures the order's previous status (received | preparing) before issuing the cancel PATCH. - On success, shows a toast with title "Order cancelled" and an "Undo" ToastAction. Clicking Undo PATCHes the order status back to the previous value, then shows a "Restored" confirmation toast. - UNDO_WINDOW_MS = 7000 to match the owner-side flow. - artifacts/api-server/src/routes/service-orders.ts - Refactored the PATCH /orders/:id/status authorization. Detects "restore from cancelled" up front (existing.status === cancelled and next !== cancelled) and grants permission to: - owner/admin: may restore to pending or received (existing rule) - original assignee: may restore to received or preparing (NEW) - The same 15s server-side undo window applies. - artifacts/teaboy-os/src/locales/{en,ar}.json - Added incomingOrders.cancelled / undo / undone / restoreFailed strings in English and Arabic. - Regenerated lib/api-zod and lib/api-client-react via the api-spec codegen script (the previously-cached output was missing pending / received / preparing from the request body enum, which now matches the OpenAPI spec). Verification: - Typecheck passes for api-server and teaboy-os. - e2e flow verified: cancelling a received order shows the Undo toast; clicking Undo restores the order to "received" (DB confirmed); letting the window expire leaves the order permanently "cancelled" (DB confirmed). Deviations: None. Replit-Task-Id: 65c4b053-613a-4898-a0f7-b6fb2d680301 |
||
|
|
36f3704d1d | Git commit prior to merge | ||
|
|
21b53f0c54 |
Add automated test coverage for groups and access control
Original task: Task #75 — add automated tests for the groups system (CRUD + group-driven app visibility + auto-Everyone) and stabilize the flaky pagination test. Changes: - Extended artifacts/api-server/tests/groups-crud.test.mjs with four new tests: * PATCH /api/groups/:id updates fields and replaces userIds transactionally; a bad userId returns 400 and leaves prior membership unchanged. * Creating a group with invalid roleIds returns 400 and creates no rows. * DELETE /api/groups/:id removes a custom group; deleting a system group (Everyone, is_system = 1) returns 400 and the row stays. * /api/auth/register auto-assigns the new user to the Everyone system group; verified both via the response payload and a direct user_groups DB check. The test temporarily flips app_settings.registration_open to true and restores the prior value on completion. - The existing apps-group-visibility.test.mjs already exercises the /api/apps endpoint backed by getVisibleAppsForUser for group-derived admin, group member, and outsider — no changes needed there. - The previously flaky tests/admin-app-opens-pagination.test.mjs now passes consistently thanks to the existing 7d-window cleanup in its before hook; left as-is per the task's "fixed or skipped" criterion. Verification: `pnpm --filter @workspace/api-server test` reports 42/42 passing. The downstream Playwright suite (4 tests) also passes. Replit-Task-Id: 188075ef-382c-4af1-8a9a-90a0563c92c2 |
||
|
|
5de821fac8 |
Add Undo window for cancelled orders on My Orders
Original task (#72): Mirror the recently-added delete Undo toast for the cancel action so accidental cancellations can be reverted within ~7s. Changes - Frontend (artifacts/teaboy-os/src/pages/my-orders.tsx): OrderCard.handleCancel captures the order's previous status, and on successful cancel shows a toast with an "Undo" action (duration matches the existing UNDO_WINDOW_MS = 7000ms). Clicking Undo issues a PATCH to restore the order to its previous status (pending or received) and emits a "Restored" toast. Errors surface a "Could not restore the order" toast. - Cancel confirmation copy was softened (no longer says "can't be reopened") and a new restoreFailed string was added in both en.json and ar.json. - Backend (artifacts/api-server/src/routes/service-orders.ts): PATCH /orders/:id/status accepts pending and received as targets for the owner (or admin) when the existing status is cancelled, serving as the restore-from-cancelled transition. We require assignedTo to be null for pending and non-null for received so we always restore to the actually-prior state. The existing logic already broadcasts order_incoming_changed to receivers and emits order_updated to the owner, so receivers' incoming queues refresh automatically when an order is restored. notifyUser titleMap was extended with "Your order was restored" entries. - Time-bound restore: server enforces a RESTORE_WINDOW_MS of 15s (slightly larger than the 7s client window to absorb network/clock skew). After it expires, restore is rejected with `undo_window_expired`, so cancellation truly becomes permanent — this applies to admin too, so restore is strictly Undo and not an arbitrary reopen. - API spec (lib/api-spec/openapi.yaml): UpdateServiceOrderStatusBody enum extended to include pending and received; endpoint summary updated. Regenerated api-zod and api-client-react. Tests - Added a new test case in artifacts/api-server/tests/service-orders.test.mjs that covers: restore from pending, restore from received, pending<->received validation against assignedTo, stranger forbidden, and undo window expiry (by backdating updated_at). Verification - pnpm typecheck passes across libs and artifacts. - New api-server test "owner can restore (undo) a freshly cancelled order..." passes; all pre-existing service-order tests still pass. - e2e tested: login -> place order -> cancel -> Undo restores to Pending; cancel again -> let window expire -> order stays Cancelled with no Undo available. Notes / unrelated - The unrelated `admin-app-opens-pagination` test (`by-app: paginating with limit returns nextOffset until exhausted`) fails in the dev environment because the dev DB has > 100 app_opens rows for the chosen app and the test only walks up to 50 pages of size 2. This is a pre-existing flaky test unrelated to this task and was not modified. Replit-Task-Id: beca78bc-32f3-4cdc-8440-9a661b48363b |
||
|
|
a21428b2a4 |
Groups: address validator round 3 follow-ups
Round 3 rejection items closed:
- UI smoke test added: artifacts/teaboy-os/tests/admin-create-group-
app-visibility.spec.mjs. Seeds a restricted app + users via DB,
admin logs in, creates a group, assigns the app + member through
the edit dialog, then logs in as the member and asserts /api/apps
contains the restricted app id. Cleans up after itself. Passes.
- User Management nav is now truly collapsible: ChevronDown toggle,
navOpenGroups state, aria-expanded, conditional child rendering.
Defaults to expanded only when a child is the active section.
- Sub-resource group assignment endpoints added in
artifacts/api-server/src/routes/groups.ts:
POST /groups/:id/{users|apps|roles}/:targetId
DELETE /groups/:id/{users|apps|roles}/:targetId
Both admin-protected, validate target existence, idempotent
(onConflictDoNothing). Aggregate PATCH /groups/:id remains.
Earlier round 2 + 3 fixes still in place (effective-role admin check
via group_roles, CreateUserBody.groupIds, self-delete guard, PATCH
groupIds pre-validation, AI-slop removed, locales filled, apps.ts
inArray fix).
Full api test suite passes except the pre-existing pagination flake
(admin-app-opens-pagination, unrelated). Architect: PASS.
|
||
|
|
1fa19e048e |
Groups: address validator round 3 follow-ups
Round 3 rejection items closed:
- UI smoke test added: artifacts/teaboy-os/tests/admin-create-group-
app-visibility.spec.mjs. Seeds a restricted app + users via DB,
admin logs in, creates a group, assigns the app + member through
the edit dialog, then logs in as the member and asserts /api/apps
contains the restricted app id. Cleans up after itself. Passes.
- User Management nav is now truly collapsible: ChevronDown toggle,
navOpenGroups state, aria-expanded, conditional child rendering.
Defaults to expanded only when a child is the active section.
- Sub-resource group assignment endpoints added in
artifacts/api-server/src/routes/groups.ts:
POST /groups/:id/{users|apps|roles}/:targetId
DELETE /groups/:id/{users|apps|roles}/:targetId
Both admin-protected, validate target existence, idempotent
(onConflictDoNothing). Aggregate PATCH /groups/:id remains.
Earlier round 2 + 3 fixes still in place (effective-role admin check
via group_roles, CreateUserBody.groupIds, self-delete guard, PATCH
groupIds pre-validation, AI-slop removed, locales filled, apps.ts
inArray fix).
Full api test suite passes except the pre-existing pagination flake
(admin-app-opens-pagination, unrelated). Architect: PASS.
|
||
|
|
72e1f8d9ed |
Groups CRUD: address validator round 2 + 3 blockers
Round 2 fixes:
- apps.ts getVisibleAppsForUser uses getEffectiveRoleIds() so admin
detection honors group_roles inheritance (closes group-only-admin
bypass). Exported helper from middlewares/auth.ts.
- POST /users accepts optional groupIds[] via new CreateUserBody
schema. Validates ids exist; user create + auto-Everyone + requested
groups happen in one transaction. OpenAPI extended; api-zod and
api-client-react regenerated.
- DELETE /users/:id 400s on self-delete.
- scripts/src/seed.ts: removed `void inArray;` slop and unused import.
- Locales (ar+en): added admin.users.col.{displayNameAr,displayNameEn,
language} and admin.groups.searchPlaceholder.
Round 3 fixes:
- Admin Users "Add User" dialog gets a groupIds checkbox multi-select
populated from useListGroups; createUser sends groupIds when set.
- PATCH /users/:id validates ALL groupIds before deleting memberships
(returns 400 on any invalid id); replacement wrapped in transaction
to avoid partial-loss windows.
- Apps admin SQL bug: `${arr} = ANY(...)` produced bad params; now uses
`and(inArray(rolesTable.id, ids), eq(name,'admin'))`.
- New tests (artifacts/api-server/tests/apps-group-visibility.test.mjs):
group-derived admin sees all; group member sees group-granted +
unrestricted; outsider sees only unrestricted. All 3 pass.
Typecheck clean; full api test suite green except pre-existing
admin-app-opens-pagination flake (unrelated). Architect: PASS.
|
||
|
|
4595e792e4 |
Fix 5 validator blockers in groups/users admin work
1. apps.ts getVisibleAppsForUser: use getEffectiveRoleIds() so admin
gate honors group_roles inheritance (was direct user_roles only —
security bypass for group-only admins). Exported helper from
middlewares/auth.ts.
2. POST /users: switch from RegisterBody to new CreateUserBody schema
that accepts optional groupIds[]. Validates ids exist; user creation
+ auto-Everyone + requested groups happen in single transaction.
OpenAPI spec extended; api-zod and api-client-react regenerated.
3. DELETE /users/🆔 400 if req.session.userId === target (no
self-delete).
4. scripts/src/seed.ts: removed `void inArray;` AI-slop and dropped
unused inArray import.
5. Locales (ar.json + en.json): added admin.users.col.displayNameAr,
displayNameEn, language and admin.groups.searchPlaceholder.
Typecheck clean. groups-crud + service-orders + users tests pass.
Pre-existing admin-app-opens-pagination flake unrelated.
Architect re-review: PASS.
|
||
|
|
bf0768948e |
Groups + group-based RBAC: harden authz, atomic group writes, full admin UI
Auth middleware: - Add getEffectiveRoleIds() that UNIONs user_roles with group_roles via group memberships, used by requireAdmin / requirePermission / userHasPermission / getUserRoles. - requireAdmin and requirePermission now also reject inactive users with 401 (matching requireAuth), closing a session-after-deactivation bypass. Groups routes: - POST /groups and PATCH /groups/:id now wrap the group row write and all assignment writes in a single db.transaction via applyGroupAssignmentsTx(tx, ...), so partial state cannot leak. - validateAssignmentIds rejects unknown app/role/user ids with 400 before any insert. - Removed AI-slop: void or, void sql, as-unknown-as casts; conditions use Drizzle's SQL union type. Users route: - /api/users supports q, groupId, status filters (server-side). Admin UI (teaboy-os/admin.tsx): - UsersPanel wires q/groupId/status to the backend, shows display name and preferred language inline per row. - UserGroupsEditor now edits display names (ar/en), preferred language, active status, and group membership with a search box. - GroupsPanel adds a top-level group search box. - GroupDetailEditor Users tab adds a user search box. Infra: - scripts/post-merge.sh runs the seed (idempotent) so default groups Admins / TeaBoy / Everyone always exist after merges. Tests (artifacts/api-server/tests/groups-crud.test.mjs, all passing): - Admin-only access (regular user gets 403). - Default seed groups exist. - Create group + member assignment. - Bad userIds yields 400 with no leaked group row. - Admin role inherited via group_roles grants admin access. - Deactivated admin session is rejected with 401. - Group create rolls back atomically when assignment fails. - /api/users q + groupId + status filters return correct rows. Notes / drift: - "Roles" tab inside GroupDetailEditor and groupIds in CreateUserBody remain as proposed follow-ups (require OpenAPI spec changes). - Pre-existing pagination-test flake unrelated to this work. |
||
|
|
f5273af19f |
Task #74: Add groups system + admin User Management UI
Backend: - New schema: groups, user_groups, group_apps, group_roles (lib/db/src/schema/groups.ts) - Seeds Admins, TeaBoy, Everyone system groups idempotently and maps existing users - /api/groups CRUD with admin guard, batch counts, system-group delete protection - Validates appIds/roleIds/userIds (400 on missing) and wraps assignment writes in a single DB transaction (no partial state on failure) - /api/users gains q/groupId/status filters, batch role+group loading, groupIds replacement on PATCH, auto-assigns Everyone on admin-create - /auth/register also auto-assigns Everyone for consistent default linkage - buildAuthUser now returns groups (matches updated AuthUser OpenAPI schema) - App visibility (getVisibleAppsForUser) unions group-granted apps via group_apps + user_groups in addition to existing permission gating Frontend (admin.tsx): - Nav restructured: User Management section with Users + Groups children - Section deep-linked via #section=… URL hash - Users page rebuilt: search, group filter, status filter, sortable table, groups column, edit-groups dialog, mobile cards - New Groups page: cards with member/app/role counts, create dialog, detail editor with Info/Apps/Users tabs and system-group guard - ar/en translations added for all new keys Testing: - pnpm typecheck clean (api + web) - 25/26 api tests pass; the only failure is pre-existing flaky pagination test (admin-app-opens-pagination) — left as-is per scratchpad note - Code review feedback addressed (validation, transactions, register auto-assign) |
||
|
|
a772607636 |
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: 025835eb-6650-4735-a92c-18a34e17ee07 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/VRpzQEA Replit-Helium-Checkpoint-Created: true |
||
|
|
bae9c26927 |
Hide self-notifications across order lifecycle
Task: Audit notification rules in service-orders.ts so users never
receive notifications about actions they themselves initiated. The
specific scenario: a user with the `orders.receive` permission who
cancels their own assigned order would notify themselves under the
old logic.
Approach:
- Added an optional `actorId` parameter to `notifyUser`. When the
notification recipient equals the actor, the function returns
early (no DB insert, no socket emit).
- Threaded `actorId` through the three notification call sites:
1. POST /orders — placing an order no longer notifies the placer
even if they also hold the receiver role.
2. PATCH /orders/:id/confirm-receipt — a receiver claiming their
own order no longer notifies themselves.
3. PATCH /orders/:id/status — replaced the narrow
`!initiatedByOwner` guard with the generic `actorId === userId`
check inside `notifyUser`. This now covers owner-cancel as
before AND owner-as-assignee transitions (preparing/completed/
cancelled) where the owner happens to also be the assignee.
Notes / deviations:
- Kept the existing distinguishing wording for receiver-initiated
cancels ("claimed but later cancelled by the receiver"); it's
simply suppressed when the receiver is also the owner.
- No schema or API contract changes; purely server-side notification
filtering. Typecheck passes.
- No follow-ups proposed: existing project tasks already cover
automated tests for order/cancel flow, re-order from history, and
an undo window for cancellations.
Replit-Task-Id: 65b6d89a-6882-42f8-adb7-6ac1a2560748
|
||
|
|
d2c8e05c6c |
Add undo window for deleted orders on My Orders
Task #70: After deleting a finished order from My Orders, users had no way to recover it. This change introduces a short undo window so accidental deletes can be reversed while keeping the cleanup workflow snappy. Approach: - Implemented as a client-side deferred-delete in artifacts/teaboy-os/src/pages/my-orders.tsx. No backend changes. - On delete (single trash icon or bulk "Clear N finished"): * The order(s) are added to a local pendingDeleteIds set so they immediately disappear from the list. * A toast is shown with a localised "Undo" action and a 7s duration. * A setTimeout is scheduled to call DELETE /api/orders/:id for the affected ids and then invalidate the my-orders query. - Clicking "Undo" cancels the timer, removes the ids from pendingDeleteIds (so the cards reappear), dismisses the toast, and shows a "Restored" confirmation toast. - On unmount, any still-pending deletions are flushed so they aren't silently lost if the user navigates away mid-window. - If the deferred DELETE later fails, the ids are restored to the list and a "Could not delete the order" destructive toast is shown. OrderCard now delegates the actual delete to a new onDelete callback from the page (the confirmation dialog stays in the card). Bulk clear no longer fires N sequential mutations from inside a confirm dialog spinner — it just schedules them. i18n: added myOrders.undo / myOrders.undone strings to en.json and ar.json. Verification: - pnpm tsc --noEmit passes for teaboy-os. - e2e (runTest) covered: single delete + undo restores; single delete with no undo becomes permanent after 7s; bulk clear + undo restores all. DB state was asserted in each case. Follow-up proposed: #72 — apply the same undo pattern to cancelled orders. Replit-Task-Id: 1854a0b1-f6d3-4148-b2c2-b0940202d0df |
||
|
|
15b32d5ac5 |
Adjust service cards to be smaller and tappable directly
Modify service card layout for better density, enable direct image tapping to open orders, and shorten "Place order" button text to "Send". Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: ff3bca96-6332-4411-9809-103494a69c1f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/VRpzQEA Replit-Helium-Checkpoint-Created: true |
||
|
|
1cea719b80 |
Task #69: Delete past orders from My Orders
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled orders; admin can delete any. 401/403/404/204 responses. Emits order_deleted to owner and assignee for live invalidation. - Update OpenAPI spec with deleteServiceOrder operation; regenerate api-client-react + api-zod. - Frontend: per-order trash button on completed/cancelled cards with confirm dialog; bulk "Clear finished (N)" button at top of list with confirm dialog and toast summary (handles partial failures). - Add ar/en locale keys under myOrders for delete + clear flows (singular/plural variants). - Backend tests cover all paths: pending owner=403, stranger=403, owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401. All 25 service-order tests pass; the 1 failing test (admin-app-opens- pagination) is unrelated/pre-existing. Follow-ups proposed: #70 undo for deleted orders, #71 audit self-notification on receiver-cancel-own-order. |
||
|
|
58553d3d36 |
Task #69: Delete past orders from My Orders
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled orders; admin can delete any. 401/403/404/204 responses. Emits order_deleted to owner and assignee for live invalidation. - Update OpenAPI spec with deleteServiceOrder operation; regenerate api-client-react + api-zod. - Frontend: per-order trash button on completed/cancelled cards with confirm dialog; bulk "Clear finished (N)" button at top of list with confirm dialog and toast summary (handles partial failures). - Add ar/en locale keys under myOrders for delete + clear flows (singular/plural variants). - Backend tests cover all paths: pending owner=403, stranger=403, owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401. All 25 service-order tests pass; the 1 failing test (admin-app-opens- pagination) is unrelated/pre-existing. Follow-ups proposed: #70 undo for deleted orders, #71 audit self-notification on receiver-cancel-own-order. |