Date: 2026-04-24 Sub-project: 2 of 2 in the organization/tenant refactor. Depends on: Sub-project 1 (rename + scope cleanup) must be merged first. Status: Design — awaiting review.
Today the admin's /tenants page renders two flat tables side-by-side (organizations and tenants, under their post-rename names) plus a prompt-customizations matrix. Tenant-level configuration (integrations, notifications, master data, prompts, booking history, API keys) is scattered across the admin and the end-user /settings/* area, with no single place to see everything about a tenant at once.
This refactor restructures the admin around the tenant as the protagonist: an expandable top-level table of organizations and a dedicated per-tenant detail page consolidating config + stats.
After this sub-project:
/organizations shows one row per organization, expandable to reveal nested tenant rows./organizations/-/tenants/:id is a dedicated per-tenant page with stacked sections for every configuration area and a stats panel./settings/*) is linked to, not duplicated.In scope:
/organizations and /organizations/-/tenants/:id.admin/ui/schema_form_test.clj if we touch schema_form.clj).Out of scope:
/settings/* UI changes. The detail page links out to them unchanged.| Path | Purpose |
|---|---|
GET /organizations |
Top-level expandable list of organizations. |
POST /organizations |
Create organization (inline form). |
GET /organizations/-/generate-slug?name=... |
Auto-generate slug from name (existing pattern preserved). |
GET /organizations/:id |
Returns the edit modal for the organization. |
PUT /organizations/:id |
Save organization edits; response is the replacement <tr>. |
GET /organizations/:id/tenants |
Returns the expanded nested <tr> of tenant rows for an organization. |
POST /organizations/-/tenants |
Create tenant under selected organization. |
GET /organizations/-/tenants/:id |
Dedicated tenant detail page (full navigation, not a modal). |
PUT /organizations/-/tenants/:id |
Save tenant identity edits (inline on detail page). |
/organizations/-/tenants/:id/file-store* |
Unchanged — existing routes, reachable from the detail page. |
/organizations/-/tenants/:id/prompts* |
Unchanged — existing routes, reachable from the detail page. |
No dedicated organization detail page. Organizations are thin: name, slug, member list. Identity is edited via modal. Member management already lives on /users. All substantive configuration belongs to tenants, which get the first-class page.
| Column | Source |
|---|---|
| Expand chevron | — |
| Name | organization.name |
| Slug | organization.slug |
| Tenants | count of tenant |
| Users | count of organization_membership |
| Docs (last 30d) | aggregate document count across the org's tenants |
| Review backlog | aggregate document WHERE needs_human_review = true |
| Last activity | MAX(document.updated_at) across the org's tenants |
| Actions | edit (modal), create-tenant shortcut |
Create form above the table (inline, existing pattern) posts to POST /organizations.
hx-get="/organizations/:id/tenants" targeting a placeholder <tr id="org-expansion-:id"> directly below the org row, hx-swap="outerHTML".<tr><td colspan="N"> containing a nested <table> of tenant rows.hx-get returning the placeholder <tr> form, reversing the swap.| Column | Source |
|---|---|
| Name (link to detail page) | tenant.name |
| Country | tenant.company_country |
| Docs | document count |
| Review | document WHERE needs_human_review = true (red badge if > 0) |
| Sources | active / total ap_doc_source (e.g. 3/5) |
| DATEV | ✓ / – from tenant_datev_integration.is_active |
| Last activity | MAX(document.updated_at) |
Clicking the name navigates to /organizations/-/tenants/:id.
Removed from the top-level /organizations page. Per-tenant prompt customizations move to the detail page (Section 6). Cross-tenant matrix view is not reproduced; we can add one later if it turns out to be missed.
/organizations/-/tenants/:id — full page with left-rail anchor navigation to the sections. Non-modal.
Organizations / <Organization Name> / <Tenant Name> (each crumb a link).Each section is a <section> with an id for anchor jumping; left-rail lists these anchors.
ap_doc_source rows for this tenant: address, provider, connection status, last sync, docs received, active flag. Create/edit delegated to the existing end-user /settings/* source-management page.tenant_datev_integration.metadata), connected-by user, credential expiry. Link out to /settings/integrations for reconnection./settings/google-drive./settings/data./organizations/-/tenants/:id/prompts?key=..../organizations/-/tenants/:id/file-store./settings/notifications./settings/booking-history.admin/http/api_keys.clj, with the tenant filter pre-applied)./qa-dataset?tenant-id=<id>./costs?tenant-id=<id> for the full per-tenant cost dashboard.Sections that have an existing dedicated admin page (/costs, /qa-dataset, /api-keys) or end-user page (/settings/*) show a summary + link, not a duplicate configuration UI. Only identity and API keys are editable on the detail page; everything else is read-only summary + navigation to the canonical editor. This keeps the page scannable and avoids two sources of truth for the same config.
Follows the existing admin patterns documented in src/com/getorcha/admin/ui/:
section, card, stat-grid, data-table, status-badge from admin.ui.components.Admin has no HTTP-handler tests today. This sub-project does not introduce them. Only test touched: test/com/getorcha/admin/ui/schema_form_test.clj, and only if admin.ui.schema_form itself is modified (unlikely).
Manual verification:
/organizations, verify aggregate counts match direct DB queries for a sample org.clj-kondo --lint src test dev clean.
/tenants → /organizations route map lands in SP1; SP2 builds on the new names directly./settings/integrations moves or changes how it presents DATEV status, the summary on the detail page drifts. Mitigation: the summary pulls from the same DB tables, not from scraping the settings page; it won't drift due to UI changes there.