Design: Admin Management Screens (SPEC-0011)
Context
joe-links has basic admin views (SPEC-0004): an admin dashboard with summary stats, a user list with inline role changes, and a links list showing all links. However, the admin experience is limited — link editing requires navigating to the user-facing edit form, user deletion uses a browser confirm() dialog with no link handling, and keyword management is minimal. As the service scales to more users and links, admins need more powerful management tools directly in the admin interface.
This spec enhances the admin screens with inline editing, proper confirmation modals, safe user deletion with link disposition choices, and a complete keyword management interface.
Related: SPEC-0004 (Application Views and Routing), SPEC-0002 (Link Data Model), 📝 ADR-0007 (Views and Routing), 📝 ADR-0005 (Data Model).
Goals / Non-Goals
Goals
- Enable admins to edit any link's URL, title, and description inline without leaving the admin links screen
- Provide admin link deletion with DaisyUI confirmation modals (not browser
confirm()) - Display link owner display names in the admin links table
- Complete keyword management: list, create with validation, and delete with confirmation
- Safe user deletion with explicit link disposition (reassign or delete)
- Admin self-deletion prevention
Non-Goals
- Bulk operations (select multiple links, bulk delete) — deferred to a future spec
- Admin audit log — would require a new table and is out of scope
- Link transfer between arbitrary users — only reassignment to the deleting admin is supported
- Keyword editing (update) — create and delete are sufficient for the initial release
Decisions
Inline Editing via HTMX Row Swap
Choice: Clicking "Edit" on an admin link row replaces the <tr> with an editable form row via HTMX hx-get targeting the same row ID. Saving swaps back the read-only row.
Rationale: Inline editing avoids page navigation, keeping the admin in context. The HTMX pattern is already established in the codebase (e.g., role changes in the users table use the same row-swap approach). Each row has a unique id="link-row-{id}" attribute that serves as the HTMX swap target.
Alternatives considered:
- Modal edit form: more disruptive; the admin loses visual context of the row's position in the table
- Navigate to
/dashboard/links/{id}/edit: breaks the admin workflow by redirecting to the user-facing edit form
DaisyUI Modals for All Destructive Actions
Choice: All destructive admin actions (delete link, delete keyword, delete user) use DaisyUI modal components with explicit confirm/cancel buttons.
Rationale: Browser confirm() dialogs are not styleable, don't support rich content (like link counts or disposition choices), and feel jarring in a polished UI. DaisyUI modals integrate with the existing theme system (SPEC-0003) and can display contextual information like the link slug, user email, and link counts.
User Deletion with Link Disposition
Choice: The user deletion modal presents two radio options: "Reassign links to me" (admin absorbs the links) or "Delete all links" (cascade delete). The choice is sent as a link_action parameter.
Rationale: Deleting a user who owns links creates an orphan problem — links without owners become unreachable from the dashboard. The two-choice approach covers the most common admin scenarios: (1) the user left the team but their links are still valuable (reassign), or (2) the user was a test account or their links are obsolete (delete). Reassignment always goes to the performing admin because they are the one making the judgment call about the links' value.
Alternatives considered:
- Reassign to any user (dropdown): adds UI complexity and requires the admin to know which user should receive the links
- Always cascade delete: risks losing valuable links when offboarding team members
- Block deletion if user has links: frustrating — forces the admin to manually reassign or delete each link first
Admin-Specific Link Endpoints
Choice: PUT /admin/links/{id} and DELETE /admin/links/{id} are separate endpoints from the user-facing PUT /dashboard/links/{id} and DELETE /dashboard/links/{id}.
Rationale: Admin endpoints live in the admin middleware group (RequireRole("admin")) and return admin-specific HTMX fragments (table rows, not detail page content). Sharing endpoints would require branching on role inside handlers and producing different HTML based on the request source. Separate endpoints keep each handler focused.