56c8bcfb855d4eba6af3c63d4ade8b0c706cb171
172 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
56c8bcfb85 |
Add download for protocol visit photos (Task #660)
Users could upload photos into Protocol "تصوير" albums but had no way to
download them — clicking a photo only opened it inline. This adds per-photo
downloads and a whole-album ZIP download.
Backend:
- objectStorage.ts: new exported extensionForContentType() mapping content
types to file extensions (stored paths are extension-less UUIDs).
- storage.ts: opt-in ?download=1 mode on GET /storage/objects/*path sets
Content-Disposition: attachment (with optional ?name base + derived ext);
default inline behavior and per-entity authz unchanged.
- protocol.ts: new GET /protocol/photo-albums/:id/download streams all album
photos as a ZIP (guarded by requireProtocolAccess), named from the album;
unreadable objects are skipped rather than failing the archive. Empty album
returns 404.
- Added archiver@8 dependency (+@types/archiver dev).
Frontend (protocol.tsx):
- Per-photo download button (attachment, name = album-index) in album detail.
- "تحميل الكل" / "Download all" ZIP button in album header (hidden when empty).
- Added photoDownloadSrc/albumDownloadSrc helpers, Download icon.
- AR/EN locale strings protocol.photos.download / downloadAll.
Notes / deviations:
- archiver v8 is ESM-only with no callable default export; used
`new ZipArchive(...)` instead of `archiver("zip")`. Recorded in memory.
- Could not run authenticated e2e (seed admin password is a secret). Verified:
api-server typecheck + build pass, tx-os typecheck passes, server healthy,
new routes wired (401 auth-required not 404). Pre-existing typecheck errors
in executive-meetings.ts / push.ts are unrelated and untouched.
|
||
|
|
3f3c9671f5 |
Add functionality to manage visit photos and albums for protocols
Introduce new database tables for photo albums and photos, add API endpoints for CRUD operations on albums and photos, and implement frontend components for managing photo albums. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: ad3facca-9626-4e19-95c0-125e6d3bc7e2 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1OXOhLE Replit-Helium-Checkpoint-Created: true |
||
|
|
ea204894aa |
Update tab name for dedications in protocol app
Rename "Gifts & Shields" to "Dedications" in English and "الهدايا والدروع" to "الإهداءات" in Arabic within the protocol app's locale files. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 4fc9b8d2-3d7b-4f65-b980-61b4ffc1e22d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/bJodRQl Replit-Helium-Checkpoint-Created: true |
||
|
|
17950daa5e |
feat(protocol): shareable public booking link bar in Room Bookings
Task #655: staff need a one-click way to copy and share the public (no-login) room-booking link with guests. - Add a link bar at the top of the "حجوزات القاعات" (Room Bookings) section showing the absolute public URL, with a "نسخ الرابط" copy button that gives toast + a temporary "تم النسخ" confirmation state. - URL is built from window.location.origin + BASE_URL (trailing slash stripped) + "/protocol/request", so it works on root and subpath deploys and when pasted into a browser/WhatsApp. - Clipboard uses navigator.clipboard with a textarea+execCommand fallback; fallback now checks execCommand's return and reports failure (destructive toast) instead of falsely showing success. Textarea is removed in a finally block. Copied-state timer stored in a ref and cleared on repeat clicks to avoid a reset race. - URL text forced dir="ltr" inside the RTL layout; new i18n keys (shareLinkTitle, copyLink, linkCopied, linkCopyFailed) added under protocol.bookings in both ar.json and en.json. Verified: tsc typecheck passes; /protocol serves 200; module transforms cleanly via Vite; architect review issues (fallback false-success, timer race) fixed. |
||
|
|
2a194de12c |
Add attendee count field to booking requests and display it
Updates API schemas and database to include attendee count for bookings, modifies the public request form to accept this new optional field, and displays the attendee count on the booking details. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 9c655ed7-866c-454d-b646-e458df854c65 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/OsTuUKE Replit-Helium-Checkpoint-Created: true |
||
|
|
62c38a508f |
Add public interface for submitting room booking requests
Introduces a new public-facing API endpoint and UI for submitting room booking requests without authentication, including rate limiting and input validation. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 5352d582-4c15-426c-84ce-4ba3fa0d5cd8 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/OsTuUKE Replit-Helium-Checkpoint-Created: true |
||
|
|
5e460920c7 |
Task #649: complete Public Relations & Protocol module (fix 8 review gaps)
Addresses the 8 spec gaps that blocked prior completion, all additive and scoped to the protocol_* module / /protocol routes. executive-meetings untouched. Backend (api-server/routes/protocol.ts, lib/db schema, seed): - External meetings create as "pending" (status dropped from create schema); added approve + reject endpoints with state-conditional WHERE status='pending' and 409 on contention; patch status enum restricted to completed/cancelled. - Added approvedBy/approvedAt/rejectionReason columns + status index; default status now "pending". - Booking reject and gift-issue reject made state-conditional (409 on 0 rows). - Dashboard: roomsAvailableNow (NOT EXISTS), giftsIssuedThisMonth/Qty; upcomingExternal filtered to future pending|approved. - Reports: added externalMeetings summary counts by status. - Seed: room names corrected (AR + EN). Frontend (tx-os/pages/protocol.tsx, App.tsx, locales): - URL-synced tabs under /protocol/:tab (slug<->tab maps). - Dedicated Rooms tab (moved out of Gifts); RoomDialog isActive toggle. - External approve/reject buttons + rejectionReason display; ExternalDialog create omits status, edit limited to completed/cancelled. - Dashboard cards clickable to navigate; reports external section. - Bookings list/calendar (grouped-by-day) view toggle. - New i18n keys in ar.json + en.json. Verification: tx-os + protocol.ts typecheck clean; drizzle push + re-seed done; both workflows restarted; /api/protocol/me returns 401 unauth; architect re-review approved. Pre-existing typecheck errors in executive-meetings.ts and push.ts are unrelated and were not touched. Note: had to rebuild lib/db declarations (tsc --build --force) so api-server project references picked up the new schema columns. |
||
|
|
2322b77b8d |
Add Public Relations & Protocol module (Task #649)
New, fully separate app module "العلاقات العامة والمراسم / Public Relations and Protocol". Does not touch executive-meetings; all new tables are prefixed protocol_ and are additive-only, routes under /protocol. Includes: - 6 protocol_ tables (rooms, room bookings, external meetings, gifts, gift issues, audit log) in lib/db. - API routes + role-scoped middleware (protocol_super_admin, pr_manager, officer, requester, viewer) with requireProtocolAccess gating. - Room-booking conflict prevention (overlap rule newStart<existingEnd AND newEnd>existingStart vs pending/approved) with AR error message. - Gifts/shields (دروع) registry + issuance with stock tracking. - Bilingual AR/EN frontend page (tabs + dialogs) at /protocol, app tile, i18n keys, and seed data (roles, permission, default rooms). Concurrency/security hardening from code review: - All role guards now validate users.isActive (inactive-user bypass fix). - Per-room SELECT ... FOR UPDATE lock serializes booking check-then-write. - Approve paths re-read inside the transaction and use state-conditional updates (WHERE ... AND status='pending' RETURNING). - Gift issuance claims the issue conditionally, then does an atomic conditional stock decrement; failure rolls back the claim. Verified: drizzle push (6 tables), seed, frontend + api-server typecheck (only pre-existing errors in untouched files), unauth gating returns 401, conflict predicate confirmed. Architect review PASS. |
||
|
|
736785e3d3 |
#635: External meetings + auto-numbering
User asked for three things in the executive-meetings module:
1. Remove the "daily number" input from the add/edit dialog — numbering
is auto-assigned by the server already, so the field was just noise.
2. Add an "اجتماع خارجي" (external meeting) toggle. When ON, the row
is force-tinted red in the schedule grid.
3. External meetings must be excluded from the auto-postpone flow:
the alert "Postpone" button is disabled, and the cascade shift
skips them as followers.
Changes
- lib/db/src/schema/executive-meetings.ts
+ `is_external boolean not null default false` column. Pushed via
drizzle-kit (no destructive migration; defaults backfill).
- artifacts/api-server/src/routes/executive-meetings.ts
+ `isExternal` added to base zod fields, POST insert, PATCH update.
+ POST/PATCH force `rowColor='red'` when `isExternal=true`.
+ POST /:id/postpone-minutes rejects external meetings with
HTTP 409 + code `external_meeting_no_auto_postpone`.
+ `computeCascadeShift` filters out external followers (preview +
writer stay in lockstep, as the comment promises).
- artifacts/tx-os/src/pages/executive-meetings.tsx
+ Meeting type + MeetingFormState gain `isExternal`; dropped the
`dailyNumber` field from the form state entirely.
+ `emptyMeetingForm` / `openEdit` updated; ManageSection.save() and
the inline ScheduleSection save body now send `isExternal` and
never send `dailyNumber`.
+ MeetingFormDialog: removed the daily-number FormRow and added a
Switch-based external-meeting FormRow with localized hint.
+ `rowColors` memo coerces to "red" whenever `m.isExternal`.
+ Audit-diff "interesting" list now includes `isExternal`.
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
+ Meeting type gains optional `isExternal`.
+ "Postpone" button disabled + tooltip when meeting is external.
- locales: added `field.isExternal`, `field.isExternalHint`,
`field.isExternalOn/Off`, and `alert.externalCannotPostpone` in
both ar.json and en.json. Existing dailyNumber keys kept (still
referenced by the manage-table column header / cell display).
Post-review hardening (from architect pass)
- PATCH now coerces rowColor → 'red' for any PATCH that touches a row
that is (or becomes) external. Prevents PDF/export paths that read
raw rowColor from showing a non-red tint for externals.
- Schedule row quick-actions popover: the "Postpone" button is now
disabled with a localized tooltip when the meeting is external,
matching the upcoming-meeting alert behavior.
- PostponeDialog.handleErr maps the 409 code
`external_meeting_no_auto_postpone` to the localized
`alert.externalCannotPostpone` toast so users see a real reason
instead of the raw English error string.
Notes
- The `dailyNumber` column stays in DB (heavily used for slot
persistence and the unique index); only the form input was removed.
Server's `nextDailyNumber()` already auto-assigns when the field is
absent on POST, and PATCH leaves it untouched when absent.
- Two pre-existing TS errors remain in api-server (`isHighlighted`
type-inference quirk on `z.union(...).transform(...)` and a
font_settings comparison) — not in scope for this task.
- Tests not added; verification by typecheck on changed surfaces and
workflow-restart smoke. No e2e harness was wired.
|
||
|
|
376918f983 |
Drag-to-reorder service cards (iOS jiggle mode)
Long-press a service tile (~500 ms) → grid enters edit mode: every
card jiggles, a floating "تم / Done" pill appears, and cards become
draggable via @dnd-kit. Drop on another tile to reorder. Tap Done or
outside the grid to persist; the new order is shared globally via
servicesTable.sortOrder.
Backend
- POST /api/services/reorder (admin-gated, requireAdmin)
- Validates full-set match (no missing/dup/unknown IDs)
- Transactional sortOrder rewrite — no partial writes
- Route registered before /services/:id to avoid path-param collision
- OpenAPI op + ReorderServicesBody Zod schema → regenerated client
Frontend
- artifacts/tx-os/src/pages/services.tsx rewritten
- PointerSensor (distance 6) + TouchSensor (delay 150 ms) — long-press
and tap-to-open stay distinct
- touch-none only applied while in edit mode so normal scrolling is
unaffected outside it
- exitEditMode awaits mutation + invalidate before flipping editMode,
preventing snap-back from stale react-query cache
- exitingRef guard against duplicate exit calls (Done button +
outside-tap handler firing for the same gesture)
- Non-admins can enter edit mode (haptic affordance) but local reorder
is silently reverted on exit
- i18n: services.editMode.{done,hint,saveFailed} added to ar.json/en.json
Pre-existing unrelated typecheck errors in push.ts and
executive-meeting-font-settings.ts are not touched by this change.
|
||
|
|
0bcb2cd28f |
Improve audit log filtering with a searchable user list and better date formatting
Introduces a combobox for filtering audit logs by actor, replaces multiple date formatting calls with a dedicated helper function, and updates locale files for new filter UI elements. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 50672f01-2472-4869-a234-ecc811a41e0b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/eDmI6vt Replit-Helium-Checkpoint-Created: true |
||
|
|
d17587e096 |
Remove Notifications tab from Executive Meetings (Task #616)
The Notifications tab inside Executive Meetings only ever rendered a
log of `meeting_created` fan-out rows (the only event type currently
wired), was filtered to the selected schedule date, and overlapped
with the Audit log. For real users it was almost always empty and
just took space in the tab strip — see attached iPhone screenshot in
the conversation. User asked explicitly to delete it.
Changes:
1. `artifacts/tx-os/src/pages/executive-meetings.tsx`
- Removed the `{ key: "notifications", icon: Bell }` entry from
the `SECTIONS` array so the tab no longer appears in the strip.
- Removed the `case "notifications": return me.canRead;` branch
from `isSectionVisible`.
- Removed the `{section === "notifications" && …}` render branch
and the `<NotificationsSection …/>` JSX.
- Deleted the `NotificationsSection` component entirely (~90 lines)
and the unused `NotificationRow` type.
- Left the `Bell` import in place — still used by the bell button
elsewhere in the page (line ~8580).
2. `artifacts/tx-os/src/locales/{ar,en}.json`
- Removed the entire `executiveMeetings.notificationsPage` block
(headers, status labels, type labels, empty state). All keys
were verified to only be consumed by the just-deleted component.
- Left `executiveMeetings.notifications.*` alone — those are the
per-user preferences UI strings and are unrelated.
3. Backend left untouched on purpose:
- `GET /api/executive-meetings/notifications` still exists.
- `recordExecutiveMeetingNotifications` still runs on meeting
create and still writes to `executive_meeting_notifications`.
- The bell icon, push, and notification preferences UI are
unaffected because they read `notificationsTable`, not the
executive-meeting-specific table.
The orphaned `GET /executive-meetings/notifications` route now has
no frontend caller. I'm leaving it in this commit (it's harmless,
still permission-gated) and proposing a follow-up to remove it
cleanly instead of expanding scope here.
Code review: not run yet — will run after committing per the
standard flow.
|
||
|
|
3f6a0fb02f |
Task #613: replace per-app hide toggles with single Dock visibility switch
The per-app hidden feature shipped in #609 was rejected — user wanted one toggle that hides/shows the entire bottom AppDock bar, leaving Home apps untouched. Removed: - lib/db/src/schema/user-hidden-apps.ts (and index.ts export); table dropped via `pnpm --filter @workspace/db run push` - GET /me/apps and PUT /me/apps/:appId/hidden routes - getHiddenAppIdsForUser + getVisibleNonHiddenAppsForUser helpers - MyApp + UpdateMyAppHiddenBody OpenAPI schemas - MyAppsBody settings UI + related i18n keys - Regenerated api-client-react + api-zod from the trimmed spec - Reverted GET /apps back to getVisibleAppsForUser Added: - artifacts/tx-os/src/hooks/use-dock-visible.ts — localStorage-backed preference with a custom window event for in-tab sync and the native `storage` event for cross-tab sync. Default = true. - DockBody in settings-panel: single ToggleRow under a new "App dock" section ("settingsPanel.section.dock" / "settingsPanel.dock.show") - AppDock now returns null when the preference is off and clears `--app-dock-height` so page padding doesn't stay reserved. Code review: PASS (architect). No remaining references to the removed infra. Typecheck shows only pre-existing errors in executive-meetings.ts and push.ts unrelated to this change. Follow-up: publish to Gitea + redeploy on Mac (proposed as a follow-up task). |
||
|
|
e382df3d62 |
Add ability to show and hide applications in settings
Update settings panel and locales to include functionality for hiding and showing applications, adding new aria-labels for improved accessibility. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 463b7df5-a279-404c-b017-da0acab30371 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qxUqlQr Replit-Helium-Checkpoint-Created: true |
||
|
|
c31156be9f |
Task #609: Per-user app enable/disable in Settings
- New `user_hidden_apps` table (userId+appId composite PK, cascade) in
lib/db/src/schema/user-hidden-apps.ts; registered in schema index;
pushed to dev DB via drizzle-kit.
- Backend (artifacts/api-server/src/routes/apps.ts):
- GET /me/apps — returns every globally-active app visible to the
user with a `hidden` flag.
- PUT /me/apps/:appId/hidden — toggles a row in user_hidden_apps,
gated by getVisibleAppsForUser + isActive so a user can't toggle
apps they can't reach.
- GET /apps (Home/Dock) now uses getVisibleNonHiddenAppsForUser so
hidden apps disappear immediately.
- lib/appsVisibility.ts: added getHiddenAppIdsForUser and
getVisibleNonHiddenAppsForUser helpers.
- OpenAPI: added /me/apps + /me/apps/{appId}/hidden, MyApp and
UpdateMyAppHiddenBody schemas; regenerated api-zod + api-client-react.
- Frontend (settings-panel.tsx): added "My apps" accordion section as
first GroupItem with MyAppsBody — Switch per app, optimistic update,
invalidates getListMyAppsQueryKey + getListAppsQueryKey so Home/Dock
refresh without reload.
- Translations: added settingsPanel.section.myApps + settingsPanel.myApps
in ar.json + en.json.
- Code review fix: /me/apps and PUT gating filter isActive even for
admins, so inactive apps don't appear in the Settings list.
- Proposed follow-up #611 (Playwright test that hidden apps disappear
from Home and Dock).
|
||
|
|
e00e015b8b |
Improve how user roles are managed and displayed on the admin page
Introduce separation of direct and inherited roles in user profiles and API responses. Modify the admin UI to disable the "order receiver" toggle when a role is inherited, providing a clearer user experience. Update API endpoints and schemas to reflect these changes, alongside locale updates for translated strings. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 33aee19f-8f0f-4399-98e0-39fe09a87e1b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/m92e9kU Replit-Helium-Checkpoint-Created: true |
||
|
|
a96b2570e3 |
Task #601: iOS PWA push notifications enablement
PWA + Web Push infra was already fully built (SW, VAPID auto-gen,
subscribe/unsubscribe API, sendPushToUser called from orders/meetings/
notes/replies, iOS-PWA detection hook). The user wasn't getting
lock-screen alerts because iPhone/iPad weren't installed as a PWA —
iOS only delivers Web Push from a Home Screen icon, not a Safari tab.
This commit polishes the iOS install path so the gap is obvious:
1. push-enable-prompt.tsx — Added a dedicated iOS install-steps card
variant that renders on iPhone/iPad Safari when running outside
standalone mode. Shows Share → Add to Home Screen → open from icon.
Independent 14-day dismiss memory from the regular Enable card.
2. notification-settings.tsx — PushToggleRow now detects iOS-non-
standalone and shows the unsupported_ios_safari hint inline with a
disabled toggle, instead of teasing an Enable affordance that always
fails.
3. use-push-subscription.ts — Exported isIosSafariNonStandalone() so
both surfaces share the detection logic (was private).
4. lib/push.ts (server) — Added info logs in loadVapid() for both the
env-key and disk-key paths, removed an accidental duplicate disk-read
block introduced during editing. Now the production redeploy check
is a one-liner: grep for "VAPID keys loaded" in the api logs.
5. ar.json / en.json — Added notifSettings.push.iosInstall.{title,desc,
step1,step2,step3} bilingual strings for the new card.
No DB migrations. No deps changed. tx-os tsc passes; api-server tsc
errors are pre-existing (routes/push.ts handler type, font_settings)
and not touched by this change.
Smoke test on real iPhone/iPad still owed (out-of-band — task step #4
is a manual verification on the user's devices after redeploy).
|
||
|
|
475415b542 |
Update quick note button label to be shorter
Update the label for the quick note button in the executive meetings header from "Take a note..." to "Note" in English and "اكتب ملاحظة…" to "ملاحظة" in Arabic. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 1f9a6661-3f64-45ae-b766-13143669519d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/m92e9kU Replit-Helium-Checkpoint-Created: true |
||
|
|
bf81debebf |
Task #590: Daily meetings — clock, inline notes, fullscreen header
Three small UX fixes on /executive-meetings (schedule tab):
1) HeaderClock: dropped the digital "HH:MM:SS م" string next to the
weekday — the analog dial already conveys the live time, so it was
redundant. Bumped AnalogClock size from 36 to 44 so the dial reads
cleanly without the digital crutch. Weekday + date stay (calendar
info, not clock info). Removed the now-unused `useState`/`useEffect`
ticker inside HeaderClock — AnalogClock has its own.
2) "اكتب ملاحظة" yellow side tab no longer navigates away to /notes.
Added a transient `quickNoteOpen` local state in
ExecutiveMeetingsPageInner. The tab's onClick now toggles it on; a
new `InlineQuickNotePanel` is rendered above <ScheduleSection> with
an auto-focused textarea, Cancel/Send buttons, Cmd/Ctrl+Enter to
submit, Esc to close. Submit POSTs to /api/notes with the existing
contract ({ content, color: "yellow" }) and closes on success. The
side tab hides while the panel is open so they don't double up.
State auto-resets when the user leaves the schedule tab.
3) Fullscreen exit pill overlap: the floating "الخروج من ملء الشاشة"
pill (fixed top-3 end-3) was covering the schedule table's "الوقت"
column header. Added `pt-12 sm:pt-14` to the fullscreen branch of
<main> so the table starts below the pill on iPad landscape
(1024-wide). Non-fullscreen layout unchanged.
i18n: added executiveMeetings.quickNote.{placeholder,send} in EN+AR;
existing executiveMeetings.quickNote.label reused for the panel
header. All keys also have inline fallbacks so missing translations
don't break.
Drive-by: fixed a TypeScript narrowing error in admin.tsx left over
from the prior task's review-comment cleanup (data.startedAt is
already typed as string, so the typeof check produced `never` in the
else branch — replaced with a direct call).
Verified: pnpm --filter @workspace/tx-os exec tsc --noEmit passes
clean, Vite HMR updated cleanly in dev.
|
||
|
|
1659dc4b68 |
Task #589: Live build stamp + server start time in System Updates panel
Problem: After `git pull && docker compose up -d --build api` on the
Mac, the admin → System Updates panel kept showing the same version
(0.1.0-dev) and the same build (20260517.b5efd9eb) because both come
from version.json, a hand-edited file that nothing rewrites on every
build. No visible signal that a new image was actually running.
Two independent signals were added — neither needs editing version.json
by hand:
1) Auto-stamp version.json at Docker build time
- docker/api-server.Dockerfile: new ARGs GIT_SHA and BUILD_DATE.
A `node -e` step rewrites version.json's `build` field to
`${YYYYMMDDTHHmm}.${sha}` (e.g. 20260518T1042.a34eb5f5). When
the args are missing/"unknown", version.json is left untouched
so plain `docker build` and Replit `pnpm dev` still work.
- docker-compose.yml: api.build.args wires through GIT_SHA and
BUILD_DATE with `${GIT_SHA:-unknown}` fallbacks.
- scripts/redeploy.sh: exports GIT_SHA (git rev-parse --short HEAD)
and BUILD_DATE (date -u +%Y-%m-%dT%H:%M:%SZ) before `docker
compose build`, so the helper script just works.
2) startedAt timestamp from the API
- artifacts/api-server/src/routes/system.ts: SERVER_STARTED_AT
captured at module load (frozen for the process lifetime) and
added to every branch of GET /system/version (not-configured,
error, invalid-version, up-to-date, update-available, catch).
- lib/api-spec/openapi.yaml: added `startedAt: date-time` to
SystemVersionResponse (required). Regenerated api-client-react
and api-zod via `pnpm --filter @workspace/api-spec run codegen`.
3) Admin UI surfacing both signals
- artifacts/tx-os/src/pages/admin.tsx: new formatBuildStamp parses
`YYYYMMDD[THHmm].sha` and renders a localized date next to the
raw token. A new "Server started" line below the build number
formats the boot timestamp via the existing formatChecked helper.
- artifacts/tx-os/src/locales/{en,ar}.json: added
admin.systemUpdates.serverStartedAt ("Server started" /
"بدأ تشغيل الخادم").
4) Docs
- replit.md: documented how to verify a real redeploy via the
System Updates panel and the manual one-liner for `docker
compose build` without the helper script.
Verified: codegen succeeds, API workflow restarted clean, Vite served
fine. Pre-existing typecheck errors in push.ts and font-settings are
unrelated and untouched.
Mac next steps:
cd ~/Downloads/TX && git pull && ./scripts/redeploy.sh
Then open admin → System Updates: build line should show a new
`YYYYMMDDTHHmm.sha` stamp and "Server started" should reflect the
new boot time.
|
||
|
|
a487c1661d |
feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
|
||
|
|
f39d8edea2 |
feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
|
||
|
|
3d3a4ba972 |
notes: file-manager layout, folders section header, kill grid gap (#578)
Cleans up the Notes page after a user creates their first folder: 1. FoldersRail: in `rail` mode (every view except "All notes"), insert a small "My folders" / "مجلداتي" section header above the user's folder list so it no longer visually nests under the dark "Unfiled" pill. New i18n key `notes.folders.myFolders` in en.json and ar.json. 2. notes.tsx filteredNotes: when `folderSelection.kind === "all"`, hide notes whose `folderId !== null`. Combined with the existing FoldersBrowser cards this gives a file-manager feel — notes inside a folder are reachable by opening the folder card and no longer appear in the flat list, so users stop thinking move = copy. Counts on the "All notes" rail entry stay as the total (totalCount prop unchanged). 3. notes.tsx grid: replaced the two separate grid cells for composer/content with a single wrapper that uses `display: contents` on mobile (children flow into the outer grid honoring order-1/2/3) and `flex flex-col gap-4` on md+ (composer and content stack in the same cell, no empty grid-row gap when the FoldersRail is taller than the composer). Removed unused conditional row-start/row-span on the inner content div. Out of scope per plan: dnd-kit behavior, shared-with-me logic, composer or folder card redesign. Validation: `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean. Vite HMR successful after fix; no babel/JSX errors. |
||
|
|
aba984d238 |
Improve handling of notification permission prompts
Update notification permission logic to differentiate between denied and dismissed states, and adjust locale messages accordingly. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 02bc349c-c9f1-47f4-967c-cfefeabde8bb Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true |
||
|
|
558d395e0c |
#576: diagnose push enable + open full edit page from quick actions
Three connected fixes on the Executive Meetings schedule.
1) Push enable diagnostics (use-push-subscription.ts):
- `enable()` now returns { ok, reason } instead of a bare boolean.
- Reasons: unsupported, unsupported_ios_safari, permission_denied,
vapid_unavailable, subscribe_failed, server_rejected.
- Detects iPhone/iPad Safari not running as a PWA (checks
navigator.standalone + display-mode media query, with iPadOS
MacIntel + maxTouchPoints fallback) and short-circuits with
unsupported_ios_safari so the user gets a concrete
"open from Home Screen first" message instead of a silent fail.
- Wrapped VAPID fetch and pushManager.subscribe in their own
try/catches so each failure mode maps to its own reason.
2) Specific failure toasts (push-enable-prompt.tsx,
notification-settings.tsx):
- Both call sites now look up
`notifSettings.push.errors.<reason>` and fall back to the
legacy generic copy if the key is missing.
- Added enableSuccess toast on the toggle path too — previously
only the prompt-card path showed feedback on success.
- New AR + EN strings under notifSettings.push.errors.*.
3) Quick-actions row sheet (executive-meetings.tsx):
- Edit + Postpone buttons now sit SIDE BY SIDE on every viewport
(flex + flex-1 + min-w-0 instead of flex-wrap + min-w-[11rem]),
same height, equal width — fixes the stacked / over-sized Edit
button shown in the user's iPhone screenshot.
- "Edit" no longer opens the small Meeting edit Dialog. It now
flips the whole schedule into edit mode (same as the toolbar
pencil) and paints a transient accent ring on the originating
row (~1.6s) + smooth-scrolls it into view. Matches image 3 in
#576 where the user expects to see the full editable list.
- Added `highlightEdit` prop on MeetingRow that composes an
extra inset+outer ring on top of the existing quickOpen /
selection / current / press shadows, plus a soft background
tint, and clears automatically when ScheduleSection clears
highlightEditRowId.
Out of scope (left for follow-up tasks): server-side push delivery
(payloads/sounds), the edit-mode UI itself.
|
||
|
|
b5efd9eb12 |
Add system updates page to admin section
Add a new API endpoint and UI section for checking system updates, including internationalized locale strings and necessary dependency updates. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 1e6de894-7c78-4557-b01a-a2c1c01f4155 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true |
||
|
|
c40384b0d9 |
#570: stronger selected-row highlight + Edit button in Quick Actions
Task: on iPad the bulk-selected row was nearly invisible (only a 2px inset navy ring), and the row-tap Quick Actions dialog had only a Postpone button — user asked for a real visible selection fill and an Edit (pencil) action wired to the existing edit-meeting flow. Changes (artifacts/tx-os/src/pages/executive-meetings.tsx): - MeetingRow: add `selectionBg = rgba(11,30,63,0.18)` painted on the row's `<tr>` backgroundColor in addition to the existing `selectionShadow` 2px navy ring. Composition order in trStyle is `quickOpenBg ?? selectionBg ?? baseBg` so an open quick-actions surface still wins (transient action state), user-picked row colours still show when nothing is selected, and the navy tint reads clearly on iPad against the table grid. - MeetingRow: new optional `onQuickEdit?: () => void` prop. Quick Actions Dialog gains a pencil-icon "Edit" button before the existing Postpone button, wrapped in `flex flex-wrap gap-2` so the two buttons sit side-by-side on iPad/desktop and stack on narrow viewports. Button only renders when a handler is wired (defensive — `quickActionsCanMutate` already gates the surface). - ScheduleSection: host a local `editingMeeting` + `editingSaving` state plus a `MeetingFormDialog` mount, with a save handler whose body mirrors ManageSection's `save()` exactly (same PATCH payload projection, same `wrapAsParagraph` / attendee shaping, same toast/error translation). Edit button on a row opens this in-place dialog instead of switching the user to the Manage tab. - Locale: add `executiveMeetings.quickActions.edit` = "تعديل" / "Edit". Notes: - No new icon import needed — `Pencil` was already in the lucide import group. - Selection ring (`#0B1E3F`) and tint use the same navy so they read as one cohesive selected state, not competing layers. - Pre-existing `use-push-subscription.ts` TS noise unchanged. |
||
|
|
4b151c50a8 |
#568 silent delete for finished orders
User requested that deleting a finished order from My Orders happen
silently — one click on the trash icon, no confirmation dialog, no
success toast. Just click and the card disappears.
Changes:
- Removed the delete-confirm AlertDialog from OrderCard along with
its `confirmDeleteOpen` state. The trash icon now calls
`handleDelete` directly.
- Removed `toast({ title: t("myOrders.deleted") })` from
`scheduleDelete`. The 7-second soft-delete grace window stays in
place (pendingDeleteIds filter + setTimeout) so the API DELETE
still fires after the window. The `deleteFailed` error toast is
preserved so silent API failures don't lose orders.
- Dropped the now-unused locale keys from ar.json and en.json:
`deleteConfirmTitle`, `deleteConfirmBody`, `deleteConfirm`,
`deleted`.
- Removed the unused `AlertDialogDescription` import (the remaining
cancel-confirm dialog uses only the title).
Cancel-order flow remains untouched (still shows confirm dialog and
undo toast as in #566).
|
||
|
|
4c57c58f8c |
#566 clean up My Orders page (3 removals)
Per user request on the طلباتي page: 1. Removed the "Clear finished orders" bulk button entirely. The button was showing as a raw i18n key (myOrder c...) for the user and they asked for it gone. Dropped the related AlertDialog, confirmClearOpen state, handleClearFinished handler, and the locale keys clearFinished_*, clearConfirmTitle/Body_*, clearConfirm, clearedCount_*, and clearedPartial in both ar.json and en.json. 2. Removed the "تراجع / Undo" ToastAction shown after deleting an order. The toast now displays only the "Order deleted" title; the 7-second timer + pendingDeleteIds soft-delete window is preserved so the actual DELETE still fires after the grace period. The undo closure was deleted since nothing references it anymore. Single-id call site stays untouched (scheduleDelete still receives [id]). 3. Removed the "ستتاح لك ثوانٍ قليلة للتراجع" description from the cancel-order confirmation dialog. The cancelConfirmBody key was dropped from both locales. The AlertDialog now shows only the title + the two action buttons. Out of scope (kept as-is): - The undo ToastAction after CANCEL (user only asked about delete). - Incoming-orders page. Notes: - ToastAction import is still used by the cancel-undo path. - Trash2 import still used by the per-card delete button. - Pre-existing TS error in use-push-subscription.ts is unrelated. |
||
|
|
620ab284e2 |
fix(executive-meetings): quick-actions UX + reschedule/cancel polish (Task #563)
User-reported polish on the executive-meetings quick-actions flow on iPad/mobile. Six fixes, all client-side: 1. Quick-actions surface now opens from ANY click on a meeting row (was: only the attendees cell). Root cause: handleRowClick guard skipped any `em-edit-*`, `em-merge-edit-*`, and `em-time-*` descendants. Those affordances only do anything in edit mode, but handleRowClick already early-returns when edit mode is on, so the testid guard was always over-aggressive. Dropped those guards plus the `[role='button']` rule from the interactive selector, kept the real `button`/`input`/`textarea`/`a`/`menuitem`/`checkbox`/etc. guards and the `em-row-actions-*` / `em-row-select-*` testid guards (those affordances are visible outside edit mode and must not trigger the surface). File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4350). 2. Originating row is now unmistakable on iPad: quickOpenShadow is `inset 0 0 0 4px highlight, 0 0 0 2px highlight` (was a single 3px inset) and the row background tints to `rgba(highlight, 0.08)` while quick-actions is open. File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4287, L4344). 3. Postpone dialog header was overlapping the Radix close X in RTL. Added `pr-8` on the DialogHeader (same pattern already used for the row-quick mini-dialog). File: artifacts/tx-os/src/components/executive-meetings/ upcoming-meeting-alert.tsx (~L1447). 4. Reschedule tab: date / start / end inputs collided on iPad portrait. Grid is now `grid-cols-1 md:grid-cols-3` (single column through iPad portrait, three across on landscape/desktop), with `min-w-0` on each cell and `w-full` on every Input so Safari's native date/time pickers stop overflowing. Same file (~L1761). 5. Cancel tab X overlap is resolved by the same DialogHeader fix in step 3 (one Dialog wraps all three tabs). 6. Removed the `cancelHint` paragraph + the `cancelHint` translation keys in ar.json (L1252) and en.json (L1164) — fully unused now. NOT pushed to Gitea (Tailscale to desktop-11cj93j still flaky). Pending pushes: #560 (VAPID), #561 (CEO roles), #563 (this). |
||
|
|
8f3b750961 |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes. - **De-dup gate:** skips push when the user has any active Socket.IO connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a connected user only gets the in-app chime, never a duplicate system notification. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (shared device) so the previous user's notifications can't leak. - Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe` (auth-gated, Zod-validated). - Push hooked into 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick only (no asset caching). All URLs (icon, badge, navigation target) resolved against `self.registration.scope`, so the SW works at root or under a subpath without code changes. - SW registration in `main.tsx` scoped to `BASE_URL`. - `use-push-subscription` hook (enable/disable/refresh + status). - New `PushEnablePrompt` card mounted in `App` — appears on first launch when supported + permission still "default", one-tap enable, dismiss persists for 14 days. Silent on unsupported devices. - `PushToggleRow` added inside Notification Settings. - ar/en strings: `notifSettings.push.*` and `common.later`. Plumbing - OpenAPI: 3 new operations under `notifications`; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the key; subscribe endpoint 401s without auth. - New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing — covers VAPID endpoint, auth gating on subscribe/unsubscribe, row persistence + removal, idempotent re-subscribe with key rotation, account-switch endpoint reassignment, and malformed-input rejection. - Two architect rounds: first flagged race + payload size (fixed), second flagged dedup gate + first-launch UX + SW base-path + tests (all fixed in this commit). |
||
|
|
243ccb3e00 |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral in-memory. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes so over-sized notes don't kill delivery. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (account switch on shared device) so the previous user's notifications can't leak. - Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe` (auth-gated, Zod-validated). - Push hooked into the 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick handlers only (no asset caching). Focuses an existing tab or opens a new one at the payload URL. - SW registration in `main.tsx` scoped to `BASE_URL`. - `use-push-subscription` hook (enable/disable/refresh + status: unsupported / denied / default / subscribed). - New `PushToggleRow` in `NotificationSettingsContent` with ar/en strings (notifSettings.push.*). iPad copy explains that the user must add to Home Screen first for iOS to allow Web Push. Plumbing - OpenAPI: 3 new operations under `notifications` tag; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the generated key; subscribe endpoint 401s without auth as expected. - Architect review surfaced two HIGH issues (account-switch leak, payload size); both fixed before completion. |
||
|
|
04f2ab7ea9 |
Task #527: Unify topbar prefs into per-user Settings panel
- New `settings-panel.tsx` consolidates four duplicate topbar buttons
(clock-style picker, quick-mute, notification-settings popover,
language-toggle globe) behind a single gear icon that opens a
right-side Sheet (full-width on mobile, capped on desktop).
- Panel groups: Language (ar/en), Notifications (per-slot
enabled/sound/vibration), Sound (master mute), Clock (visibility,
12/24h, style).
- Reuses existing hooks (useUpdateLanguage, useUpdate{Clock*},
useUpdatePrefs, useHome/TopbarClockVisibility) so all persistence
paths (DB columns + localStorage) stay identical — no migration.
- home.tsx topbar now renders only inbox link, single notification
bell (kept as nav link), Settings gear, and logout.
- New i18n keys under `settingsPanel.*` in ar.json + en.json.
- Old ClockStylePicker / NotificationSettingsPicker / QuickMuteButton
exports remain in their original files (unused) — kept to minimize
diff scope; can be removed in a follow-up.
|
||
|
|
a794f92e61 |
Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports: - Custom image upload (or fall back to Lucide icon) via the existing ServiceImageUploader; rendered on the home launcher when set. - Open mode picker: internal (default), external_tab (window.open), external_iframe (renders inside /embedded/:id). External URL input shown conditionally and required by the form when an external mode is chosen. - Route field is locked (readOnly + lock hint) when editing a built-in app, since those slugs are hardcoded in the SPA router. Backend: - apps schema gains image_url, external_url, open_mode (default 'internal'); drizzle-kit push applied. - New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS + isBuiltinAppSlug, re-exported from lib/db. - PATCH /apps/:id rejects route changes whose previous slug is built-in with 400 + code='builtin_route_locked'. Same-route no-op is allowed; non-route updates on built-ins still work. Other: - New SPA route /embedded/:id and embedded-app page (iframe host with back + open-in-new-tab + error/not-embeddable states). - OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran. - en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*, builtinPathLocked, embeddedFrame.*. - New tests in apps-builtin-route-lock.test.mjs (4/4 pass) covering reject built-in route change, allow non-route built-in updates, allow non-builtin route changes, allow built-in same-route no-op. Notes / drift: - BUILTIN_APP_SLUGS is duplicated inline in admin.tsx (BUILTIN_APP_SLUGS_FE) because the browser bundle cannot import @workspace/db (pulls pg). Comment points at the canonical source; drift risk filed as a follow-up. - Pre-existing failures unrelated to this task: 3 tests in executive-meetings-* and tsc errors in api-server/src/routes/executive-meetings.ts. Out of scope. |
||
|
|
a55fb16c4c |
Task #515: Reply to a note from the popup without leaving the page
The floating "new note" popup's Reply button used to navigate to /notes?thread=X&reply=1, yanking the user out of whatever page they were on (Tasks, Calendar, etc.). Now Reply opens an inline composer inside the popup card itself. Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx): - New composerOpen / replyText / justSent state, reset whenever the popup head rotates so a half-typed reply never leaks across payloads. - handleReply now opens the inline composer and focuses the textarea (rAF) instead of routing. Acknowledge (mark-as-read) is deferred to successful send so Cancel keeps the note unread. - handleSubmitReply uses the existing useReplyToNote hook. Resolves recipientUserId from replyPayload.replier.id for the reply variant (owner replying back), undefined for the note variant (recipient replying — server infers the original sender). - On success: brief on-card "Reply sent" confirmation (aria-live), then auto-dismiss after 700ms. On failure: destructive toast with the textarea preserved. - Composer textarea: Cmd/Ctrl+Enter to send, plain Enter for newline, Esc to cancel (stops propagation so the global Esc listener doesn't dismiss the whole popup with an unsent reply). dir="auto" so the user's reply renders RTL/LTR per its own characters. - pointerdown/click stopPropagation on the composer wrapper so typing doesn't accidentally drag the card. Locale keys added to ar.json + en.json under notes.popup: inlineReplyPlaceholder, sendReply, cancelReply, replySending, replySent, replyFailed. Pre-existing failing tests (groups-crud, executive-meetings- notifications, executive-meetings-postpone-race) are unrelated to this change. |
||
|
|
0988585c65 |
Task #512: Per-recipient Delete in Notes Inbox
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.
Backend (artifacts/api-server/src/routes/notes.ts):
- DELETE /notes/received/:id — recipient-only; 404 if no row.
- POST /notes/received/bulk-delete — body {ids:number[]}, max 500,
single SQL DELETE, returns {ok, notFound} for partial-success UI.
- Both registered before DELETE /notes/:id so Express matches /received
first.
Frontend (artifacts/tx-os):
- New hooks useDeleteReceivedNote / useBulkDeleteReceivedNotes in
src/lib/notes-api.ts; both invalidate notes + folders queries.
- Inbox bulk bar gets a Delete button (rose) next to Archive plus a
confirm AlertDialog with all/none/partial toasts (src/pages/notes.tsx).
- ThreadDialog gets a per-row Delete next to Archive plus a single
confirm dialog that closes the thread on success.
- AR + EN locale strings added for all new copy.
Tests:
- artifacts/api-server/tests/notes-inbox-delete.test.mjs — recipient
delete, non-recipient/sender 404, bulk mix of valid+missing ids, and
DB-level checks that the note + other recipients survive.
- artifacts/tx-os/tests/notes-inbox-bulk-delete.spec.mjs — Playwright
flow seeds three received notes, bulk-deletes two from the inbox,
then deletes the third via ThreadDialog per-row.
|
||
|
|
84398de390 |
Task #511: Fully remove Chat feature
Destructive removal per user confirmation ("حذف نهائي ما يرجع").
Removed:
- API: routes/conversations.ts, schema/conversations.ts, all chat
socket handlers in src/index.ts, /admin/users/:id/dependents/
conversations+messages endpoints, conversation/message dependency
counts in users/stats routes.
- Web: pages/chat.tsx, /chat route, dock chat filter, MessageSquare
icon and messages StatCard on home, all chat-related UI in
notifications + admin (dependency badges, delete-dialog rows,
UserDependentConversations/Messages sections, count map keys).
- Locales: nav.chat, home.stats.messages, full chat.* block,
admin.deleteUser conv/msgCount, admin.users.counts.conv/msg,
admin.audit.unit.conversation_*/message_*, admin.dependents.user*.
- OpenAPI spec: tags, all /conversations/* paths, conv/msg dependent
paths, related schemas (ConversationWithDetails, MessageWithSender,
UserDependentConversation/MessageItem+Page, etc.), UserProfile and
UserDeletionConflict conv/msg fields, HomeStats.unreadMessages.
Regenerated client via orval.
- Database: dropped message_reads, messages,
conversation_participants, conversations (CASCADE); deleted
notifications with related_type='conversation' or type='chat';
deleted apps row with slug='chat'; ran drizzle push-force.
- Seed: removed chat:access permission + user-role assignment +
seeded chat app entry from scripts/src/seed.ts.
- Tests: deleted conversations-leave.test.mjs; cleaned chat refs from
list-dependency-counts, delete-force-warnings, audit-log-coverage,
and admin-inline-dependency-counts (e2e) — replaced chat dependents
with note dependents where needed for force-delete coverage.
Notes preserved: notes.tsx noConversationsYet/conversationWith refer
to NOTE THREADS (not chat) and were intentionally NOT touched.
executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
|
||
|
|
992f98418c |
Add ability to archive multiple incoming notes at once
Introduces a bulk archive feature for the received notes tab, including API integration, UI selection state, confirmation dialogs, and internationalization support. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 7c13c05c-e8c2-461e-a5dc-94bbc8e114ac Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv Replit-Helium-Checkpoint-Created: true |
||
|
|
a565d47027 |
Task #508: Remove unused السعر (Price) field from admin Services UI
User requested removal of the price field from the admin Services edit
form ("بالنسبه الى الاسعار أحذفها لايوجد اسعار") because there are no
prices in the product.
Changes — frontend only, all in tx-os:
- artifacts/tx-os/src/pages/admin.tsx
- Drop `price: string` from ServiceForm type.
- Drop `price: "0.00"` from emptyServiceForm.
- Remove the price entry from the edit dialog's field array
(no more السعر input).
- Remove the `<div>{service.price}</div>` from the services list.
- Remove the `price` line from the edit-load mapper.
- artifacts/tx-os/src/locales/ar.json + en.json
- Drop the now-unused `servicePrice` key.
Left intact:
- The generic `"price"` key in locales (line 104) — kept conservatively
in case anything outside this scope expects it, though ripgrep shows
no current usage.
- The `services.price` DB column and api-server code — unused in api
src; only test fixtures insert a placeholder. Removing would require
a migration and break tests for zero benefit.
- The user-facing services page — never displayed price.
Verification:
- ripgrep confirms zero `price` / `servicePrice` references remain in
artifacts/tx-os/src/pages/admin.tsx and the locales (other than the
generic shared key).
- Vite HMR applied cleanly with no TS errors.
- Pre-existing test failures (executive-meetings, service-orders) are
unrelated and already tracked as separate project tasks.
Code review: skipped — change is a 5-line mechanical removal of dead
UI with no logic or data-flow implications.
Follow-ups: none proposed — this is a self-contained, complete fix.
|
||
|
|
59d838b8f2 |
Task #497: Allow row-drag rotation without time windows
Lifted the per-row missing-time drag block (originally #492). Meetings without a (startTime, endTime) tuple are a normal state and now rotate freely alongside timed rows. Server (artifacts/api-server/src/routes/executive-meetings.ts): - Dropped the `no_time_window` early-return guard in /api/executive-meetings/rotate-content. - Sort + slot construction now tolerate null start times (NULLS LAST, tie-break by id) so a null tuple rotates as a real slot. - All other safeguards (cancelled_in_rotate, optimistic lock, renumberDayByStartTime, audit logging) remain intact. Client (artifacts/tx-os/src/pages/executive-meetings.tsx): - Removed missingTimeCount / dayRotatable memos, the dayRotatable prop wiring, dragBlockedByMissingTime + effectiveDragEnabled composition, the row's data-drag-blocked / native title / cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire DragBlockedTooltipButton component + its render site, and the no_time_window errorToast branch in rotateContent. Pruned now-dead Tooltip + AlertTriangle imports. i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block (tooltip + errorToast) from en.json and ar.json. Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs` and added `executive-meetings-rotate-allows-missing-times.spec.mjs`, which inserts 3 meetings (one with null times), verifies no drag-blocked warning button / aria-disabled, drags the top row to the bottom slot, asserts the rotate-content POST succeeds, and confirms exactly one row still has a null tuple after rotation. Verified: row-drag, row-quick-actions, and the new spec all pass (8/8). Architect code review PASSED. |
||
|
|
34dd8d50a4 |
Task #496: hide row Quick Actions in edit mode + drop missing-time banner
- ScheduleSection: gate `quickActionsCanMutate` on `canMutate && !editMode` so clicking a row in edit mode no longer opens the Postpone dialog. In view mode the quick-actions surface still works as before. - MeetingRow: add a useEffect that force-closes local `quickOpen` when capability flips off (edit-mode toggle while dialog is open) and gate `quickOpenShadow` + `data-quick-open` on capability so no stale frame paints. Caught by code review. - Remove the amber `em-rotate-blocked-hint` missing-time banner per product feedback. Kept `missingTimeCount` / `dayRotatable` so row drag still aborts client-side and the per-row tooltip + native title still explain why. - Drop now-unused i18n keys `executiveMeetings.rotate.needsTimeWindow.hint*` from en.json and ar.json (zero/one/two/few/many/other variants). `tooltip` + `errorToast` strings stay (consumed by tooltip button + rotate-content catch handler). - Update executive-meetings-rotate-needs-time-window.spec.mjs to drop the banner visibility assertion and the post-unblock `toHaveCount(0)` re-check. Tooltip / aria-disabled / no-rotate-POST / successful- rotate-after-unblock checks remain. Verified: rotate-needs-time-window + row-quick-actions specs (7 tests) all pass with --workers=1. |
||
|
|
e19506aac7 |
Block EM row-drag when any meeting on the day lacks a time window (#492)
The /api/executive-meetings/rotate-content endpoint hard-rejects with
`code: "no_time_window"` whenever any visible non-cancelled meeting on
the date is missing (start_time, end_time). Before this change, dragging
a row on such a day produced an opaque English error toast ("Every
meeting must have a scheduled time window to rotate") that confused AR
users and didn't tell them what to fix.
Fix:
- Compute `missingTimeCount` / `dayRotatable` from `orderedMeetings`
in executive-meetings.tsx and thread `dayRotatable` into MeetingRow.
- MeetingRow now gates useSortable + safeRowDragListeners on
`effectiveDragEnabled = dragEnabled && dayRotatable`. When drag is
blocked specifically by missing time, the <tr> shows
`cursor-not-allowed`, dimmed opacity, `aria-disabled="true"`,
`data-drag-blocked="no_time_window"`, and a bilingual `title`
tooltip. The aria-disabled is spread AFTER dnd-kit's attributes so
it wins the prop merge.
- Inline amber `em-rotate-blocked-hint` banner above the bulk toolbar
surfaces the missing-time count so mutators see why drag is paused.
- rotateContent's catch handler now detects ApiError code
"no_time_window" (local apiJson now attaches .code/.status to the
thrown Error) and shows the bilingual
`executiveMeetings.rotate.needsTimeWindow.errorToast` instead of
the raw server message.
- Locale keys added under `executiveMeetings.rotate.needsTimeWindow`
in en.json + ar.json (tooltip, hint_one/_other [+ AR plurals],
errorToast).
Tests:
- New tests/executive-meetings-rotate-needs-time-window.spec.mjs
inserts one untimed + two timed meetings and asserts the hint
banner + every row's aria-disabled + data-drag-blocked, then
attempts a drag and asserts NO rotate-content POST is sent and
DB state is unchanged. After UPDATE-ing the missing time, asserts
the hint and aria-disabled lift.
- Existing executive-meetings-row-drag and touch-reorder specs still
pass — drag works exactly as before on fully-timed days.
Files: artifacts/tx-os/src/pages/executive-meetings.tsx,
artifacts/tx-os/src/locales/{en,ar}.json,
artifacts/tx-os/tests/executive-meetings-rotate-needs-time-window.spec.mjs
Server route untouched.
|
||
|
|
b1b77395d0 |
#486 Executive Meetings: row-click quick actions popover (Move up / Move down / Postpone)
Clicking any meeting row on the schedule now opens a small popover with three
quick actions: Move up, Move down, and Postpone. Gated only on canMutate (NOT
editMode) per spec.
Backend
- New POST /executive-meetings/swap-times endpoint (artifacts/api-server/src/
routes/executive-meetings.ts). FOR UPDATE locks both rows by ascending id to
avoid deadlocks, optimistic-lock check via expectedUpdatedAt{A,B} (returns
409 stale_meeting + conflict.lastActor — same shape PostponeDialog
understands), guards different_dates and no_time_window, swaps only
(startTime, endTime), audits each row as `meeting_swap_times`, calls
renumberDayByStartTime so the # column matches the new chronological order,
and broadcasts emitExecutiveMeetingsDayChanged.
- New zod body schema ExecutiveMeetingsSwapTimesBody in lib/api-zod/src/manual.ts.
Frontend
- New shared lib/api-json.ts (ApiError + apiJson) extracted from upcoming-
meeting-alert.tsx so the page can reuse the same fetch/error contract.
- upcoming-meeting-alert.tsx exports PostponeDialog so the page can mount it
for quick-action postpone without duplicating UI.
- artifacts/tx-os/src/pages/executive-meetings.tsx: Schedule wires
swapTimes/quickMoveUp/quickMoveDown/postpone state, computes per-day
neighbours via meetingNumbersById, and mounts a single page-level
PostponeDialog. MeetingRow wraps `<tr>` in a Popover/PopoverAnchor; row
onClick opens the popover with skip rules for buttons/inputs/contenteditable
and ARIA roles (button/checkbox/switch/combobox/dialog) plus testid
prefixes (em-row-grip/-actions/-select, em-edit-*, em-merge-edit-*, em-time-*)
so the time cell's inline editor and other affordances don't collide.
- en/ar locales gain executiveMeetings.quickActions.{label,moveUp,moveDown,postpone}.
Tests
- artifacts/api-server/tests/executive-meetings-swap-times.test.mjs (new):
happy path, 409 stale_meeting (with conflict actor), 400 different_dates,
400 no_time_window. Each scenario uses a distinct far-future date to avoid
daily_number races with seeded data.
- artifacts/tx-os/tests/executive-meetings-row-quick-actions.spec.mjs (new):
drives the date input, verifies row click → popover, Move up swap reflected
in DB, and Postpone item opens the dialog.
Code review (architect) flagged an edit-mode conflict where the time cell
(role=button div) would also bubble to the row handler; fixed by adding ARIA
role and em-time-* skip rules. Architect also flagged keyboard-trigger gap
and edit-mode test gaps — proposed as follow-ups.
Other test failures in the repo (executive-meetings reorder, font-settings,
notes-share, service-orders) pre-date this change and are unrelated.
|
||
|
|
7ea7744e26 |
#481 Polish cascade-affected meetings table in postpone/reschedule prompt
- Drop the leading "#" index column; dailyNumber already serves as a stable per-row identifier. - Replace the hard-coded amber background/border with the user's chosen alert accent via hexToRgba(accent, 0.12 / 0.45). Threaded `accent` through PostponeDialog → CascadePromptBlock so children don't read prefs directly. Both the loading and main prompt blocks track the accent; the rose blocked-by-midnight variant is intentionally kept. - Render times as localized 12-hour with ص/م (AR) or AM/PM (EN) by reusing the shared formatTime helper instead of slicing "HH:mm". - Drop now-unused i18n key cascadeColIndex from ar.json + en.json. - Update the e2e test to match the 3-column schema (meeting#, title, times); kept the dailyNumber assertion in the first cell. tsc clean. Cascade specs pass (Postpone by 10, Reschedule cascade, Cascade prompt UI: Shift/Keep, no-followers fallthrough). |
||
|
|
ef00f48d23 |
Update translations for cascade meeting list notes
Add and update localization strings for the cascade meeting list, including a note about `cascadeListItem` being intentionally kept for backward compatibility. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 7ea15d6b-48ed-4f23-a920-ac6569ce3a81 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/8BABrKh Replit-Helium-Checkpoint-Created: true |
||
|
|
ae31ed90c6 |
#480 Show cascade-affected meetings as a numbered table
Replace the simple <ul> in the cascade-prompt panel inside the upcoming- meeting alert with a 4-column table: row index, the meeting's dailyNumber, title (truncate + title=), and "from → to". The new `meetingNumbersById` map is built once in UpcomingMeetingAlert from the already-loaded dayData and threaded through PostponeDialog into both CascadePromptBlock call sites (postpone-minutes and reschedule). Existing `cascade-followers-list` and `cascade-follower-<id>` testids are preserved on the table/rows so prior selectors keep matching. Adds 4 new i18n keys (cascadeColIndex/MeetingNumber/Title/Times) in ar.json and en.json. The legacy cascadeListItem key is left in place since it isn't worth a separate cleanup. Extends the existing "Cascade prompt UI: Shift sends cascadeFollowing" e2e test to assert the first row's index cell renders "1" and the meeting-number cell renders the seeded follower's dailyNumber. insertImminentMeeting now also returns dailyNumber so the assertion can read it. tsc clean; targeted cascade specs pass (3/3). |
||
|
|
d7a386cf75 |
notes: bulk-select + bulk-delete on Sent tab (task #472)
- Add `useBulkDeleteSentNotes` hook that fans out parallel DELETE
/notes/:id calls via Promise.allSettled and returns {ok, failed}
so the UI can render a precise toast on partial failure. Caches
invalidated once via `invalidateNotesAndFolders`.
- NotesPage: page-level `sentSelectionMode` + `selectedSentIds`
Set, auto-reset when navigating away from the Sent tab.
- Header gets a Select / Cancel toggle, gated on
`view === "sent" && filteredSent.length > 0`.
- New sticky bulk-action bar above the sent list with count,
select-all/clear toggle, and Delete button.
- SentList accepts optional selectionMode/selectedIds/onToggle
props; cards swap onClick (open → toggle) and show a Checkbox
overlay + rose ring when checked.
- AlertDialog confirms before deleting; toast reports all-success,
partial, or all-failed using pluralized i18n keys.
- New i18n keys under `notes.bulk*` in ar.json + en.json.
No server changes (existing DELETE /notes/:id is reused).
Out of scope (per task spec): undo, bulk-archive, bulk endpoint,
multi-select on Active/Received/Archived tabs.
|
||
|
|
ec29657bf5 |
Task #461: Fix notification sounds on iPad (route via media channel)
Problem: User reported zero sound on iPad — even the "Test sound" button in notification settings produced silence. YouTube and other videos worked fine on the same iPad. Root cause: The app's notification player used the Web Audio API (AudioContext + BufferSource). On iOS Safari (iPad/iPhone), Web Audio plays through the **ringer channel**, which is silenced by: - The hardware silent switch (older iPads), - Control Center's mute icon, - Or simply having ringer volume at 0 (independent of media volume). HTMLAudioElement (the API behind <video> and YouTube) plays through the **media channel**, which is what users actually have turned up. Fix in artifacts/tx-os/src/lib/notification-sounds.ts: - Added an HTMLAudioElement playback engine alongside the existing Web Audio engine. Engine selection is one-time per session via the existing `detectIos()` helper (handles iPadOS-as-Mac masquerade). - iOS path: lazy `new Audio(url)` per sound id with preload="auto", primed in `unlock()` by play()-then-pause() at volume 0 inside the user gesture so each element is permanently allowed to play later out-of-gesture (iOS requirement). Real `play()` resets currentTime and plays at full volume; rejection of the play promise dispatches the existing autoplay-blocked hint. - Non-iOS path: unchanged Web Audio behavior preserved verbatim. - All public API preserved: `unlock()`, `play()`, `testPlay()`, `isUnlocked()`, `AUTOPLAY_BLOCKED_EVENT`. No changes needed to use-audio-unlock, use-autoplay-hint, use-notifications-socket, or notification-settings. - Throttling (3s burst-coalesce), bypass for testPlay, vibration, test-only observability globals, and isIos hint flag all preserved. - `__txosNotifCtxState` test hook now reports "running"/"suspended" on iOS based on `unlocked` flag (no AudioContext exists there). Locale updates (ar.json + en.json): - `notifSettings.autoplayHintIos` rewritten: now tells users to raise the **media** volume (same one that controls YouTube) instead of the old "turn off Silent Mute" advice — matches the new playback channel. Verification: tsc --noEmit clean. Pre-existing `test` workflow failure on unrelated executive-meetings.ts type errors is out of scope. Out of scope per task: no PWA, no push notifications, no settings UI redesign, no new sound files. Manual iPad QA needed before merging. |
||
|
|
1aad708cb7 |
notes(#454): per-recipient view/edit folder sharing
- Schema: noteFolderShares.permission ('view'|'edit', default 'view').
- Server: resolveFolderEditAccess + emitFolderChanged helpers.
- POST/PATCH/DELETE/checklist now allow folder editors.
- Editors stamp notes with folder owner's userId; labels validated
against owner; PATCH editors blocked from unfile (folderId=null)
and cross-folder moves to non-matching owners.
- emitFolderChanged fires for owner AND editor mutations across
create / patch (old + new folder) / delete / checklist toggle.
- PUT /shares accepts both legacy recipientUserIds and new
recipients:[{userId,permission}]; diffs add/update/remove/perm-flip.
- GET /shares, /shared-with-me, /shared-notes return permission /
myPermission / readOnly.
- Client:
- notes-api types: FolderSharePermission, myPermission on
SharedFolder/SharedFolderView, permission on FolderShareRecipient,
readOnly:boolean on SharedFolderNote.
- useUpdateFolderShares takes recipients[].
- useCreate/Update/DeleteNote also invalidate ['note-folders'] so
actor's shared-folder rail badge stays fresh.
- FolderShareDialog: per-row checkbox + segmented View/Edit toggle.
- SharedFolderView: editor mode mounts Composer + NoteCard with
full edit/delete/archive affordances; Send hidden in editor
context (Composer + NoteCard hideSend prop) since /send is
owner-only.
- folders-rail: per-folder permission badge.
- socket: note-folder-shared also invalidates ['notes'] +
['note-folders'] for fanout to owner / other editors.
- Locales: shareDescriptionPerm, permissionView/Edit, canEdit
(en + ar).
- Pre-existing executive-meetings.ts type errors are out of scope.
|
||
|
|
a4949983f3 |
Notes: live folder-sharing (read-only) — Task #445
Owner can share a whole folder with other users; recipients see it
under "Shared with me" in the folders rail and view notes read-only
(cannot edit, add, move, or delete).
- DB: new noteFolderSharesTable (folderId/recipientUserId, cascade,
unique idx). Pushed via drizzle-kit.
- API (artifacts/api-server/src/routes/notes.ts):
- GET/PATCH/POST /note-folders return sharedWithCount.
- GET /note-folders/shared-with-me, GET /note-folders/:id/shares,
PUT /note-folders/:id/shares (idempotent diff), DELETE
/note-folders/:id/shares/:userId, GET /note-folders/:id/shared-notes
(verifies recipient via noteFolderSharesTable, stamps readOnly).
- Emits note-folder-shared / note-folder-unshared on share changes.
- Existing note write paths remain owner-scoped → recipients can't
mutate owner notes.
- Frontend types/hooks (notes-api.ts): SharedFolder, FolderShareRecipient,
SharedFolderNote/View; useFolderShares, useUpdateFolderShares,
useSharedWithMeFolders, useSharedFolderNotes.
- FoldersRail: Share menu item, sharedWithCount badge, "Shared with me"
section, "shared-folder" selection kind, onShareFolder prop.
- notes.tsx: SharedFolderView (no Composer, read-only cards using
ChecklistView), FolderShareDialog (seeds existing recipients,
idempotent PUT on save), revocation fallback effect, dialog mount.
- use-notifications-socket.ts: subscribe to note-folder-shared /
-unshared → invalidate shared-with-me + open shared-folder notes for
live rail/view refresh.
- i18n: AR/EN keys for share/sharedBy/sharedByName/sharedWithCount/
sharedWithMe/readOnly/shareRevoked/sharedEmpty/shareSaved/shareFailed.
Pre-existing failing `test` workflow (executive-meetings type errors)
is unrelated and out of scope.
|