Admin Panel Refactor: Organization-Grouped Tenant Management

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.

Context

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:

Scope

In scope:

Out of scope:

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.

Top-level Table

Organization row (collapsed)

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.

Expansion mechanism

Nested tenant row columns

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.

Prompt-customizations matrix

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.

Tenant Detail Page

/organizations/-/tenants/:id — full page with left-rail anchor navigation to the sections. Non-modal.

Sections

Each section is a <section> with an id for anchor jumping; left-rail lists these anchors.

  1. Identity & Company Info — editable inline: name, company address, VAT ID, tax ID, country. PUT-per-field on blur, following the inline-edit pattern already in use in the ERP UI.
  2. Ingestion Sources — table of 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.
  3. ERP Integration (DATEV) — status, company-name (from tenant_datev_integration.metadata), connected-by user, credential expiry. Link out to /settings/integrations for reconnection.
  4. OAuth Integrations — Google Drive status + folder. Link out to /settings/google-drive.
  5. Master Data — three subsections: GL accounts, cost centers, business partners. Each: row count, active status, last updated. Link out to /settings/data.
  6. Prompt Customizations — each of the 8 prompt keys (extraction, vision, accounts-match, cost-center-match, accrual-match, tax-compliance, resolve-uncertain-validations, triage) with its current additions (if any) and an edit link to /organizations/-/tenants/:id/prompts?key=....
  7. File Store (FP&A) — current backend + summary config. Link to /organizations/-/tenants/:id/file-store.
  8. Notifications — channel count, workflow count. Link to /settings/notifications.
  9. Booking History — latest upload, total items. Link to /settings/booking-history.
  10. API Keys — list scoped to this tenant: prefix, name, permissions summary, created, last used, revoked status. Create/revoke inline (existing logic from admin/http/api_keys.clj, with the tenant filter pre-applied).
  11. QA Dataset — counts by status (pending / approved / rejected). Link to /qa-dataset?tenant-id=<id>.
  12. Stats / Cost — token usage last 30d (chart), OCR pages, ingestion success rate, avg processing time. Link to /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.

Hiccup and HTMX conventions

Follows the existing admin patterns documented in src/com/getorcha/admin/ui/:

Testing

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:

clj-kondo --lint src test dev clean.

Rollout

Risks

Follow-ups (not in this sub-project)