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

201 lines
9.9 KiB
Markdown
Raw Permalink Normal View History

# 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
### (a) Expand/clarify inside Slack — **recommended (phase 1)**
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.