Files
EpicNext-Cms/docs/superpowers/specs/2026-09-01-housekeeping-content-polls-vertical-design.md
T
Simo aa7f263e8a
CI / runtime-diagnostics (pull_request) Skipped
CI / check (pull_request) Successful in 37s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
docs(housekeeping): design content polls vertical
2026-09-01 21:36:35 +02:00

7.1 KiB

Housekeeping Content Polls Vertical Design

Date: 2026-09-01

Status: Approved

Objective

Replace the generic ASE poll command forms with a dedicated operator vertical for finding, creating, editing, deleting, and analysing polls. The workflow must be complete inside ASE, preserve the existing public voting behaviour, and keep the legacy /admin/polls pages available as a stable fallback until the wider Housekeeping cutover is approved.

Scope

The vertical covers these canonical routes:

  • /ase-next/content/engagement/polls
  • /ase-next/content/engagement/polls/create
  • /ase-next/content/engagement/polls/:id

It includes poll search and status filtering, bounded pagination, schedule and participation summaries, prefilled create/edit forms, question and option administration, aggregate choice results, paginated free-text responses, and reason-protected destructive actions.

Prefixes, help content, and the public /polls routes remain unchanged. The existing database schema and runtime dependencies remain unchanged.

Data Contract

Poll routes expose route-specific typed private payloads through the existing ContentQueryItem.privatePayload boundary:

  • poll list items carry title, status, visibility flags, schedule, question count, distinct voter count, answer count, and update time;
  • poll create needs no operator-selected foreign-key data and returns a typed empty create payload;
  • poll detail returns exactly one selected poll, its ordered questions and options, aggregate choice results, and one bounded page of individual free-text responses;
  • each individual free-text response carries the vote identifier, user identifier, nullable username, answer, and submission time so a deleted or unavailable user record cannot break the result view.

The query contract validates every route-specific payload and fails closed when a production adapter returns malformed, partial, or mismatched data. Poll detail is always selected by the canonical route parameter; its returned ID and cardinality must match the request. IDs are positive decimal strings and timestamps are canonical UTC ISO values.

List filtering accepts a bounded search string, an explicit status value, and a positive page number. Detail free-text pagination is scoped to a selected text question and positive response page. Normal list pages use the existing bounded page size. Poll detail has fail-closed caps of 100 questions and 100 options per question. Choice totals come from aggregate queries; newline-delimited multiple-choice combinations are expanded using their aggregate weights, and raw choice vote rows are never exposed in the ASE payload. Individual free-text responses use a maximum page size of 50 and expose an exact total for navigation.

Operator Experience

The dedicated Polls list provides a visible create action, title search, status filter, result totals, schedule state, question and participation statistics, and previous/next navigation. Operators never need to copy numeric poll IDs.

The create screen covers title, description, status, public-results visibility, the existing poll-level multiple-choice compatibility flag, and optional start/end times. Browser-native date/time inputs are displayed and submitted as UTC, then normalized to canonical ISO strings. End time must be later than start time. Question type remains the canonical source of public vote semantics; the poll-level compatibility flag must not change the established public handling of single-choice, multiple-choice, or text questions.

The detail screen is a dedicated workflow split into Overview, Questions, and Results sections. Overview prepopulates every poll field and shows operational statistics. Questions binds the current poll ID automatically and supports create, prefilled update, ordering, and deletion. Results shows per-option counts and percentages for choice questions, plus a paginated operator-only table of individual text responses with username, user ID, and submission time. A missing username is rendered as an explicit unavailable-user label while retaining the immutable user ID.

Question validation is type-aware:

  • single-choice and multiple-choice questions require at least two non-empty, unique options;
  • text questions accept no options and persist an empty option collection;
  • a poll can contain no more than 100 questions and a question no more than 100 options;
  • an existing question cannot be reassigned to another poll through update input;
  • option order is stable and follows the operator-entered order.

Poll and question deletion use dedicated command IDs that require an audit reason and cannot be bypassed through the generic change commands. Question deletion removes its votes before the question. Poll deletion removes all votes and questions before the poll, inside the existing mutation transaction. Non-destructive create and update commands keep their current confirmation behaviour.

Dependency Decision

No new runtime dependency is needed. Existing React, Zod, Drizzle, browser date/input APIs, and ASE table/form primitives cover the workflow with a smaller security and maintenance surface. Any later dependency proposal must demonstrate a concrete accessibility, correctness, or maintenance benefit before adoption; Knip remains the dependency/source audit for this vertical.

Permissions and Failure States

polls.view can read the poll list and poll results. polls.edit is required for create, detail administration, and every mutation. Operators with view-only access can inspect results but do not receive editable controls.

Each route preserves explicit loading, forbidden, dependency-error, empty, not-found, validation-error, conflict, and ready states. A genuine unknown poll returns the dedicated not-found state; malformed payloads and unavailable dependencies must not be presented as 404s. Field validation appears next to the relevant control while command-level conflicts remain visible without discarding the operator's entered values.

Public Compatibility

The public poll list, detail, voting submission, and result-visibility rules are not redesigned by this vertical. Existing question-type semantics remain authoritative, showResults continues to control public aggregate visibility, and ASE-only free-text identity data is never included in public payloads. Regression coverage must prove that the new operator queries and mutations do not broaden public access or change accepted vote formats.

Verification

The vertical is complete only when typed query-contract tests, production-adapter tests, command-policy tests, database-mutation tests, component rendering and interaction tests, public poll regression tests, the Housekeeping matrix, TypeScript, Biome, Knip, and the production build all pass.

Coverage must include list filtering and pagination, create/edit date validation, all three question types, duplicate/insufficient options, stable option order, question ownership, choice aggregates, paginated free-text responses, view-only permissions, reason enforcement, explicit child deletion, fail-closed collection limits, malformed payloads, and true not-found behaviour. The draft PR description is updated in English and Dutch after verified implementation.