c0e55752a358b20f9a8e6fe2c361c4adfa4437a5
70 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c0e55752a3 |
Task #183: clickable dependency counts on admin Apps/Services/Users
Turned the inline dependency-count badges on each admin row into
focusable, keyboard-accessible buttons that open a drill-in modal
listing the actual rows behind the count.
Backend (artifacts/api-server/src/routes):
- apps.ts: GET /admin/apps/:id/dependents/{groups,restrictions,opens}
- services.ts: GET /admin/services/:id/dependents/orders
- users.ts: GET /admin/users/:id/dependents/{notes,orders,conversations,messages}
All paginated (limit default 50, max 200; offset) returning
{items, totalCount, limit, offset, nextOffset}. Restrictions joins
the role names that include each required permission.
OpenAPI:
- Added 8 path operations + 16 schemas (Item + Page) and re-ran
`pnpm --filter @workspace/api-spec run codegen`.
Frontend (artifacts/tx-os/src/pages/admin.tsx):
- New DependencyDrillIn component reuses DrillInShell (Escape +
backdrop close, RTL/LTR safe) and the existing LoadMoreSection
pagination pattern from AppOpensDrillIn.
- Each count part in Apps, Services, and Users panels is now a
<button> with a unique data-testid (e.g. app-counts-groups-213,
user-counts-messages-5) and an aria-label that reads
"View {count}".
- AdminPage owns dependencyTarget state for Apps/Services counts;
UsersPanel owns its own (it already encapsulates user list).
Translations:
- Added admin.dependents.* (titles, subtitles, empty/error/load
labels, conversation/order/message helper strings, order status
enum) to en.json and ar.json.
Verification: - tx-os typecheck clean; api-server has only the pre-existing
executive-meetings.ts errors (untouched).
- e2e tested via runTest: login as admin, opened Apps panel,
drilled into groups + opens (Load more grew 50→100), drilled into
Tea orders, drilled into user 5 conversations and messages,
switched to Arabic/RTL and re-opened the Groups drill-in to
confirm Arabic rendering with no raw i18n keys.
Replit-Task-Id: fe96a05b-325f-4e1f-901b-3a2235fb24b5
|
||
|
|
90c319fb25 |
Show inline dependency counts on the Roles admin list (Task #182)
The Apps, Services, Users, and Groups admin panels already surface their dependency counts inline so admins know what's affected before clicking. The Roles panel previously hid this — admins had to open the delete dialog to see how many users/groups would be affected. This change adds the same inline display to the Roles panel for consistency. Changes - lib/api-spec/openapi.yaml: Added optional `userCount` and `groupCount` fields to the `Role` schema (matching the App pattern: optional, populated only by the admin list endpoint, with descriptive comments). - artifacts/api-server/src/routes/roles.ts: GET /roles now batches two grouped count queries (user_roles, group_roles) and merges the counts into each list item — same shape as GET /apps. Empty-list short-circuits before running the aggregations. - lib/api-zod/src/generated/api.ts: Regenerated via the api-spec codegen script (orval). ListRolesResponseItem now includes the optional counts. - artifacts/tx-os/src/pages/admin.tsx (RolesPanel): Each role card renders an inline counts row using the existing `admin.roles.usersCount` / `admin.roles.groupsCount` translation keys (no new copy needed). Mirrors the Apps panel pattern: 11px muted-foreground text with bullet separators, only renders when at least one count is > 0, and exposes a `data-testid="role-counts-<id>"` for tests. Notes / deviations - The task description said "the role list endpoint already returns userCount/groupCount" but it didn't — the counts only existed on /roles/:id/usage. Added them to the list endpoint following the same pattern Apps and Groups already use. - The pre-existing admin.roles.usersCount/groupsCount keys have no `_one`/`_other` plural variants; I kept it that way to stay consistent with the Apps panel keys (which also have no plural variants). Verification - `pnpm -w run typecheck` passes for tx-os and roles.ts (pre-existing unrelated typecheck errors in executive-meetings.ts remain — not touched by this change). - e2e test (testing skill, status: success): logged in as the seeded admin, opened the Roles panel, verified inline counts render on the admin and user roles, bullet separator is present, and roles with zero dependencies don't render an empty counts area. Replit-Task-Id: 8c99d912-8b3a-4e80-aca7-ec167e6e75e6 |
||
|
|
b3ad701ba6 |
Task #243: Admin audit log — focused readability + actor-filter subset
Landed a tight subset of the 13-item umbrella, mirroring the proven narrow-then-defer pattern from #242: - #195 — Plain DELETE /api/services/:id now writes a `service.delete` audit row carrying nameEn + nameAr (force-with-deps still uses the dedicated `service.force_delete`). Added matching `service.delete` formatter case + EN/AR i18n keys, and surfaced nameAr on the existing `service.force_delete` summary. - #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export. openapi.yaml updated, codegen regenerated, server filter wired through parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI got an actor dropdown wired into params + export URL + reset, and a new audit-logs-actor-filter API test (4 cases) covers list narrowing, exclusion, invalid input, and CSV export. - #178 — Formatter unit tests for user.delete (id-only, EN/AR display name resolution, force flag, force + name) and the new service.delete (id-only, EN/AR), 11 new cases (33/33 pass). Skipped #194 — already implemented; users.ts DELETE persists displayName fields and audit-summary already renders user.deleteWithName/forceDeleteWithName. Deferred via follow-ups (no duplicate of existing #182/#183/#184): - F1: #196 recent-activity endpoint + 5 admin panels - F2: #205+#206+#208 permission history CSV/name resolution/timeline - F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs Validation: tx-os typecheck clean; pre-existing executive-meetings.ts errors not regressed; all targeted server tests pass (delete-force-warnings 10, audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new actor-filter 4, broader audit/services sweep 40); e2e test verified actor dropdown rendering, filter behavior, readable Arabic service.delete summary, and CSV export honoring the filter. |
||
|
|
fb2d75ecd7 |
Task #164: Per-user notification preferences for Executive Meetings
Lets each user choose whether to receive in-app and/or email notifications
for each executive-meeting event type (meeting_created, request_submitted,
request_approved/rejected/needs_edit, task_assigned, task_completed).
Defaults to "everything on" when no preference row exists, preserving the
prior fan-out behavior for users who never visit the new UI.
Schema:
- New executive_meeting_notification_prefs table (user_id FK CASCADE,
notification_type varchar(64), in_app bool default true, email bool
default true, plus a unique index on (user_id, notification_type)).
- Pushed to dev DB via `pnpm --filter @workspace/db push`.
Backend:
- Exported EXECUTIVE_MEETING_NOTIFICATION_TYPES (canonical list) +
filterRecipientsByNotificationPref(ids, type, channel) helper that
returns only recipients whose row says the channel is on (default-on
semantics for missing rows).
- recordExecutiveMeetingNotifications now filters recipients by
channel="inApp" before inserting; sendExecutiveMeetingEmail filters
by channel="email" before SMTP delivery.
- New endpoints under /executive-meetings/notification-prefs:
GET → { types, prefs } merged with defaults.
PUT → upserts each supplied (type, channel) pair via
onConflictDoUpdate inside a transaction.
Frontend:
- New NotificationPrefsCard at the top of the Notifications section in
artifacts/tx-os/src/pages/executive-meetings.tsx. Renders a Switch per
(event type × channel) with batched save, dirty-state tracking, reset
button, and useToast feedback.
- Translation keys for the card added to en.json and ar.json under
executiveMeetings.notificationsPage.prefs.
Tests:
- 5 new tests in artifacts/api-server/tests/executive-meetings.test.mjs:
GET defaults, PUT roundtrip + upsert, 400 on unknown type, in-app
fan-out filtering (muted approver gets no row, control approver still
does), and channel-independence (muting only the email channel leaves
in-app delivery intact while persisting email=false in the DB row that
sendExecutiveMeetingEmail's filter reads).
- All 36 executive-meetings tests pass. Full suite shows only one
pre-existing flaky test elsewhere (groups-crud count assertion),
unrelated to these changes.
- Added e2e UI test that logs in as admin, toggles a preference, saves,
refreshes, and confirms persistence.
- After-hook cleans up new prefs rows for created users.
Follow-ups proposed: #236 (one-click reset to defaults), #237 (admin
view/override of any user's prefs).
Replit-Task-Id: 284ce15d-40d7-447e-90ca-090b44d8227b
|
||
|
|
60a18e1c7c |
Task #162: Let admins pre-set required permissions while creating an app
The "Required permissions" section was previously edit-only because `POST /api/apps/:id/permissions` needs an app id, leaving a brief window where a freshly created app was visible to everyone before the admin could re-open the dialog and gate it. The Add app dialog now lets the admin pick required permissions up front and the new app + its `app_permissions` rows are written in a single transaction. Changes: - `lib/api-spec/openapi.yaml`: extended `CreateAppBody` with an optional `permissionIds: integer[]` field. Ran `pnpm --filter @workspace/api-spec run codegen` so `lib/api-zod` and `lib/api-client-react` reflect it. - `artifacts/api-server/src/routes/apps.ts`: `POST /apps` now de-dupes and pre-validates `permissionIds`, returns 404 if any id is unknown (without creating the app), and inside one transaction inserts the app, the `app_permissions` rows (with `.onConflictDoNothing()` against the composite primary key), and a single `permission_audit` row (`previousIds: []`, `newIds: requestedIds`). After the transaction it also writes one `app.permission.add` audit_logs entry per inserted permission so the admin log mirrors the post-create flow. - `artifacts/tx-os/src/pages/admin.tsx`: added `permissionIds: number[]` to `AppForm`, a new `NewAppPermissionsPicker` component (rendered only in create mode — edit mode keeps the existing `AppPermissionsEditor` with its impact preview) that lets admins add/remove permissions locally before submit, and wired `handleSaveApp` to forward the selected ids when creating. Existing edit path strips the field so the update payload remains unchanged. - `replit.md`: documented the new picker and POST /api/apps behavior. No impact preview is shown in the create-mode picker because a brand new app starts with zero users seeing it, so adding permissions cannot hide it from anyone. Code-review follow-up: tightened input validation so non-integer or non-positive `permissionIds` now return 400 with a clear error instead of being silently dropped by the previous filter. The legacy single-add endpoint already used this exact 400 message, so behavior stays consistent across both create and update paths. Verification: - `pnpm --filter @workspace/api-spec run codegen` passes. - `pnpm --filter @workspace/api-server` typechecks with no new errors (executive-meetings.ts errors are pre-existing and unrelated). - Ran the existing app-permission test suites (`app-permission-audit.test.mjs`, `app-permissions-crud.test.mjs`, `app-permissions-impact.test.mjs`) directly — all 16 tests pass. - Ran an e2e Playwright test (login as admin → Add app → pick a permission → save → verify the row shows 1 restriction → reopen and confirm the assigned permission). All steps passed. Follow-up proposed: automated tests for the new create-with-permissions endpoint behavior (#231). Replit-Task-Id: c229777c-4036-4a6a-b4cb-05ccc18f6c9b |
||
|
|
b507b33cad |
Add app-permissions impact preview before tightening an app's gate
Mirrors the existing role-permissions impact preview UX for app
permissions. Admins now see how many currently-visible users would lose
access before they add a permission requirement to an app, plus the
groups (via group_apps) that offset the loss because their members keep
access regardless.
Backend
- New endpoint POST /api/apps/:id/permissions/impact-preview in
artifacts/api-server/src/routes/apps.ts. Implements the same OR
semantics as getVisibleAppsForUser: a user "sees" an app if they hold
ANY required permission (direct or via a group role) OR they belong
to a group granted the app via group_apps. Admins are excluded from
counts since they always see every app. Short-circuits with
noChange:true when the candidate set equals the current set.
- OpenAPI schema (lib/api-spec/openapi.yaml): adds the path,
AppPermissionsImpactBody, AppPermissionImpactGroup,
AppPermissionsImpact. Regenerated lib/api-client-react bindings.
Frontend
- AppPermissionsEditor (artifacts/tx-os/src/pages/admin.tsx): debounced
(350ms) cancel-safe preview when a pending permission is selected,
warning banner with affected/visible counts and offsetting groups,
and a confirmation dialog when affectedUserCount > 0. Add button is
disabled while the preview is loading or errored to keep the warning
trustworthy.
- i18n keys added to en.json and ar.json under
admin.appPermissions.{impactTitle, impactLoading, impactError,
impactNone, impactSummary, impactViaGroups, confirmTitle, confirmBody,
confirmAction}.
Tests
- artifacts/api-server/tests/app-permissions-impact.test.mjs: 7 tests
covering noChange short-circuit, unrestricted-app tightening,
candidate that keeps an existing permission, group_apps offset,
unknown app (404), invalid payload (400), and admin-only enforcement.
All 18 app-permissions tests pass.
- E2E flow verified via runTest: admin login → /admin → Apps → edit
app → select permission → preview banner appears → Add → confirm
dialog → cancel without writing.
Out-of-scope (filed as follow-ups #228 and #229): listing the specific
affected user IDs in the preview, and warning when REMOVING a
permission broadens access.
No deviations from the task spec.
Replit-Task-Id: 8b2ff9ea-f95e-4bdb-b268-95b5d17154ca
|
||
|
|
a8467f810b |
Task #148: Make pnpm --filter @workspace/db run push work without manual SQL
## Original task
`pnpm --filter @workspace/db run push` was failing with a duplicate-key
error on `app_permissions` (and a missing FK on
`executive_meeting_notifications`) because legacy data in the dev DB
violates constraints the schema now declares. Devs had to drop into psql
to fix it, which made bootstrapping painful and left
`role_permission_audit` reliant on hand-applied SQL.
## Changes
- Added `lib/db/scripts/pre-push-cleanup.ts`, an idempotent cleanup that:
- Collapses duplicate `app_permissions` rows to one per
`(app_id, permission_id)` so the composite PK can be added.
- Deletes orphan `executive_meeting_notifications` rows so the new
`ON DELETE CASCADE` FK can be added.
- Skips both checks when the tables don't exist yet (fresh DB
no-op).
- Wired the script into `lib/db/package.json` so both `push` and
`push-force` run cleanup first (`pnpm run pre-push-cleanup && drizzle-kit push ...`).
- Added `tsx` to `lib/db` devDependencies (catalog version) so the
package can run the cleanup without leaning on another workspace.
- Updated `replit.md` Deployment / Migration Runbook to reflect that
cleanup is now automatic — no manual SQL required in any environment.
## Verification
- `pnpm --filter @workspace/db run push` now collapses 2 duplicate
groups + removes 1978 orphan notifications, then succeeds with
`[✓] Changes applied`.
- Re-running push is idempotent: second run reports no duplicates and
no orphans, then succeeds.
- `pnpm --filter @workspace/db run push-force` (used by
`scripts/post-merge.sh`) was also verified end-to-end.
- Confirmed `app_permissions` now has the composite PK,
`executive_meeting_notifications` has the cascade FK, and
`role_permission_audit` matches the schema.
## Notes
- A pre-existing unrelated typecheck error in
`artifacts/api-server/src/routes/executive-meetings.ts` (font
settings `scope` overload) was confirmed to exist on `main` before
any changes here and is out of scope for this task.
- Proposed follow-up #213 to move from `push`/`push-force` to
versioned `drizzle-kit migrate` so legacy-data backfills are checked
in instead of living in a generic pre-push script.
Replit-Task-Id: 2efc4e22-0c65-48a1-b573-319474698b96
|
||
|
|
2bf2f3cd3a |
Task #147: Add structured permission-change audit (users, groups, apps)
Mirrors the existing role-permission audit pattern with a unified
`permission_audit` table capturing actor, target, prev/new id sets, and
timestamp written in the same transaction as the change.
Schema & API
- New `permission_audit` table (target_kind, target_id, change_kind,
actor_user_id, previous_ids[], new_ids[], created_at) with index on
(target_kind, target_id, created_at).
- Transactional audit writes in routes/users.ts (POST/DELETE roles,
PATCH groupIds), routes/groups.ts (PATCH + add/remove members for
users/roles/apps), routes/apps.ts (POST/DELETE permissions).
- Cross-entity mirroring: when group membership changes via a group
endpoint, a user.groups row is also written for each affected user
(and vice versa via PATCH /users), so each entity's history is
exhaustive regardless of which editor was used.
- Admin-only GET /users/:id/audit, /groups/:id/audit, /apps/:id/audit
with limit/offset/actorUserId/from/to filters and the same response
shape as role audit.
- OpenAPI types + codegen regenerated.
UI
- Reusable PermissionAuditHistory component in admin.tsx wired into
UserGroupsEditor, GroupDetailEditor (new "history" tab), and the
editing-app dialog. App history correctly resolves permission ids
(NOT roles) via useListPermissions.
- Bilingual i18n keys added under admin.{users,groups,apps}.history*
in en.json + ar.json.
Tests
- New backend tests: user-permission-audit, group-permission-audit,
app-permission-audit (14 cases — transactional capture, GET filters
& pagination, admin-only, 404 on unknown id, plus 2 new mirror
tests covering cross-entity audit visibility). All pass; 35
adjacent role/groups/users/audit-coverage tests still pass.
Notes
- replit.md updated to list `permission_audit` table.
- Restored opengraph.jpg (an unrelated stray binary diff).
- Code-review comments addressed: cross-entity asymmetry fixed via
mirroring; opengraph.jpg restored.
- Follow-ups proposed: timeline UI improvements, cascade audit on
delete/bulk paths, e2e UI test for History sections.
Replit-Task-Id: 4ba8c8de-8dfd-42c7-a3bc-964a1ed3a1a3
|
||
|
|
89df2385a8 |
Task #207: custom subheadings inside executive-meeting attendee cells
Adds a new `kind` column (`varchar(16) NOT NULL DEFAULT 'person'`) on `executive_meeting_attendees` so users can interleave free-text section labels with person rows in a meeting's attendee list. Subheadings are excluded from the running attendee number and from the per-meeting attendee count surface, but reorder and delete identically to person rows. DB - New `kind` column in `lib/db/src/schema/executive-meetings.ts`, defaulting to `"person"`. Applied via direct SQL because `drizzle-kit push` trips on a pre-existing duplicate-row issue in `app_permissions` (already documented in replit.md). API (artifacts/api-server/src/routes/executive-meetings.ts) - `attendeeSchema` accepts `kind: z.enum(["person","subheading"])` with default `"person"`. - All 4 insert paths round-trip `kind`: POST create, PATCH meeting update (attendees replacement), PUT `/attendees`, and duplicate. - `pdf-renderer` mapper forwards `kind`. `PdfMeetingAttendee.kind` typed as `string | null` to match the DB column shape. Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx) - `AttendeeFlow` renders subheadings on a separate full-width row (`basis-full`, semibold, centered) and increments the running person index only for `kind === "person"`. Pending ghost row branches on `pendingKind`. - Inline "+ subheading" chip via `onStartAdd(type, "subheading")`. - Manage dialog: addSubheading button, subheading rows hide the title field, show a kind badge, and reorder/delete identically. Manage list summary count filters to person rows only (architect fix). - Print page renders subheadings as `.em-print-subheading` and skips them in the running counter. - New locale keys under `executiveMeetings.schedule` and `executiveMeetings.manage.attendees` in both `ar.json` and `en.json`. PDF - Subheadings print as `— label —` and never advance `personIdx`. Tests - New spec `executive-meetings-attendee-subheadings.spec.mjs` seeds mixed person+subheading rows and asserts (a) the subheading row renders, (b) numbering stays `1-`, `2-`, `3-` with a subheading wedged between persons, (c) zero-subheading meetings keep legacy numbering. Runs in both AR and EN. All 4 cases pass. Code review - Architect found one regression (Manage list summary count included subheadings) — fixed. |
||
|
|
fe736e3ff7 |
Task #146: Filter and paginate role permission history
Summary
- Backend: GET /api/roles/:id/audit now accepts limit (default 10, max 200),
offset, actorUserId (0 = no filter), and from/to (YYYY-MM-DD UTC). The
response is now a paginated envelope `{entries, totalCount, limit, offset,
nextOffset}` instead of a bare array.
- OpenAPI: lib/api-spec/openapi.yaml updated with the new params and a new
RolePermissionAuditList schema; client hooks regenerated via
`pnpm --filter @workspace/api-spec run codegen`.
- Frontend: RolePermissionHistory in artifacts/tx-os/src/pages/admin.tsx is
now self-contained — owns its own filter state, fetches the user list for
the actor dropdown, applies actor changes immediately, and validates date
inputs (rejecting invalid / inverted ranges).
- Pagination: switched to TRUE OFFSET PAGINATION. The first page comes from
React Query (so live cache invalidations after a save still refresh it),
and subsequent "Load more" clicks fetch `offset = nextOffset` imperatively
via getRolePermissionAudit() and append the rows to local state. There is
no client-side ceiling on how far back an admin can page; we simply stop
showing the button when nextOffset is null. A filtersKey effect resets the
appended pages whenever any filter (actor / from / to) changes so we never
serve overlapping or out-of-order rows.
- i18n: added missing keys in artifacts/tx-os/src/locales/{en,ar}.json
(historyEmptyFiltered, historyShowing, historyLoadMore, historyFilters.*,
historyErrors.*).
- Tests: artifacts/api-server/tests/role-permission-audit.test.mjs updated
to read the new envelope and now also covers offset-based pagination,
actorUserId filtering (including the "0 = no filter" semantic), and
date-range filtering (in-range / past-range / inverted / garbage). All 9
audit tests pass; tx-os typecheck clean.
- E2E: ran the testing skill end-to-end against /admin → role edit dialog →
history panel: created a fresh role through the UI, made 25 permission
writes, verified 10 → 20 → 25 pagination with no duplicates and correct
hide-on-end behaviour, filter-by-actor reset to first page, empty date
range showed empty state, inverted dates surfaced the validation error.
Drift / notes
- Pre-existing executive-meetings tests in the `test` workflow are still
failing — unchanged by this task.
- The Arabic language toggle isn't exposed in the page header in this build,
so the e2e Arabic step was skipped; locale strings are in place and the
test ids do not change with locale.
- Code review (initial pass) flagged a 200-row UI ceiling in the previous
"growing limit" approach. Replaced with true offset pagination (described
above) so admins can scroll back through arbitrarily long histories.
Code review follow-ups (round 2)
- Tightened parseRoleAuditUtcDate to reject impossible calendar days
(2024-02-31, 2025-13-01, etc.) instead of silently rolling forward.
- Aligned OpenAPI: actorUserId schema is now `minimum: 0` so the spec
matches the runtime "0 = no filter" contract; client regenerated.
- Added a targeted backend test that asserts impossible dates → 400.
Code review follow-ups (round 3)
- UX: when applied filters become invalid, the History list now hides the
stale entries and shows the "fix filters first" hint instead, and the
Load more button is hidden until filters are valid again. Avoids
presenting yesterday's results as the current view.
Code review follow-ups (round 4)
- Belt-and-braces: also reset the appended history pages when the first
page's totalCount + first-row id signature changes, so an external cache
invalidation (e.g. another save while the dialog is still open) cannot
leave appended pages out of sync with the refreshed first page.
Replit-Task-Id: cc3c9e83-921f-4f72-b443-79f95f6467b1
|
||
|
|
058cb8b817 |
Make forced-delete dependency chips clickable to pivot the audit log
Task #134: Admins investigating a forced deletion can now click any dependency chip on a force_delete row to jump to a pre-filtered audit log view of the related history. Backend (artifacts/api-server, lib/api-spec): - Added targetType + targetId query params to GET /api/admin/audit-logs and its CSV export. targetId is validated as a positive integer (400 on bad input). Codegen regenerated for the React API client. Frontend (artifacts/tx-os/src/pages/admin.tsx): - Dependency chips on forced-delete rows are now real <button> elements with aria-labels and keyboard focus. Non-deletion rows are unchanged. - Chip → pivot mapping: groupCount→targetType=group, memberCount→targetType=user, appCount→targetType=app, roleCount→targetType=role; cascade chips (orderCount, messageCount, noteCount, etc.) pivot to the parent (targetType+targetId). - Active filter is reflected in the URL hash (deep-linkable + reload safe) and shown as a dismissible pill in the panel; the pill includes a Clear button. Switching audit sub-section drops the filter. - Section sync logic preserves hash params and uses a one-shot skipNextSectionSync ref so initial deep-linked hashes aren't clobbered. - New i18n keys in en.json and ar.json for filter labels and chip aria-labels. Tests: - New backend tests in artifacts/api-server/tests/audit-logs-target-filter.test.mjs cover targetType narrowing, targetType+targetId narrowing, invalid targetId rejection, combination with forcedOnly, and CSV export honoring the new filters (7 tests, all passing). - Verified end-to-end via the browser testing skill: chip click, filter pill, clear, deep-link reload all behave correctly. Pre-existing unrelated failures (not touched): two PDF archive tests in executive-meetings.test.mjs and the matching typecheck errors in executive-meetings.ts. Replit-Task-Id: 46f972ef-2874-4fc3-95c5-53d0ff0732e9 |
||
|
|
11aaaf2abe |
Replace executive-meetings print-to-PDF with server-side PDF generator
The "Download PDF" button on the executive-meetings page now hits a real
backend endpoint that returns a true PDF (no more browser print dialog),
respects each user's font preferences (family, size, weight, alignment)
with proper Arabic RTL shaping, and archives every download.
The renderer maps each saved fontFamily ("system", "Cairo", "Tajawal",
"Noto Naskh Arabic", "Amiri") to a concrete pair of bundled font files
so the chosen family genuinely changes the embedded glyphs — Cairo and
Tajawal pick Noto Sans Arabic, the Naskh-style families and the system
default pick Noto Naskh Arabic, and Latin glyphs render in DejaVu Sans
across the board. Headers, body cells, and footer all flow through the
same script-aware font selection.
Backend (artifacts/api-server)
- New GET /api/executive-meetings/pdf?date=&lang= route in
src/routes/executive-meetings.ts that fetches the day's meetings +
attendees, renders a PDF, uploads it to object storage, writes an
executive_meeting_pdf_archives row (date, generated_by, byte_size,
storage_url), and streams the file back inline.
- New src/lib/pdf-renderer.ts using pdfkit + bidi-js with bundled
Noto Naskh Arabic and DejaVu Sans fonts in assets/fonts/.
- Added byte_size column on executive_meeting_pdf_archives (also in
lib/db schema) and rebuilt lib/db.
- Added ambient types for bidi-js; installed @swc/helpers to satisfy
fontkit at runtime.
- build.mjs now copies pdfkit's data/ folder (Helvetica.afm, etc.)
into dist/data so the bundled server can construct PDFDocument.
Frontend (artifacts/tx-os)
- PdfSection in src/pages/executive-meetings.tsx now renders a single
"Download PDF" button that fetches the endpoint, builds a Blob, and
downloads it. Removed the print/archive-creation buttons.
- Archive list shows a Download button for new /objects/... rows and a
read-only "Legacy snapshot" badge for older print: rows.
- Added byteSize on PdfArchive + size formatting; updated en/ar locales.
Tests
- New test "PDF GET /executive-meetings/pdf returns a real PDF and
archives it" in tests/executive-meetings.test.mjs covers: bad-date
400, unauthenticated 401, real %PDF body + content-type/disposition,
archive row with byteSize/generatedBy/filePath, empty-day handling,
and font-family mapping (Cairo embeds NotoSansArabic; Noto Naskh
Arabic embeds NotoNaskhArabic).
- All 23 executive-meetings tests pass.
Rebase
- Rebased onto main-repl/main (
|
||
|
|
8b2fe3164d |
Task #109: Admin UI to manage app required permissions
- Added 3 admin-only API endpoints in artifacts/api-server/src/routes/apps.ts:
- GET /api/apps/:id/permissions — list permissions gating an app
- POST /api/apps/:id/permissions — add a permission (idempotent via
onConflictDoNothing() on the (app_id, permission_id) composite PK)
- DELETE /api/apps/:id/permissions/:permissionId — remove (idempotent, 204)
- Documented the new endpoints in lib/api-spec/openapi.yaml with two new schemas
(AddAppPermissionBody, AppPermissionLink) and re-ran codegen.
- Added a "Required permissions" section (AppPermissionsEditor) to the existing
Edit App dialog in artifacts/tx-os/src/pages/admin.tsx, using the generated
hooks. The section is shown only when editing an existing app (it needs an
app id). Wired up admin.appPermissions.* i18n keys in en.json + ar.json.
- Added artifacts/api-server/tests/app-permissions-crud.test.mjs with 5 tests
(empty list, idempotent add, 404 on unknown app/perm, idempotent delete,
403 for non-admins). All 5 pass; related tests
(app-permissions-unique, apps-group-visibility, list-dependency-counts) still pass.
- Verified the new admin UI end-to-end with the testing skill: admin login,
open Edit App dialog, add/remove a required permission, and confirm the
section is hidden in the Add App dialog.
Notes / scope:
- Pre-existing duplicate rows in app_permissions had to be deduped and
`pnpm --filter @workspace/db run push` was run once so the composite PK
could be added (separate task "Re-run the Drizzle schema push" already
exists for this project-wide chore).
- No audit logging here — separate existing task already covers it.
- The "test" workflow shows a pre-existing ECONNREFUSED race; an existing
task already tracks making the test workflow wait for the API server.
Replit-Task-Id: 912bb163-6b6f-43d3-9934-41fc97519337
|
||
|
|
539f11e25d |
Task #152: cell-merge + current-meeting tint on EM schedule
Adds two related features to the executive-meetings schedule: 1. Current-meeting tinting — meeting/attendees/time cells of the row matching the current time get a soft wash of the user's highlight color (~13% alpha), keeping their original text color and the existing colored ring. The # cell stays solid white/red. 2. Cell-merge overlay — editors can merge "meeting+attendees", "meeting+attendees+time", or the whole row into a single free-text cell. Stored in the DB so all viewers see the same overlay; the originals (titleAr/En, attendees, start/endTime) stay untouched and are restored on Unmerge. Implementation notes - DB: 3 nullable columns added to executive_meetings (merge_start_column, merge_end_column, merge_text). Applied via direct ALTER TABLE because pnpm --filter db run push is blocked by the pre-existing app_permissions duplicate (Task #148, surfaced as the new follow-up #156). Schema file kept in sync. - API: meetingPatchSchema accepts an optional `merge` field (`null | {start,end,text}`) with start<=end ordering and server-side sanitizeRichText on the text. Reuses the existing PATCH route. - Frontend: new MergeMenu component + canonical-order range resolution so merges still render when boundary columns are hidden, and gracefully degrade to unmerged rendering (without losing DB state) when column reordering makes the visible span non-contiguous. Unmerge stays available whenever a stored merge exists, even in degraded state, and existing mergeText is preserved on re-merge. - i18n: en + ar keys under executiveMeetings.merge.* Out of scope (deferred to follow-ups #154 / #155, per task brief) - API + e2e test coverage for the merge endpoint and UI flows - Realtime emission for executive-meetings mutations (no existing emit on this surface; covered by #155) Validation - TypeScript compiles cleanly across api-server + tx-os. - API tests: 108/110 pass — the 2 failures are the pre-existing app_permissions-unique cases (Task #148, follow-up #156), unrelated to this change. - Architect code review approved after addressing hidden-column fallback, non-contiguous reorder degradation, and Unmerge-availability edge cases. |
||
|
|
a14b589006 |
Audit role permission changes (Task #100)
Record an audit trail every time a role's permissions change and surface recent history inside the role edit dialog. Schema (lib/db): - New `role_permission_audit` table: id, roleId (FK roles), actorUserId (FK users, nullable on delete), previousPermissionIds int[], newPermissionIds int[], createdAt. Created via raw SQL because `drizzle push` currently fails on a pre-existing app_permissions duplicate (followup #148 tracks fixing this). API (artifacts/api-server): - PUT /api/roles/:id/permissions now wraps the permission delete/insert, the legacy audit_logs row, and the new role_permission_audit row in a *single* transaction so the permission set and its audit trail always commit (or roll back) together. Previous IDs are also read inside the transaction to avoid races. - A role_permission_audit row is written on EVERY PUT call (per task spec), including no-op saves; the GET handler computes addedPermissionIds/removedPermissionIds so the History UI can distinguish meaningful changes from no-op saves. - New GET /api/roles/:id/audit (admin-only, ?limit=1..50, default 10) returns recent entries newest-first with computed added/removedPermissionIds and joined actor info. - New tests in tests/role-permission-audit.test.mjs cover write, every-call write (incl. no-op), GET ordering+diff+actor, no-op diff reporting, 404, and limit. Drive-by fixes: - Fixed pre-existing syntax corruption in tests/apps-open.test.mjs (stray `ccaPassa,` line from a prior bad merge that prevented the whole api-server test workflow from running). - Made tests/roles-crud.test.mjs "rejects an invalid name" robust to parallel test execution by scoping the leak check to the bad name instead of relying on a global row count. API spec / codegen (lib/api-spec, lib/api-client-react): - Added `getRolePermissionAudit` operation and `RolePermissionAuditEntry` schema; ran orval codegen. Frontend (artifacts/tx-os): - New RolePermissionHistory component renders inside the role edit dialog (between permissions and Save/Cancel) using useGetRolePermissionAudit. Shows timestamp, actor, added/removed permission names (resolved via permissionsById), and total count. - Save invalidates the audit query so the new entry appears immediately on the next open. - Bilingual i18n strings (en + ar with full Arabic plural variants: zero/one/two/few/many/other) under admin.roles.history*. Verification: - tx-os typecheck passes. - All 5 new audit tests + 13 existing roles tests pass. - E2E (testing skill) verified the full flow: empty state on a fresh role, save adds a permission, reopened dialog shows the new entry with actor name in Arabic. Docs: - replit.md updated to list `role_permission_audit` in Database Tables. Follow-ups proposed: #146 paginate/filter history, #147 audit other admin permission changes, #148 fix drizzle push duplicate. Replit-Task-Id: 9b8886a2-6e76-4072-b345-a772421fa161 |
||
|
|
3aa6e81ed7 |
Show users/groups impact before removing role permissions (Task #99)
Adds a per-permission impact preview to the role editor so admins can see
who would lose each permission before saving — and a confirmation step
when the change would actually revoke access from at least one user.
API
- New endpoint POST /api/roles/:id/permissions/impact-preview
- Body: { permissionIds: number[] } (the proposed kept set)
- Response: { removed: [{ permissionId, permissionName, userCount,
groupCount, groups: [{id,name}] }], totalAffectedUsers }
- A user is counted as "affected" only when no other role they hold
(direct or via groups) still grants the removed permission.
- Implemented in artifacts/api-server/src/routes/roles.ts using two
drizzle selectDistinct queries (direct + via groups) merged in JS.
Switched away from a sql`${arr}::int[]` approach that failed because
drizzle expands JS arrays as a parameter list, not as a single array
parameter. Now uses inArray().
OpenAPI / client
- Added schemas RolePermissionsImpactBody, RolePermissionImpactGroup,
RolePermissionImpactItem, RolePermissionsImpact in
lib/api-spec/openapi.yaml.
- Regenerated lib/api-client-react/src/generated/* (new
usePreviewRolePermissionsImpact hook).
UI
- artifacts/tx-os/src/pages/admin.tsx RolesPanel:
- Debounced (350ms) on-demand fetch only when at least one permission
has been unchecked relative to the saved state.
- Inline amber summary panel listing each removed permission with
user/group counts, plus a total-affected-users line.
- Confirmation modal before save when totalAffectedUsers > 0.
- Added EN/AR translations (impactTitle, impactPerPermission,
impactViaGroups, impactTotal, confirmRemoval*) using i18next
_one/_other plural keys to match existing pluralized strings.
Tests
- New artifacts/api-server/tests/role-permissions-impact.test.mjs
(6 tests, all green): empty removals, group-derived impact,
"covered by another role" exclusion, 404 on unknown role, 400 on
invalid body, 403 for non-admin.
- Existing roles-crud tests still pass.
- E2E flow verified end to end via the testing tool: amber summary
appears, confirmation dialog gates the destructive save, and the
resulting permission set persists.
Code-review follow-ups (applied in this same task)
- Tightened the OpenAPI 404 description for the new endpoint to clarify
that unknown permission IDs in the body are tolerated (only a missing
role yields 404). Regenerated the API client.
- Added a request-sequencing guard in the role editor so a stale
in-flight impact-preview response cannot overwrite a newer selection.
- When the impact preview fails while removals exist, the inline panel
now shows an explicit error message and the Save button is disabled
until the user resolves it (e.g. by changing the selection so the
preview can re-run successfully). EN/AR translations added under
admin.roles.impactError.
Notes
- The pre-existing test workflow failure (ECONNREFUSED on 127.0.0.1:8080
before the API server is ready) is unrelated and already tracked.
- replit.md not updated — change is endpoint-level and does not alter
documented architecture.
- A modification to artifacts/tx-os/public/opengraph.jpg may show up in
the diff; it is a build artifact regenerated by the vite dev server
on restart and is not part of this change.
Replit-Task-Id: 30b7a2f7-7b60-429a-89d4-79280a81803f
|
||
|
|
59c7338d63 |
Task #98: Warn admins before renaming a role used in code references
- Added GET /api/roles/:id/usage endpoint returning {userCount, groupCount},
guarded by requireAdmin and validating the role id.
- Updated PATCH /api/roles/:id audit metadata to record renamed=true,
renamedFrom, and renamedTo when the role name changes (in addition to
existing name fields), satisfying the traceability requirement.
- Added RoleUsage schema + /roles/{id}/usage operation to openapi.yaml,
regenerated lib/api-zod via orval. Codegen script in lib/api-spec was
updated intentionally to preserve `export * from './manual'` in
api-zod's index, which orval was overwriting.
- RolesPanel edit dialog (admin.tsx) now tracks the original role name
on open and shows an amber warning banner with from/to text plus a
usage line (users + groups counts) whenever the trimmed name input
differs from the original. Warning is suppressed for system or
protected roles since their name input is disabled.
- Added en/ar locale strings: renameWarningTitle, renameWarningBody,
renameUsage.
- Reverted accidental opengraph.jpg change picked up earlier in the
session — it is unrelated to this task.
Verification: e2e test created a non-system role, assigned the admin
user to it, opened the edit dialog, observed the warning + usage line,
saved the rename, and confirmed both the role name persisted and the
audit log captured renamed/renamedFrom/renamedTo.
Note: The pnpm `test` workflow shows pre-existing failures in
executive-meetings.ts that are unrelated to this task (verified via
git stash before changes).
Replit-Task-Id: c1ee048d-50c6-4439-a430-ee0b7ec09959
|
||
|
|
896f690ac0 | Git commit prior to merge | ||
|
|
c0cf2115dd |
Task #96: Show dependency counts in admin app/service/user lists
Goal: make the admin delete dialog show its dependency warning on
the FIRST click (matching the existing Groups UX), instead of
needing a 409 round-trip from the DELETE endpoint to populate the
warning.
Changes:
- lib/api-spec/openapi.yaml: added optional count fields to App
(groupCount, restrictionCount, openCount), Service (orderCount),
and UserProfile (noteCount, orderCount, conversationCount,
messageCount).
- artifacts/api-server/src/routes/apps.ts: GET /admin/apps now
batch-loads groupCount/restrictionCount/openCount via grouped
COUNT queries on group_apps, app_permissions, and app_opens.
- artifacts/api-server/src/routes/services.ts: GET /services now
batch-loads orderCount from service_orders.
- artifacts/api-server/src/routes/users.ts: GET /users now batch-
loads noteCount/orderCount/conversationCount/messageCount from
notes, service_orders, conversations.created_by, and
messages.sender_id.
- artifacts/tx-os/src/pages/admin.tsx: Apps, Services, and Users
delete buttons pre-populate their conflict-state from the row's
count fields so the DeletionWarningDialog shows the warning on
the first click. The lazy 409 fallback still works as a safety
net for any caller without counts.
- replit.md: documented the new admin-panel delete UX.
Tests:
- Added artifacts/api-server/tests/list-dependency-counts.test.mjs
with 6 tests verifying each list endpoint exposes the new count
fields with the expected non-zero values when dependents exist
AND zero values when they do not (apps, services, users).
- Existing artifacts/api-server/tests/delete-force-warnings.test.mjs
(9 tests) still passes — DELETE behavior unchanged.
- Verified end-to-end with a Playwright browser test: clicking the
service delete icon ONCE opens the confirmation modal with the
dependency warning ("orders", count >= 1) immediately.
Notes / drift:
- Count fields were added to the shared App/Service/UserProfile
schemas (not list-only sub-schemas) so the same shape is returned
from list and detail endpoints. Code review flagged this as
acceptable but broader than strictly necessary; left as is to
keep the API consistent.
Out of scope (already pre-existing on main, tracked as follow-ups):
- artifacts/api-server/tests/apps-open.test.mjs has a syntax error.
- artifacts/api-server/tests/app-permissions-unique.test.mjs has
two failing assertions about the composite primary key.
- artifacts/api-server/src/routes/executive-meetings.ts has 3 pre-
existing typecheck errors around the font-settings scope column.
- The `test` workflow exits with ECONNREFUSED when the api-server
isn't already up — needs a readiness check.
Replit-Task-Id: bd14fe73-9961-431b-ab5a-ab70f116e8c7
|
||
|
|
25a3ef21d2 |
Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule: 1. Word-like inline editing of meeting title and attendee names with bold, italic, underline, color, font-family, font-size, and text-align controls (Tiptap-backed EditableCell + FormattingToolbar). 2. Drag-to-reorder column headers (dnd-kit), persisted in the existing em-schedule-cols-v1 storage key. Old up/down buttons removed. 3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder endpoint permutes daily_number + start/end times in a single transaction using a two-phase negative→final assignment that respects the (date, daily_number) UNIQUE constraint and survives partial-day reorders. Audit-logged and role-guarded. 4. Highlight the meeting whose start/end window is "now", refreshed every 60 s. Toggle and color picker live in the customize popover and persist to localStorage (em-current-meeting-highlight-v1). Backend changes - Widened titleAr / titleEn / attendees.name to text. - New artifacts/api-server/src/lib/sanitize.ts with an allowlist for inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees, duplicate, reorder, and applyApprovedRequest (add_attendee) paths so rich text round-trips safely. - Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn; applyApprovedRequest add_attendee sanitizes the inserted name; reorder transaction return value cleaned (`byId` removed). - 5 new tests cover sanitization stripping, reorder happy path, cross-day rejection, permission denial, and applyApprovedRequest sanitization. Pre-existing app_permissions failures are unrelated (tracked by #126/#127). Frontend changes - artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar (custom FontSize Tiptap extension + @tiptap/extension-text-align). - ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation (optimistic with revert on error), highlight tick, and HighlightPrefs. - New SortableHeader component for column drag. - AttendeesCell passes the original attendee index to PUT writes so edits reach the right row even after grouping into virtual/internal/external. - Print page renders sanitized rich text via dangerouslySetInnerHTML on names and titles; attendee.title is rendered as a plain text node (XSS). - Translation keys added in ar.json and en.json. |
||
|
|
93c77f587c |
Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule: 1. Word-like inline editing of meeting title and attendee names with bold, italic, underline, color, font-family, font-size, and text-align controls (Tiptap-backed EditableCell + FormattingToolbar). 2. Drag-to-reorder column headers (dnd-kit), persisted in the existing em-schedule-cols-v1 storage key. Old up/down buttons removed. 3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder endpoint permutes daily_number + start/end times in a single transaction using a two-phase negative→final assignment that respects the (date, daily_number) UNIQUE constraint and survives partial-day reorders. Audit-logged and role-guarded. 4. Highlight the meeting whose start/end window is "now", refreshed every 60 s. Toggle and color picker live in the customize popover and persist to localStorage (em-current-meeting-highlight-v1). Backend changes - Widened titleAr / titleEn / attendees.name to text. - New artifacts/api-server/src/lib/sanitize.ts with an allowlist for inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees, duplicate, reorder, and applyApprovedRequest (add_attendee) paths so rich text round-trips safely. - Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn; applyApprovedRequest add_attendee sanitizes the inserted name; reorder transaction return value cleaned (`byId` removed). - 5 new tests cover sanitization stripping, reorder happy path, cross-day rejection, permission denial, and applyApprovedRequest sanitization. Pre-existing app_permissions failures are unrelated (tracked by #126/#127). Frontend changes - artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar (custom FontSize Tiptap extension + @tiptap/extension-text-align). - ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation (optimistic with revert on error), highlight tick, and HighlightPrefs. - New SortableHeader component for column drag. - AttendeesCell passes the original attendee index to PUT writes so edits reach the right row even after grouping into virtual/internal/external. - Print page renders sanitized rich text via dangerouslySetInnerHTML on names and titles; attendee.title is rendered as a plain text node (XSS). - Translation keys added in ar.json and en.json. |
||
|
|
5114b207da |
Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule: 1. Word-like inline editing of meeting title and attendee names with bold, italic, underline, color, and font controls (Tiptap-backed EditableCell). 2. Drag-to-reorder column headers (dnd-kit), persisted in the existing em-schedule-cols-v1 storage key. Old up/down buttons removed. 3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder endpoint swaps daily_number in a single transaction using a two-phase negative→final assignment that respects the (date, daily_number) UNIQUE constraint. Audit-logged and role-guarded. 4. Highlight the meeting whose start/end window is "now", refreshed every 60 s. Toggle and color picker live in the customize popover and persist to localStorage. Backend changes - Widened titleAr / titleEn / attendees.name to text (applied via direct ALTER TABLE because db:push --force is currently blocked by Task #126). - New artifacts/api-server/src/lib/sanitize.ts with an allowlist for inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees, duplicate, and reorder paths so rich text round-trips safely. - Code-review fix: duplicate handler now re-sanitizes titleAr/titleEn. - Code-review fix: print page no longer interpolates the unsanitized attendee.title into dangerouslySetInnerHTML. - 4 new tests cover sanitization stripping, reorder happy path, cross-day rejection, and permission denial. Pre-existing app_permissions failures are unrelated and tracked by Task #126. Frontend changes - artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar. - ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation (optimistic with revert on error), highlight tick, and HighlightPrefs. - New SortableHeader component for column drag. - AttendeesCell now passes the original attendee index to PUT writes so edits reach the right row even after grouping into virtual/internal/ external sections. - Print page renders sanitized HTML via dangerouslySetInnerHTML on names and titles (titles still rendered as plain text on the print page). - Translation keys added in ar.json and en.json. Drift - Sanitization for attendee.title was identified by the architect review as a defense-in-depth gap and proposed as Task #130 rather than fixing inline; the print-page XSS path that depended on it was fixed directly. |
||
|
|
be4ecb30bb |
Task #95: Let admins export the audit log as CSV
What was done - Added a new admin-only endpoint GET /api/admin/audit-logs/export that streams the filtered audit log as a CSV download. Honors the same `action`, `from`, and `to` query parameters as the list endpoint, validated through a shared parseFilters/buildWhere helper extracted from the existing handler. - Columns: action, actor_username, target_type, target_id, created_at, metadata (compact JSON). Rows ordered newest-first and capped at 50,000 to bound response size. UTF-8 BOM prepended so spreadsheet apps (incl. Excel) detect encoding correctly for Arabic content stored in metadata. - Response sets Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename="audit-log-YYYY-MM-DD.csv", Cache-Control: no-store, and is written via res.write streaming. - Updated lib/api-spec/openapi.yaml with operationId exportAuditLogsCsv and ran codegen to regenerate the React client + Zod schemas. - Frontend (artifacts/tx-os/src/pages/admin.tsx → AuditLogPanel): added an "Export CSV" button next to the existing filter actions. Clicking it fetches the export URL with current filters using same-origin cookies, triggers a browser download, and surfaces a localized error if the request fails. The button is disabled while filters are invalid or while a download is in flight, and a spinner replaces the icon during export. - Added bilingual translation keys admin.audit.export.button / admin.audit.export.failed in en.json and ar.json. Security hardening (post code-review) - csvEscape now prefixes a single quote when a cell value begins with one of =, +, -, @, tab, or CR, mitigating CSV/spreadsheet formula injection (Excel, LibreOffice, Google Sheets evaluate such cells as formulas). Verified end-to-end by inserting a synthetic audit row whose action started with "=" and confirming the export wrote it as "'=...". Verification - Typecheck of tx-os passes; api-server has only pre-existing executive-meetings errors unrelated to this change. - Manual curl smoke test: 200 with correct headers, BOM present, action and date filters honored, invalid date returns 400. - E2e UI test (Playwright via testing skill): logged in as admin, opened the Audit Log section, downloaded the unfiltered CSV (correct filename and header row), then re-exported with action=app.delete and confirmed only matching rows came back. - Targeted security check confirmed CSV injection mitigation (see above). Notes / drift - None for the spec. The generated `exportAuditLogsCsv()` helper in lib/api-client-react is typed Promise<Blob> but, because customFetch auto-infers text/csv as text, would currently return text if anyone called it directly. The Audit Log UI uses a raw fetch with credentials: "include" to download the blob, so this does not affect the feature. Updating the generated client's responseType handling is project-wide config and is intentionally out of scope here. Follow-ups proposed - #123 Add automated tests for the audit log CSV export (test_gaps) - #124 Let admins pick which columns to include when exporting the audit log (next_steps) Replit-Task-Id: 88a50100-caa7-4b37-b9da-cfdc42bee119 |
||
|
|
bdc19d5012 |
Prevent duplicate app permission entries from being saved
Original task (#91): The `app_permissions` join table had no unique constraint, allowing the same `(app_id, permission_id)` pair to be inserted repeatedly. The Admin Panel restriction had grown to 24 duplicate rows. This change prevents that recurring at the DB level and verifies that the existing conflict-safe insert path keeps working. Schema change: - `lib/db/src/schema/apps.ts`: added a composite primary key on `(app_id, permission_id)` for `appPermissionsTable`, matching the pattern used by other join tables in this repo (rolePermissions, userRoles, groupApps, etc.). This is enforced at the database level. DB migration: - Cleaned up the 23 duplicate rows still present in the DB before applying the constraint (kept the earliest row per pair using `ctid`), then ran `pnpm --filter @workspace/db run push` to sync the schema. Verified the new primary key `app_permissions_app_id_permission_id_pk` rejects duplicate inserts. Graceful handling of duplicates: - The seed script (`scripts/src/seed.ts`) already uses `.onConflictDoNothing()` when inserting into `app_permissions`. With the new primary key, that call is now properly idempotent (no longer silently allowing duplicates). - There is currently no HTTP route or admin UI that POSTs into `app_permissions` (the task description listed `routes/apps.ts` as a relevant file, but no such handler exists today). The constraint itself is what prevents future regressions, and any future endpoint should follow the seed's `.onConflictDoNothing()` pattern. Tests: - Added `artifacts/api-server/tests/app-permissions-unique.test.mjs` with two cases that prove the new behavior: 1. A plain duplicate INSERT fails with SQLSTATE 23505 (unique violation) and only one row remains. 2. `INSERT ... ON CONFLICT DO NOTHING` (the pattern Drizzle's `.onConflictDoNothing()` emits) handles duplicates gracefully: no error thrown, `rowCount` is 0, and exactly one row remains. - All previously-passing api-server tests for apps/groups still pass after the schema change. Replit-Task-Id: 0589a4dc-5898-4c66-8feb-3cd48289fe89 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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.
|
||
|
|
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) |
||
|
|
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. |
||
|
|
7ebf59d4b7 |
Task #64: Service Orders — receiver page + admin role toggle
Backend (artifacts/api-server)
- Add admin-only role-toggle endpoints:
- POST /users/:id/roles { roleName } — idempotent, returns UserProfile
- DELETE /users/:id/roles/:roleName — idempotent, returns UserProfile
- Allow assigned receiver to cancel their own claimed order
(received/preparing) in PATCH /orders/:id/status. Owner & admin
rules unchanged.
- New backend test cases: receiver can cancel own assigned order;
another receiver gets 403.
API spec / codegen
- Add AddUserRoleBody schema and the two new role endpoints
under a new "roles" tag in lib/api-spec/openapi.yaml.
- Regenerated api-zod and api-client-react.
Frontend (artifacts/teaboy-os)
- New page src/pages/orders-incoming.tsx at /orders/incoming:
RBAC-gated (admin || order_receiver), shows "My active orders"
and "Awaiting receiver" sections, with claim, mark preparing,
mark completed and cancel buttons. Handles 409 already_claimed.
- Add Inbox button to home top bar, conditional on the same roles.
- Admin users table now has an "Order Receiver" Switch wired to
the new role-toggle hooks.
- Extend use-notifications-socket to invalidate the incoming-orders
query on order_incoming_changed and on notification_created with
type === "order".
- Bilingual locale keys (ar/en) for the new page and admin label.
Tests
- All 25 api-server tests pass (24 existing + 1 new receiver-cancel
case). All 3 teaboy-os e2e tests pass.
Follow-up filed: #67 (notify requester when receiver cancels).
|
||
|
|
a8b30baaac |
Task #62: Service Orders backend foundation (corrected)
After prior code review rejection, refactored to match spec exactly: - Permission renamed orders:receive → orders.receive (dot form), seeded with order_receiver role - service_orders table: user_id, service_id, notes, status (pending/received/preparing/completed/cancelled with CHECK), assigned_to, created_at, updated_at - /confirm-receipt is now the receiver atomic claim (UPDATE WHERE pending+unassigned), 409 already_claimed on miss - /status accepts preparing|completed|cancelled with permission matrix: * preparing/completed: assigned receiver or admin * cancelled: owner (pending|received) OR admin (any non-cancelled status) - requirePermission middleware no longer auto-bypasses admin; admin gets the permission via explicit seed grant - Notifications use type='order', relatedType='order' - Realtime emits notification_created (per receiver) + order_incoming_changed (broadcast) + order_updated (owner) - OpenAPI Order schema rewritten (no quantity, no per-status timestamps), endpoint summaries updated, codegen run - Tests cover: client place+list, non-receiver 403, no-role 403, parallel claim race (200/409), status matrix, owner-cancel rules, admin-cancel-completed, descriptions excluded from service summary All 24 api-server tests pass. Ready for code review re-check. |
||
|
|
2602edaca0 |
Task #62: Service Orders backend foundation (corrected)
After prior code review rejection, refactored to match spec exactly: - Permission renamed orders:receive → orders.receive (dot form), seeded with order_receiver role - service_orders table: user_id, service_id, notes, status (pending/received/preparing/completed/cancelled with CHECK), assigned_to, created_at, updated_at - /confirm-receipt is now the receiver atomic claim (UPDATE WHERE pending+unassigned), 409 already_claimed on miss - /status accepts preparing|completed|cancelled with permission matrix: * preparing/completed: assigned receiver or admin * cancelled: owner (pending|received) OR admin (any non-cancelled status) - requirePermission middleware no longer auto-bypasses admin; admin gets the permission via explicit seed grant - Notifications use type='order', relatedType='order' - Realtime emits notification_created (per receiver) + order_incoming_changed (broadcast) + order_updated (owner) - OpenAPI Order schema rewritten (no quantity, no per-status timestamps), endpoint summaries updated, codegen run - Tests cover: client place+list, non-receiver 403, no-role 403, parallel claim race (200/409), status matrix, owner-cancel rules, admin-cancel-completed, descriptions excluded from service summary All 24 api-server tests pass. Ready for code review re-check. |
||
|
|
cdf5bf4d33 |
Service Orders — backend foundation (Task #62)
- New `service_orders` table (status pending|claimed|delivered|received|cancelled) - New `orders:receive` permission + `order_receiver` role; admins implicitly allowed - Added `requirePermission(name)` and `userHasPermission(userId, name)` middleware helpers - New routes: - POST /api/orders place order (authenticated) - GET /api/orders/my list current user's orders - GET /api/orders/incoming receivers see pending+active visible orders - PATCH /api/orders/:id/status claim (atomic), deliver, cancel - PATCH /api/orders/:id/confirm-receipt requester confirms a delivered order - Atomic claim via UPDATE ... WHERE status='pending' AND assigned_to IS NULL (returns 409 already_claimed on race) - Realtime: emits `notification_created` to receivers/requester, `order_incoming_changed` to all receivers, `order_updated` to requester - Service shape in order responses limited to id/nameAr/nameEn/imageUrl (no description fields), per spec - OpenAPI updated with new paths and schemas; codegen run - Seed updated idempotently (permission, role, role_permissions) - New tests in artifacts/api-server/tests/service-orders.test.mjs (full lifecycle, atomic claim race, unauth rejection) — all 21 api tests pass No deviations from the planned scope. Tasks #63 (client UI) and #64 (receiver page + admin role toggle) remain blocked-by #62 and are next. |
||
|
|
216ff65e04 |
Task #48: Let admins page through more than the last 100 opens
Added offset/limit pagination to the two admin app-opens drill-in endpoints so admins can investigate spikes that span more than the default 100 most-recent opens. Backend (artifacts/api-server/src/routes/stats.ts): - New parsePaging() helper validates `limit` (1..200, default 100) and `offset` (>=0, default 0); invalid values return 400. - Both `/stats/admin/app-opens/by-app/:appId` and `/stats/admin/app-opens/by-user/:userId` accept the new params, apply `.limit(limit).offset(offset)`, and return `limit`, `offset`, and a `nextOffset` (number | null) computed from `totalCount`. - Added a stable secondary sort (`id desc`) so paged results don't shuffle when timestamps tie. Spec & client (lib/api-spec/openapi.yaml + regenerated clients): - Added `limit`/`offset` query params and `limit`/`offset`/`nextOffset` response fields to AdminAppOpensByApp/AdminAppOpensByUser. - Re-ran `pnpm --filter @workspace/api-spec run codegen`. Frontend (artifacts/teaboy-os/src/pages/admin.tsx + locales): - AppOpensDrillIn / UserOpensDrillIn now accumulate extra pages in local state and expose a "Load more" button via a shared LoadMoreSection footer that also shows "Showing X of Y". - Extra pages are fetched via the generated `getAdminAppOpensByApp` / `getAdminAppOpensByUser` functions; accumulated state resets when the appId/userId or stats query params change. - Added en/ar translations for `loadMore`, `loadMoreError`, `shownOf`. Tests: - New artifacts/api-server/tests/admin-app-opens-pagination.test.mjs covers happy-path paging for both endpoints, the default page size, and 400 responses for invalid limit/offset. - All 10 api-server tests pass; full workspace typecheck passes. Replit-Task-Id: b6382efe-765f-4689-8c93-196fee253f63 |
||
|
|
0b7593bbc2 |
Let leaving admins choose who takes over the group (task #46)
Original ask: when the only admin leaves a group, let them pick a
specific successor instead of always auto-promoting the longest-tenured
member. Keep "auto" available, with bilingual labels.
Changes:
- lib/api-spec/openapi.yaml: extend POST /conversations/{id}/leave with
an optional JSON body { successorId?: number | null }.
- Regenerated lib/api-client-react and lib/api-zod via api-spec codegen.
- artifacts/api-server/src/routes/conversations.ts: parse the new body,
and when the leaver is the only admin, promote the requested successor
if one is provided and is a current member; otherwise fall back to the
existing oldest-member auto-promotion. Returns 400 if successorId is
not a remaining member.
- artifacts/teaboy-os/src/pages/chat.tsx:
- Track a successorChoice state ("auto" | userId).
- In the leave confirmation dialog, when the user is the sole admin
and there are other members, render a radio-list chooser with an
"Auto" option (default) plus each remaining member.
- Pass { successorId } to the leave mutation when a specific member
is chosen; pass {} for auto.
- Reset choice on success.
- artifacts/teaboy-os/src/locales/{en,ar}.json: added successorTitle,
successorHelp, successorAuto strings.
Verification:
- pnpm -w run typecheck passes for libs and all artifacts.
- Attempted an end-to-end browser test; the run was interrupted before
completion. Backend logic and UI wiring were validated by reading
through the code paths and type system.
Replit-Task-Id: c9065bc1-ab4e-4a3b-a865-81754f5c2e5a
|
||
|
|
a864b84567 |
Add notes functionality to the application with CRUD operations and labeling
Integrates a new Notes feature, including backend API routes for notes and labels, database schema updates, frontend UI components for creating, viewing, editing, and deleting notes, and internationalization support for notes in both English and Arabic. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 4ab7f101-06a9-4dd7-94c8-617f1751327c Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fYGOSe0 Replit-Helium-Checkpoint-Created: true |
||
|
|
62b79c7e7d |
Task #42: Tell members when someone leaves a group on their own
Original task: Post a bilingual system message into the group chat when a member leaves via POST /api/conversations/:id/leave, with real-time delivery via Socket.IO and rendering in the chat thread. Findings: - The leave handler in artifacts/api-server/src/routes/conversations.ts already inserts a `member_left` system message (and `admin_promoted` when a successor is auto-promoted) and emits via Socket.IO using insertAndEmitSystemMessage. Direct conversations and last-leaver cleanup paths are correctly skipped. - artifacts/teaboy-os/src/pages/chat.tsx renderSystemMessage already handles `member_left`, `admin_promoted`, and `adminPromotedAfterLeave`. - Bilingual locale keys exist in src/locales/en.json and ar.json under chat.system.memberLeft / adminPromoted / adminPromotedAfterLeave. Change made: - Extended the MessageWithSender.kind enum in lib/api-spec/openapi.yaml to include `member_left` and `admin_promoted` (previously only user/group_renamed/members_added/member_removed were declared). - Re-ran `pnpm --filter @workspace/api-spec run codegen` to regenerate api-zod and api-client-react schemas so the spec, runtime validation, and client types stay in sync. - Verified `pnpm -w run typecheck` passes across all packages. No behavioural code changes were necessary; the feature was already implemented end-to-end and only the API spec enum needed to catch up. Replit-Task-Id: 40fccb3a-4fc0-4ad7-a6e9-d1ac67ef7769 |
||
|
|
b6a0dc6ab9 |
Add admin recent-opens drill-in popup for top apps and users
Task #41: Show a usage history popup when admins click a top apps row or a most-active users row on the admin dashboard. Changes: - API: extracted the existing range/from/to parser in artifacts/api-server/src/routes/stats.ts into a shared parseRangeWindow() helper so the same window logic powers the existing /stats/admin endpoint and two new ones. - API: added GET /stats/admin/app-opens/by-app/:appId returning the last 100 opens for an app inside the selected window, including the user (id, username, displayName, avatarUrl) and a totalCount. requireAuth + requireAdmin guarded. - API: added GET /stats/admin/app-opens/by-user/:userId with the same shape but per-app metadata for each open. - Spec: added matching paths and AdminAppOpensByApp / AdminAppOpensByUser schemas in lib/api-spec/openapi.yaml, reran orval codegen for api-client-react and api-zod. - UI: in artifacts/teaboy-os/src/pages/admin.tsx the Top apps and Most active users leaderboard rows now open a drill-in modal instead of immediately navigating away. The modal shows a scrollable timeline of recent opens (timestamp + user/app), respects the dashboard's current range selector (7d/30d/90d/custom), and exposes an "Edit app" / "View user" footer button that preserves the previous jump-to behaviour. Loading, error, and empty states are handled. - i18n: added admin.dashboard.drillIn keys in en.json and ar.json (titles, subtitle, empty/error, footer buttons). Verification: pnpm run typecheck passes. Follow-ups proposed: #47 (clickable rows inside the popup), Replit-Task-Id: f2fbe652-a788-4f25-9fb2-9ef2c535c8da #48 (paginate beyond 100 opens). |
||
|
|
b910733adc |
Prevent database synchronization issues by ignoring specific tables
Configure Drizzle to exclude the `user_sessions` table from schema introspection. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 4e1b5887-b92a-4851-b4ff-a7278a110e95 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/YPEna1J Replit-Helium-Checkpoint-Created: true |
||
|
|
db9cf7b315 |
Send system messages for group rename / add / remove
Original task (#38): When admins rename a group or add/remove members, post a system message into the chat thread so other members see who changed what and when, with bilingual (AR/EN) text and real-time delivery. Changes - lib/db/src/schema/conversations.ts: added `kind` (varchar default "user") and `meta` (jsonb) columns on the `messages` table. Pushed the new columns directly via ALTER TABLE since drizzle-kit push prompted on unrelated rename ambiguities. Also healed pre-existing schema drift on `users.clock_hour12`, `conversations.avatar_url`, and `conversation_participants.is_muted/is_archived` so the API could start. - lib/api-spec/openapi.yaml: extended MessageWithSender with `kind` (enum: user | group_renamed | members_added | member_removed) and optional `meta`. Re-ran codegen. - artifacts/api-server/src/routes/conversations.ts: added insertAndEmitSystemMessage + small user-display helpers; PATCH conversation now emits a `group_renamed` system message when a name actually changes; add-participants emits `members_added` with the actor + added users; remove-participant emits `member_removed` with actor + removed user. Each system message is broadcast over Socket.IO via the existing `new_message` channel so all current members receive it immediately. - artifacts/teaboy-os/src/pages/chat.tsx: render messages with `kind != "user"` as centered, muted pill bubbles (no avatar / sender label) using a new renderSystemMessage helper that picks the language-appropriate name out of meta. Conversation list preview also uses it so the last activity reads sensibly when the most recent message is a system message. - artifacts/teaboy-os/src/locales/{en,ar}.json: added chat.system.* strings (groupRenamed, membersAdded with plural variants, memberRemoved, someone, listSeparator). Verification - Typecheck (libs + artifacts) passes. - e2e via testing skill: registered fresh users, created a group, renamed it, added a member, removed a member; all three centered system messages appeared in the thread in order with the expected copy. Notes / deviations - Used the actor's userId as senderId for system messages (kept existing NOT NULL FK) instead of introducing a nullable sender, which keeps the migration lightweight. This means system messages count toward unread for non-actor members; flagged as a follow-up. Replit-Task-Id: 6dfa2b99-fbac-4146-b59b-8c04a14c9e96 |