afterhours-shift-manager/docs/admin-ui-evaluation.md

9.9 KiB

Evaluation: admin web UI vs. clarifying/expanding in-Slack admin commands

Spike for issue #136 — decide how to make administration easier: a separate admin web UI, or clarifying/expanding the in-Slack admin commands (Block Kit modals + an App Home admin section). This document records the options, a recommendation, and the scope of the follow-up build. It is a "decide and recommend" spike — no admin-surface code ships in this PR. The route is left for sign-off (see "Decision needed" at the end).

Current admin surface

Every admin action is a positional-text subcommand of /oncall admin …, authorized by is_admin = user_id in schedule.get_admin_users() (src/shared/shared/schedule.py, get_admin_users) and handled in _handle_admin() / _handle_admin_holiday() (src/slack-bot/app.py):

  • admin override <date> <ext> [day|night] — assign a shift
  • admin open <date> [day|night] — mark a shift open
  • admin clear <date> [day|night] — remove an override (revert to weekly)
  • admin roster add|remove|rename <ext> [name]
  • admin holiday add <date> <slots> [x<mult>] <label> / holiday remove <date> / holiday list

A same-day override / open / clear also repoints 3CX (_update_3cx_routing), and holiday add provisions the 08:00/17:00 one-off schedules and may activate inline — so these commands have real side effects, which makes a confirmation preview valuable.

Pain points (from the issue, confirmed in code)

  • Positional and error-prone. Order-sensitive args — most acutely holiday add <date> <slots> [x<mult>] <label>, which hand-parses an optional x<mult> token out of the middle of the label (_admin_holiday_add). On any mistake the only feedback is a usage string.
  • Discoverability. The single source of documentation is the static admin block in build_help_blocks(is_admin=True) (src/shared/shared/blocks.py). No pickers, no validation hints, no previews.
  • No structured input. No date picker, no roster dropdown, no confirmation/preview before a change lands (and, same-day, repoints 3CX).
  • No overview. No calendar/grid of upcoming overrides + holidays to scan.

What already exists (lowers the cost of the Slack-native path)

  • App Home tab is live. home_tab_enabled: true in slack-app-manifest.yaml; build_home_view() renders it and app_home_opened re-publishes it (src/slack-bot/app.py). It currently shows only the non-admin help — there is a natural place to add an admin section.
  • Interactivity is wired. settings.interactivity.is_enabled: true, and the bot already registers @app.action(...) handlers (pickup/swap buttons). Adding @app.action(...) for admin buttons and @app.view(...) for modal submits is the same mechanism — no new hosting, scope, or reinstall is required for modals (views.open/view_submission ride the existing interactivity URL).
  • All mutations already have typed methods on the Schedule class (set_override, mark_open, remove_override, add_roster_entry, remove_roster_entry, rename_roster_entry, create_holiday, remove_holiday). Modals and a web API would both call the same methods, so the authorization and side-effect logic does not get duplicated.

Options considered

Add Block Kit modals (views.open) for the error-prone, high-value actions, an admin section in App Home, and keep the text subcommands as a fallback.

  • Pros: reuses existing interactivity + App Home; no new infra, auth, hosting, or OAuth scope; native date pickers / dropdowns / numeric inputs remove the positional-arg footguns; a confirmation preview before same-day 3CX repoints; one deploy target; the modal handlers call the same Schedule methods the text commands do, so authorization stays in one place.
  • Cons: still lives in Slack; modal layout is constrained (no true calendar/grid — an upcoming list is the practical "overview"); a small amount of new view-state plumbing.

(b) Full admin web UI

A separate hosted app (following the internal-portal pattern: Google OAuth, CDK-hosted, SHOC design system) talking to the schedule DynamoDB table through a new API.

  • Pros: full calendar/grid, bulk edits, the richest UX; not constrained by Block Kit.
  • Cons: significant effort and ongoing cost — a new auth path (Google OAuth), hosting (CDK), an API surface in front of DynamoDB, a second deploy target, and a security / cross-family review. It also forks the authorization model: get_admin_users() holds Slack user IDs, so a Google-OAuth UI needs an identity mapping (Google account → Slack admin) to keep one source of truth. Likely overkill unless admin volume is high.

(c) Status quo — keep text-only commands

  • Pros: zero work.
  • Cons: every pain point above persists; the holiday add arg order keeps biting.

Recommendation: phased — (a) now, (b) only if admin volume grows

Adopt (a) as phase 1 and treat (b) as a deferred phase 2 gated on real demand. Rationale:

  • Admin actions are low-frequency and well-bounded. Overrides, opens, clears, roster edits, and a handful of holidays a year are individually quick. The cost today is correctness/discoverability per action (getting the positional args right), not throughput — which is exactly what modals fix, and which a web UI fixes at far greater cost.
  • (a) reuses everything; (b) builds a new stack. The Slack-native path needs no new hosting, scope, auth, or review and lands a large UX win for the effort of a few modals plus an App Home section. (b) is a new auth + hosting + API + review surface for a marginal gain at current volume, and it splits the authorization model.
  • (a) does not block (b). If admin volume later justifies a calendar/grid or bulk edits, the Schedule methods and get_admin_users() authorization are already the clean seam a web API would sit on. Phase 1 is not throwaway.

Either way, get_admin_users() stays the single authorization source of truth, and the text commands stay as a fallback so no current workflow breaks.

Scope of phase 1 (option a) — for the follow-up build

The phase-1 build is intentionally not in this PR. Scoped here so the follow-up issue can be sized:

Modals to build (views.open + view_submission)

Start with the two highest-pain actions, then optionally extend:

  1. Override modal — replaces/supplements admin override.
    • datepicker for the date, a roster dropdown (static_select built from Schedule.get_roster() so the extension/name can't be mistyped), and a day/night radio_buttons/static_select.
    • A confirmation line in the submit response when the date is today (it will repoint 3CX).
  2. Holiday-add modal — replaces/supplements admin holiday add (the worst positional offender).
    • datepicker (date), number_input/plain-text slots, an explicit multiplier input (its own field instead of a parsed x<mult> token), and a label input.
    • view_submission validates (date not in the past, slots ≥ 1) and surfaces errors as Block Kit field errors instead of a usage string.

Optional follow-ons once the pattern is proven: open, clear, and roster add/remove/rename modals. These are lower-pain (fewer args) so they can stay text-only initially.

Handlers / wiring

  • A new @app.action(...) per admin button to call client.views_open(...) with the modal view (built by new build_*_modal() helpers in src/shared/shared/blocks.py, mirroring the existing build_* block builders).
  • A new @app.view(...) per modal callback_id to read view.state.values, re-check is_admin against get_admin_users() (modals can be opened from Home — never trust the surface), call the same Schedule method the text command uses, run the same 3CX/announce side effects, and ack() (with field errors on validation failure).
  • Reuse _refresh_schedule_post(...) after a successful mutation, exactly as the text handlers do.

App Home admin section

  • In build_home_view(), when the viewer is an admin (look them up via get_admin_users() before publishing), append an Admin section: buttons that open the modals above, plus a compact upcoming overrides + holidays list (the practical substitute for a calendar/grid).
  • publish_home(...) already runs on app_home_opened; it needs the viewer's user id (it has event["user"]) to decide whether to include the admin section.

Help / validation

  • Keep build_help_blocks(is_admin=True) as the text fallback reference, and point it at the new modals ("or open the Admin tab in Home").

Manifest / scope impact

  • None. Modals and App Home interactivity use the already-enabled interactivity request URL and the existing app_home config. No new OAuth scope and no reinstall are required (unlike the pins:write change in the sibling #135 work).

Scope of phase 2 (option b) — only if later justified

Recorded so the deferral is a decision, not an omission:

  • Auth: Google OAuth following the internal-portal pattern, plus a Google → Slack-admin identity mapping so get_admin_users() remains the source of truth (do not introduce a parallel admin list).
  • Hosting: CDK-hosted app on the SHOC design system, a second deploy target alongside the SAM stack.
  • Data path: a thin API in front of the schedule DynamoDB table that calls the same Schedule methods (no direct table writes from the browser).
  • Reviews: security / cross-family review for the new public surface and auth path.

Decision needed

Recommendation is (a) now, (b) later if volume grows. The build route — ship phase 1 (Slack-native modals + App Home admin), commit to the full phased path, or jump straight to a web UI — is left for sign-off on the issue/PR before any admin-surface code is written.