Spec: F43 — Admin HQ (/hq) for Staff & Superadmin Users
Status: shipped
Contract Graph boundary (2026-07-14): F43.cue owns the 12 staff/superadmin HQ API methods and their backend authorization/state proofs. F85 now owns the pilot API boundary separately. The graph keeps React route/browser runtime, recent Sentry issues, and the F85 pilot dashboard runtime visibly open; API source presence is not evidence that the staff product surface is complete.
1. Problem & CEO Job
Job: See clearly — Lumi’s support, devops, and product operators lack a dedicated surface. The
unskinned Django CRUD shell at /tmt/ cannot test email, trigger notifications, inspect cross-org
metrics, or manage users without SSH into a production container.
Admin HQ (/hq) is the staff/superadmin operating dashboard within the React SPA, with separate
navigation and permission tiers, invisible to non-staff users.
2. User Stories & Acceptance Criteria
Staff User (is_staff=True)
-
As a staff user, I want to access
runlumi.app/hq, so that I can operate and monitor Lumi.-
GET /hqreturns the admin dashboard. Non-staff users are redirected to/work/today. -
GET /hq/*routes all requireis_staff=Truein the JWT.
-
-
As a staff user, I want to see operational metrics at a glance, so that I know the system’s health.
- Dashboard shows: total orgs, total users, active orgs (7-day), daily active users, API error rate, Celery queue length.
- Dashboard shows recent Sentry issues (if Sentry integration is configured).
-
As a staff user, I want to test email delivery and notifications, so that I can verify they work before a customer complains.
-
/hq/notificationshas a form: send a test email to any address, send a test push notification to any device. - Email form accepts user search (resolves to email) or a custom email address.
- Push form uses debounced user search picker, sends real device push via FCM.
- Form shows delivery status (sent/failed/no-active-devices) and timing.
-
-
As a staff user, I want to browse and search organizations, so that I can assist customers.
-
/hq/orgslists all organizations with search, sort, and filter. - Clicking an org shows details: member count, subscription status, last pulse date, date created.
-
Superadmin User (is_superuser=True)
-
As a superadmin, I want to manage users across all organizations, so that I can handle account issues.
-
/hq/userslists all users with search, sort, and filter. Superadmin only. - Clicking a user shows: email, name, orgs they belong to, last login, is_staff flag, date joined.
- Superadmin can toggle
is_staffon any user (grant/revoke staff access). - Superadmin can temporarily impersonate a user (login as them) for debugging — audited.
-
-
As a superadmin, I want to view audit logs across all organizations, so that I can investigate security incidents.
-
/hq/auditshows a cross-org audit log with search, filter by org/action/actor, and date range.
-
-
As a superadmin, I want to view system configuration, so that I know what’s deployed.
-
/hq/systemshows: Django version, Python version, LLM provider, Redis status, DB status, Celery worker status, last deploy timestamp (from version.json). -
/hq/pilot(F85) shows the pilot activation gates (update-completion / retention / action-rate / expansion / payment / accuracy) and the per-firm cohort table. Staff-only, sameis_staffguard as every other/hq/*route; readsGET /api/hq/pilot/gates/andGET /api/hq/pilot/cohort/. The API boundary is now contract-owned by F85; the browser/runtime criterion remains open.
-
3. Scope Boundaries
- In:
/hqroute group in the React SPA, protected byis_staffcheck.- Separate admin sidebar with HQ-only navigation items.
- Backend API endpoints for HQ data (all require
is_staff=True):GET /api/hq/dashboard/— metricsPOST /api/hq/notifications/test-email/POST /api/hq/notifications/test-push/GET /api/hq/orgs/— list organizationsGET /api/hq/orgs/{id}/— org detailGET /api/hq/users/— superadmin onlyGET /api/hq/users/{id}/— superadmin onlyPATCH /api/hq/users/{id}/— superadmin only (toggle is_staff)POST /api/hq/users/{id}/impersonate/— superadmin onlyGET /api/hq/audit/— superadmin onlyGET /api/hq/system/— superadmin only
- JWT payload includes
is_staffandis_superuserclaims. useAuthStoredecodes and storesisStaffandisSuperuserfrom JWT.
- Out:
- Replacing Django admin at
/tmt/—/tmt/remains available for raw DB access. - Org-level admin features (billing management, org settings override) — future spec.
- Dashboard with graphs/charts — v1 is numbers and tables. Charts in v2.
- Full impersonation session management — v1 is a simple JWT impersonation token.
- Replacing Django admin at
4. Data Model
4.1 JWT Token Claims
Django simplejwt already includes user attributes in the token. Extend the token serializer to include is_staff and is_superuser:
# backend/apps/accounts/serializers.py — TokenObtainPairSerializer
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token["is_staff"] = user.is_staff
token["is_superuser"] = user.is_superuser
return token
4.2 No New Models
HQ is read-only + operational actions. No new database models. The audit log uses the existing AuditLog model.
4.3 Impersonation Token
When a superadmin impersonates a user, a short-lived JWT (5-minute TTL) is issued for the target user with an impersonated_by claim. This token is not refreshable. The action is audited.
5. API Contract
All endpoints require Authorization: Bearer <token> with is_staff=True.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/hq/dashboard/ | staff | Returns metrics JSON |
| GET | /api/hq/analytics/ | staff | Growth analytics: zero-filled daily series (org_signups, user_signups, pulses, active_users) over ?days= (7–90, default 30), plus subscription_breakdown and top_orgs (top 10 by pulses in window) |
| POST | /api/hq/notifications/test-email/ | staff | Send test email. Body: {user_id?, email?, subject?} — resolves User → email if user_id given |
| POST | /api/hq/notifications/test-push/ | staff | Send real device push. Body: {user_id (preferred), member_id? (back-compat), title?, body?} — returns {ok, devices, sent} or {ok: false, detail: "no active devices"} |
| GET | /api/hq/orgs/ | staff | List orgs. Query: ?search=&ordering=&page=. Rows include member_count (active), subscription_status (latest sub), last_pulse_date — annotated, no N+1 |
| GET | /api/hq/orgs/{id}/ | staff | Org detail with member count, subscription, last pulse |
| GET | /api/hq/users/ | superuser | List users. Query: ?search=&ordering=&is_staff=&page=. Rows include org_count, last_login |
| GET | /api/hq/users/{id}/ | superuser | User detail |
| PATCH | /api/hq/users/{id}/ | superuser | Toggle is_staff. Body: {is_staff: bool} |
| POST | /api/hq/users/{id}/impersonate/ | superuser | Returns impersonation JWT. Response: {access, refresh?} |
| GET | /api/hq/audit/ | superuser | Cross-org audit log. Query: ?org=&action=&actor=&from=&to=&page=. Rows include org_name, actor_email, detail (metadata → target fallback) |
| GET | /api/hq/system/ | superuser | System status: Django version, Python, LLM provider, Redis, DB, Celery, deploy timestamp |
Permission Classes
# backend/apps/hq/permissions.py
class IsStaff(permissions.BasePermission):
def has_permission(self, request, view):
return bool(request.user and request.user.is_staff)
class IsSuperuser(permissions.BasePermission):
def has_permission(self, request, view):
return bool(request.user and request.user.is_superuser)
These check the Django User model fields directly — they are available on request.user after JWT authentication (simplejwt sets request.user from the token’s user_id).
6. UI / UX
6.1 Routes
All routes under /hq:
| Route | Page Component | Access | Description |
|---|---|---|---|
/hq | HqDashboardPage | staff | Metrics dashboard with stat cards |
/hq/notifications | HqNotificationsPage | staff | Test email/push forms |
/hq/orgs | HqOrgsPage | staff | Org list with search table |
/hq/orgs/:id | HqOrgDetailPage | staff | Org detail: members, subscription, pulse |
/hq/users | HqUsersPage | superadmin | User list with search, is_staff toggle |
/hq/users/:id | HqUserDetailPage | superadmin | User detail, impersonate action |
/hq/audit | HqAuditPage | superadmin | Cross-org audit log with filters |
/hq/system | HqSystemPage | superadmin | System config & service status |
6.2 Protection
// App.tsx
<Route element={<HqLayout />}>
<Route path="/hq" element={<HqDashboardPage />} />
<Route path="/hq/notifications" element={<HqNotificationsPage />} />
<Route path="/hq/orgs" element={<HqOrgsPage />} />
<Route path="/hq/orgs/:id" element={<HqOrgDetailPage />} />
<Route path="/hq/users" element={<HqUsersPage />} />
<Route path="/hq/users/:id" element={<HqUserDetailPage />} />
<Route path="/hq/audit" element={<HqAuditPage />} />
<Route path="/hq/system" element={<HqSystemPage />} />
</Route>
HqLayout checks isStaff from the auth store. If !isStaff, redirect to /work/today. For superadmin-only pages, the page component itself checks isSuperuser and shows a 403 or redirects.
6.3 HqLayout — Sidebar
HqLayout (frontend/src/layouts/HqLayout.tsx) renders a fixed sidebar with a darker background to visually distinguish the admin surface from the main app:
Design tokens:
- Sidebar background:
bg-slate-800(#1E293B) — darker than main app (#FFFFFF/bg-white) - Sidebar text:
text-slate-100for nav items,text-slate-400for section labels - Nav item hover:
hover:bg-slate-700 - Active nav item:
bg-slate-700with left border accent (border-l-2 border-blue-400) - Main content area:
bg-[#FAF8F4]— matches the main app background tone
Sidebar layout:
┌──────────────────────────┐
│ HQ │ ← fixed top, border-b border-slate-700
│ Admin HQ (title) │ uses t('title')
├──────────────────────────┤
│ │
│ ◈ Dashboard │ ← all staff users see these
│ ◈ Organizations │
│ │
│ ── Superadmin ──────── │ ← hidden unless isSuperuser:
│ ◈ Users │ conditional render with isSuperuser check
│ ◈ Audit Log │
│ ◈ System │
│ │
│ ◈ Notifications (test) │ ← staff only (test tools)
│ │
├──────────────────────────┤
│ ← Back to Lumi │ ← Link to /work/today, uses t('back_to_lumi')
│ │ icon: ArrowLeft
└──────────────────────────┘
Behavior:
- Superadmin nav section is conditionally rendered with
isSuperuserfrom the auth store — staff-only users see only Dashboard, Organizations, Notifications, and Back to Lumi. - Active nav item is highlighted with a blue left border accent (
border-l-2 border-blue-400). - “Back to Lumi” always navigates to
/work/today(not a simple back navigation) to exit the admin context entirely. - The sidebar width is fixed at
w-56(14rem / 224px).
6.4 Dashboard — Stat Cards
HqDashboardPage displays 6 stat cards in a 2-row × 3-column responsive grid.
Card design:
┌─────────────────────┐
│ Total Organizations │ ← label (text-xs text-slate-500 uppercase)
│ 47 │ ← value (text-3xl font-bold)
│ 7 days │ │ ← subtitle (text-xs text-muted-foreground)
└─────────────────────┘
Grid layout:
Row 1: [Total Orgs] [Total Users] [Active Orgs (7d)]
Row 2: [DAU (today)] [API Errors (24h)] [Celery Queue]
- Cards use the
Card,CardHeader,CardContent,CardTitleshadcn/ui components fromfrontend/src/components/ui/card. - Each card shows: label (i18n key), value (fetched from
GET /api/hq/dashboard/), and an optional subtitle/context. - Loading state: each card shows a
Skeletoncomponent while data loads. - Empty/zero state: show “0” — zero is valid data, not an error.
- Error state: if the API call fails, cards show ”—” and a toast with
t('error_load'). - Responsive: 3 columns on large screens, 2 on medium, 1 on small.
6.5 Organizations — List Table with Search
HqOrgsPage renders a searchable, sortable table of all organizations in the system.
Search bar: An <Input> component above the table with a search icon. As the user types, results filter client-side (or debounced server-side via ?search= query param).
Table columns:
| Column | Source | Sortable |
|---|---|---|
| Name | organization.name | ✅ |
| Members | organization.member_count | ✅ |
| Subscription | organization.subscription_status | ✅ |
| Last Pulse | organization.last_pulse_date | ✅ |
| Created | organization.created_at | ✅ |
- Uses the shadcn/ui
<Table>component fromfrontend/src/components/ui/table. - Rows are clickable: navigating to
/hq/orgs/:idfor detail view. - Empty state: “No organizations found” with illustration if search yields no results.
- Loading state:
Skeletonrows matching the table structure. - Pagination: server-side paginated (page size 20), with pagination controls at the bottom.
6.6 Superuser-Only Sections
Superadmin-only pages (Users, Audit Log, System) follow these UI rules:
- Nav hiding: The sidebar section divider ”── Superadmin ──” and the 3 links below it are conditionally rendered only when
isSuperuser === trueinuseAuthStore. - Direct URL access: If a staff-only user navigates directly to
/hq/users, the page component renderst('superadmin_only')with a link back to/hq. No flash of superadmin UI. - Superadmin pages include: A small badge at the top of each page:
t('superadmin_only')in a yellow/warning badge style, to remind the user they’re in a privileged area.
6.7 i18n — hq Namespace
All hq namespace keys with their en and vi values:
| Key | English | Vietnamese |
|---|---|---|
hq.title | Admin HQ | Trung tâm vận hành |
hq.dashboard | Dashboard | Bảng điều khiển |
hq.organizations | Organizations | Tổ chức |
hq.users | Users | Người dùng |
hq.audit | Audit Log | Nhật ký kiểm tra |
hq.system | System | Hệ thống |
hq.notifications | Notifications (test) | Thông báo (kiểm thử) |
hq.back_to_lumi | Back to Lumi | Về Lumi |
hq.impersonate | Impersonate | Mạo danh |
hq.test_email_sent | Test email sent | Email kiểm thử đã gửi |
hq.test_push_sent | Test push sent | Push kiểm thử đã gửi |
hq.staff_only | Staff access only | Chỉ nhân viên truy cập |
hq.superadmin_only | Superadmin access required | Yêu cầu quyền quản trị viên cấp cao |
hq.error_load | Could not load HQ metrics — check your connection and try again. | Không tải được số liệu HQ — kiểm tra kết nối và thử lại. |
hq.metric_total_orgs | Total organizations | Tổng tổ chức |
hq.metric_total_users | Total users | Tổng người dùng |
hq.metric_active_orgs | Active orgs (7 days) | Tổ chức hoạt động (7 ngày) |
hq.metric_dau | Daily active users | Người dùng hoạt động hôm nay |
hq.metric_api_errors | API errors (24h) | Lỗi API (24h) |
hq.metric_celery_queue | Celery queue | Hàng đợi Celery |
hq.disconnected | Disconnected | Mất kết nối |
All keys are used via useTranslation('hq') and t('key_name') — no hardcoded English strings in JSX.
Existing locale files at frontend/src/i18n/locales/{en,vi}/hq.json already contain all 21 keys listed above.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Non-staff user redirected from /hq to /work/today | frontend xfail | hq.test.tsx::staff gate |
| 2 | Staff user can access /hq and see dashboard | frontend xfail | hq.test.tsx::dash metrics |
| 3 | HqLayout renders darker slate-800 sidebar | frontend xfail | hq.test.tsx::route rendering |
| 4 | Staff (not superuser) sees limited nav (no users/audit/system) | frontend xfail | hq.test.tsx::superuser nav |
| 5 | Dashboard shows org count and member count | frontend xfail | hq.test.tsx::dash metrics |
| 6 | Superadmin can see Users nav link | frontend xfail | hq.test.tsx::user listing |
| 7 | Superadmin can see Audit Log nav link | frontend xfail | hq.test.tsx::audit log |
| 8 | Org listing nav link renders for staff | frontend xfail | hq.test.tsx::org listing |
| 9 | Non-staff user redirected from /hq to /work/today | e2e | describe("Admin HQ") |
| 10 | Staff user can access /hq and see dashboard | e2e | same block |
| 11 | Staff user redirected from /hq/users (superadmin-only) | e2e | same block |
| 12 | Superadmin can access /hq/users and see user list | e2e | same block |
| 20 | staff@demo.local can access /hq — heading visible, URL stays /hq | e2e | cross-cutting-2.spec.ts › F43 — Admin HQ (staff user) |
| 21 | staff@demo.local sees HQ sidebar nav | e2e | cross-cutting-2.spec.ts › F43 — Admin HQ (staff user) |
| 22 | sales@demo.local (non-staff) is redirected from /hq | e2e | cross-cutting-2.spec.ts › F43 — Admin HQ (staff user) |
| 23 | Unauthenticated /hq visit redirected to login | e2e | cross-cutting-2.spec.ts › F43 — Admin HQ (staff user) |
| 24 | staff@demo.local can access /hq/jobs (Recruitment ATS tab) | e2e | cross-cutting-2.spec.ts › F43 — Admin HQ (staff user) |
| 13 | Superadmin can toggle is_staff on a user | backend | tests/test_hq.py::TestUserDetail |
| 14 | Superadmin can impersonate a user | backend | tests/test_hq.py::TestImpersonate |
| 15 | Impersonation token is short-lived and non-refreshable | backend | same class |
| 16 | Test email sends successfully via /api/hq/notifications/test-email/ | backend | tests/test_hq.py::TestNotifications |
| 17 | Dashboard metrics return correct counts | backend | tests/test_hq.py::TestDashboard |
| 18 | Cross-org audit log is accessible to superadmin only | backend | same class |
| 19 | JWT token includes is_staff and is_superuser claims | backend | tests/test_auth.py |
8. Current Behavior
- Django admin at
/tmt/— unskinned, functional for raw DB access. Requiresis_staff=Trueto log in. - No
/hqroute in the React SPA. - JWT tokens do not include
is_stafforis_superuserclaims (simplejwt’s defaultget_tokenonly includesuser_id). - No admin-specific API endpoints.
- Frontend
useAuthStoreonly storesisAuthenticated,access_token,refresh_token,organization_id, anduser. - No role differentiation in the frontend beyond
isAuthenticated.
9. Error States
| Scenario | Behavior |
|---|---|
Non-staff hits /hq/* | Redirect to /work/today. No flash of admin UI. |
| Staff hits superadmin-only route | Page shows “Superadmin access required” with a link back to /hq. |
JWT expired while in /hq | Standard 401 → refresh flow → if refresh fails, redirect to /login. |
| Impersonation token expires | Redirect to /hq/users with toast “Impersonation session expired.” |
| Test email fails | Show error toast with SMTP error message. Do not crash. |
| Celery/Redis down for dashboard | Show “Disconnected” badge next to queue count. |
10. Diagrams & Docs Touched
- frontend/DIAGRAMS.md: add
/hqroute group,HqLayoutcomponent, role-based route guards. - backend/DIAGRAMS.md: add
apps/hq/app, request lifecycle withIsStaff/IsSuperuserpermission checks. backend/apps/hq/— new Django app.
11. Implementation Audit
Edge Cases Catalog (formerly §9, 2026-06-14; 3 items): cross-tenant staff mutation guard, high-risk action second-confirmation (impersonation 5-min TTL), partial-permission redacted cards. Full catalog in git history.
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | JWT claims is_staff + is_superuser in login/register/refresh tokens | ✅ match | accounts/views.py L64-65 (login), L104-105 (register), L174-175 (refresh); test_api.py::TestTokenClaims (4 tests) |
| 2 | apps/hq/ Django app with 11 endpoints (dashboard, org list/detail, user list/detail/toggle/impersonate, audit, system, test-email, test-push) | ✅ match | backend/apps/hq/urls.py, views.py, permissions.py; tests/test_hq.py (57 tests) |
| 3 | IsStaff + IsSuperuser permission classes gating HQ routes | ✅ match | backend/apps/hq/permissions.py |
| 4 | Frontend auth store decodes JWT, stores isStaff + isSuperuser | ✅ match | authStore.ts; authStore.test.ts |
| 5 | HqLayout + 8 HQ pages (Dashboard, Orgs, Users, Audit, System, Notifications + detail views) | ✅ match | 13 Hq*.tsx files in frontend/src/pages/hq/; frontend/src/test/hq.test.tsx (11 tests) |
| 6 | E2E tests for Admin HQ | ✅ match | describe("Admin HQ") in lumi.spec.ts |
| 7 | i18n hq namespace in en + vi | ✅ match | 62 locale keys confirmed by registry |
| 8 | Non-staff redirect and superadmin-only route guard enforced | ✅ match | HqLayout permission guard + backend/apps/hq/permissions.py::IsSuperuser |
12. Open Questions / Decisions
- URL path:
/hqvs/adminvs/staff? Decision:/hq— short, distinct, doesn’t conflict with Django’s/admin(which we don’t use; we use/tmt/), and reads well as “headquarters.” - Impersonation scope: Full-session impersonation vs single-request? Decision: JWT-based session with 5-minute TTL. The superadmin gets a token for the target user. They use it in a separate browser tab/profile. No “switch back” mechanism in v1 — the token expires and they refresh to return to their own session.
- Dashboard metrics source: Real-time queries vs. cached? Decision: real-time queries for v1. The metrics are simple COUNT queries (not aggregates over millions of rows). Add caching in v2 if needed.
- Sentry integration: Should the dashboard show Sentry issues? Decision: show a link to Sentry in v1. Inline issues in v2 when Sentry API integration is built.
is_staffassignment: Who can grant staff access? Decision: superadmin only, via the/hq/users/{id}page (toggleis_staffflag). New users are never staff by default.
13. Bug / Change Log
2026-07-10 — W-HQ-05: System page showed dishonest/blank status (real bug, WU7)
Type: Bug fix (FE↔BE contract mismatch, Amodei honest-AI violation)
Summary:
GET /api/hq/system/matched §5’s documented contract in name only: it returned only{db: "ok"|"error", celery, debug, version}— nodjango_version,python_version,llm_provider,redis, orlast_deploy. The frontend read fields (db_status,celery_status,redis_status) the backend never sent, so every boolean coerced tofalseand rendered “Down” for a healthy DB serving the very request — dishonest per SOUL.md/GOAL.md’s Amodei test.system_viewnow returns the full §5 contract:django_version(django.get_version()),python_version(platform.python_version()),llm_provider(settings.LLM_PROVIDER),redis(a real 1s-timeoutredis.from_url(...).ping(), honestly"unavailable"when unreachable — legitimate in local dev), andlast_deploy(BUILD_TIMEenv var stamped by.github/workflows/deploy-{dev,prod}.yml;null/honest"—"when unset, never a fabricated “just now”).HqSystemPage.tsxnow types the response as the real"ok" | "error" | "unavailable"string union (not booleans) and mapsdb/celery/redisdirectly — a field the backend omits renders"—", never a fabricated"Down".- Tests: 9 new cases in
TestSystemView(backend/tests/hq/test_hq.py) — all fields present, db-ok-when-healthy, redis ok/unavailable branches, real django/python versions, configured LLM provider, last_deploy honest-null vs real-BUILD_TIME. 2 new frontend cases infrontend/src/test/hq/HqSystemPage.test.tsxpin “never renders Down for a healthy dependency” and “renders Down only for a genuinely-down one, — for absent last_deploy.” - No i18n changes —
django_version,python_version,llm_provider,redis_status,db_status,celery_status,deploy_timestamp,status_ok,status_downkeys already existed inen/vihq.jsonfrom the original (unwired) UI.
2026-07-10 — Analytics page polish: KPI deltas, semantic status colors, ranked top-orgs list
Type: Improvement (five-veto pass on the new analytics page)
Summary:
GET /api/hq/analytics/response gainsprevious— totals over the prior window of equal length (org_signups,user_signups,pulses, andactive_usersas peak daily distinct, matching the headline KPI semantics). KPI cards now show % deltas (“+38%” green / “−12%” red / “new” when the prior window was empty) with a “vs prior Nd” caption.- Subscription donut + legend use semantic status colors (active=green, trialing=blue, grace=amber, read_only=red, cancelled=muted) instead of palette order; legend labels reuse
billing.status_*translations. - Top orgs switched from a vertical bar chart (only 2–3 of 10 axis labels fit at 390px) to a ranked list with proportional bars — readable at 320.
- Tests:
test_previous_window_totals(+zero-history case) backend; delta + ranked-list cases frontend. 2 newhq.*i18n keys propagated to 61 locales.
2026-07-10 — HQ analytics page (charts)
Type: Feature (owner-requested)
Summary:
- New staff-only endpoint
GET /api/hq/analytics/— zero-filled daily series (org signups, user signups, pulses, distinct active pulse users) over a clamped?days=window (7–90, default 30), subscription-status breakdown, top 10 orgs by pulse volume. Allowlisted in the tenant-isolation guard (cross-org staff read). - New route
/hq/analytics(HqAnalyticsPage) + HQ nav link: KPI strip (2-col mobile / 4-col xl) + six chart cards (1-col mobile / 2-col md) using the shared Lumi chart kit (LineChartWidget,PieChartWidget+ inline legend,BarChartWidget); 7/30/90-day window switcher. - i18n: 13 new
hq.*keys in en/vi, propagated to all 61 locales. - Tests:
TestHqAnalytics(6) inbackend/tests/hq/test_hq_analytics.py;hqAnalytics.test.tsx(4) frontend. - Route trees in
frontend/DIAGRAMS.mdupdated.
2026-07-10 — HQ list payload enrichment (mobile QC follow-up to PR #1586)
Type: Bug fix (API contract drift)
Summary:
- The HQ admin tables rendered columns the list endpoints never sent, so cells showed
—at every width: orgs lackedmember_count/subscription_status/last_pulse_date; users lackedorg_count/last_login; audit lackedorg_name/actor_email/detail. orgs_list_viewnow annotates member count (active members), latest subscription status, and last pulse date viaCount/Subquery— single query, no N+1.users_list_viewannotatesorg_countand appendslast_login; enriched in the HQ view (not the sharedUserSerializer) so the fields don’t leak into accounts endpoints.audit_log_viewgainsselect_related("organization", "actor__user")plusorg_name,actor_email, anddetail(compact metadata JSON →target_type target_idfallback → empty).- Tests:
TestHqListPayloadEnrichment(7 tests) inbackend/tests/hq/test_hq.pypins the payload contract.
2026-07-02 — Fix §11 stale line numbers, test count, and file path
Type: Documentation
Summary:
- §11 Row 1 evidence: corrected
accounts/views.pyline ranges from L61-62/L95-96/L162-163 to L64-65 (login), L104-105 (register), L174-175 (refresh). Verified bygrep -n 'is_staff\|is_superuser' backend/apps/accounts/views.py. - §11 Row 2 evidence: test count updated from 34 → 57 (
grep -c 'def test_' backend/tests/test_hq.py). - §11 Row 5 evidence:
Hq*.tsxcount updated from 11 → 13; path corrected fromfrontend/src/pages/tofrontend/src/pages/hq/; test file path and count corrected fromhq.test.tsx (7 tests)tofrontend/src/test/hq.test.tsx (11 tests). - No code changes.
2026-07-10 — /hq/notifications: user-picker test sends (fix three latent bugs)
Type: Bug fix + feature
Summary:
- (a) Added
?search=filter toGET /api/hq/users/— icontains on email OR name, pagination preserved, superuser-only unchanged. - (b)
test_email_viewnow accepts optionaluser_id(resolves User → email server-side); raw email field kept for backward compat; 404 on unknown user_id. - (c)
test_push_viewREWRITTEN: sends real device push viasend_push_to_memberinstead ofsend_email_with_log. Acceptsuser_id(preferred) ormember_id(back-compat). Resolves the user’s OrganizationMembers, counts active DeviceTokens, returns{ok, devices, sent}or{ok: false, detail: "no active devices"}when zero active tokens. Staff-only + audit log. - Frontend: replaced raw email/device_token fields with a shared debounced user search picker
(
GET /api/hq/users/?search=…, shows email + name). Email card keeps an optional “use custom email” collapsed input. Push card shows no-active-devices error. hqApi.testPush now sends{user_id, title?, body?}; hqApi.testEmail sends{user_id?, email?, subject?}. - Tests: 19 backend tests in TestNotifications (search filter, email-by-user_id, push-by-user_id happy path, no-devices, back-compat member_id, non-staff 403). 4 frontend unit tests.
- i18n: added
search_user,searching,select_user_first,use_custom_emailkeys to en + vi hq.json (and all other locales).
Type: Documentation
Summary:
- 13 of 15
[ ]ACs in §2 flipped to[x]— confirmed shipped against §11 audit rows 1–8 (all ✅). - 2 ACs remain
[ ]: Sentry issues inline display (deferred to v2 per §12 decision 4 — v1 shows a link only) and the F85 pilot tab (no §11 evidence; pilot endpoints not listed in §11 Row 2’s 11-endpoint set). - No code changes.
2026-06-15 — Frontend route namespaced
Frontend route namespaced: “Back to Lumi” / non-staff redirect target /today -> /work/today (app-wide IA restructure; /hq and API paths unchanged).
2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)
Type: Documentation Change
Summary:
- §9 Edge Cases (3 items) absorbed into §11 Implementation Audit note.
- §12 Implementation Audit converted from bullet/narrative to canonical 4-column table (8 rows, all ✅).
- Sections renumbered: §10 ES → §9, §11 Diags → §10, §12 Audit → §11, §13 OQ → §12, §14 BL → §13.
Reason:
- Align with TEMPLATE.md canonical section order (§1–§13) per SP fleet restructuring pass.
Impact:
- Spec authors and future implementation agents.
None yet — spec not implemented.