Docs / Admin
Feedback, bug, and friction intake
Summary — Staff and leads capture bugs, friction, blocks, and other operational gaps as feedback reports. Each report moves through a fixed status machine, gets an automated triage suggestion, and — once triaged — is manually routed by a lead into a change request, an incident, or a workflow block.
Purpose — Understand the feedback report lifecycle, the fields it captures, how the automated triage suggestion is computed, and what each routing outcome actually creates downstream.
Audience — Staff can create and view reports. Lead (or admin) roles perform all triage, classification, and resolution actions.
Prerequisites — None. This is a platform-level governance tool, not tied to a specific business module (though every report is tagged to one via moduleScope).
Overview
Key concepts:
- Feedback report — A single intake record: a bug, friction point, workflow block, missing requirement, automation gap, data-quality issue, or privacy risk.
- Feedback status machine — 10 statuses, transitions centralized in
state-machine.ts(never duplicated in the UI, per project rule 15). - Automated triage suggestion — A read-only recommendation (
suggestTriage()) shown while a report awaits routing. Nothing auto-executes; a lead must click the corresponding action. - Workflow block — A separate record type for something actively blocking an operational flow (e.g., a stuck consultation), with its own 6-status lifecycle.
- Friction signal — An aggregated counter (abandoned forms, repeated validation errors, etc.) surfaced on the dashboard, not an individually actionable record.
- Routing outcomes — Triaged reports link to a real
change_requestsorops_incidentsrow in the ops-control module, not just a status label.
Screenshot: Admin → Feedback dashboard showing "Open by type", "Aging by severity", "Affected modules", and "Validation failures" stat tiles above the filterable report list.
Feedback report fields
Captured via createFeedbackReportSchema (Zod, src/lib/modules/feedback/validators.ts):
| Field | Constraint | Notes |
|---|---|---|
feedbackType | required, enum | See Feedback types table below |
title | required, 1–500 chars | |
summary | required, 1–5000 chars | |
source | optional, max 100 chars | Free text — where the report came from |
environment | optional, max 100 chars | e.g. production |
moduleScope | required, 1–100 chars | Which module/area is affected |
routePath | optional, max 500 chars | The URL where it happened |
entityType / entityId | optional (entityId must be a UUID) | Links the report to a specific record |
workflowStep | optional, max 200 chars | |
severity | optional, enum (low/medium/high/critical) | |
reproducibility | optional, max 2000 chars | |
privacyClassification | optional, max 100 chars | |
evidenceLinks | optional, array of strings (max 1000 chars each) | Split from newline-separated textarea input in the create form |
Feedback types
| Value | Label shown in UI |
|---|---|
bug | Bug |
performance | Performance |
friction | Friction |
block | Block |
missing_requirement | Missing Requirement |
automation_gap | Automation Gap |
data_quality | Data Quality |
privacy_risk | Privacy Risk |
The feedback status machine
Statuses (FeedbackStatus): new, classified, triaged, linked_to_change_request, linked_to_incident, linked_to_workflow_block, mitigated, resolved, closed, rejected.
Allowed transitions (FEEDBACK_TRANSITIONS in state-machine.ts):
| From | Allowed to |
|---|---|
new | classified |
classified | triaged |
triaged | linked_to_change_request, linked_to_incident, linked_to_workflow_block |
linked_to_change_request | mitigated, resolved |
linked_to_incident | mitigated, resolved |
linked_to_workflow_block | resolved |
mitigated | resolved |
resolved | closed |
closed | — (terminal) |
rejected | — (terminal) |
rejected is reachable from any non-terminal status — any report in new through resolved can be rejected (ACTIVE_FEEDBACK_STATUSES includes everything except closed/rejected). closed and rejected are the only two terminal statuses.
The detail page (/admin/feedback/[id]) only renders an action button for a next status if TRANSITION_ACTIONS has an entry for it — every status in the table above has a corresponding button ("Classify", "Mark Triaged", "Create Change Request", "Open Incident", "Mark Mitigated", "Resolve", "Close", "Reject"), except linked_to_workflow_block, which opens an inline form instead of a single-click button.
Automated triage suggestion
Shown as a blue info box on the detail page while report.status is new, classified, or triaged. Computed by suggestTriage() (src/lib/modules/feedback/triage-rules.ts) — a suggestion only; a lead must still click the matching action. auto_mitigate is deliberately never suggested — fast mitigations require a human to choose them.
| Condition | Suggested outcome | Reason shown |
|---|---|---|
| An open report with the same title + module scope already exists | reject_as_duplicate | "An open report with the same title and module scope exists" |
feedbackType = privacy_risk | create_incident | "Privacy risks always route to incident handling" |
feedbackType is bug or performance, severity is high/critical, and environment = production | create_incident | "{severity} severity with live production impact" |
feedbackType is bug or performance (otherwise) | create_change_request | "Routes to governed engineering work" |
feedbackType = block | create_workflow_block | "Blocks route to operational owner resolution" |
feedbackType = missing_requirement | request_client_input | "Missing requirements need phase-input review and client input" |
feedbackType = friction, severity low | route_to_backlog | "Low-severity friction routes to the product backlog" |
feedbackType = friction (otherwise) | create_change_request | "Recurring or impactful friction routes to engineering work" |
feedbackType = automation_gap | create_change_request | "Automation gaps route to an automation change request" |
feedbackType = data_quality | create_change_request | "Data quality issues route to the data correction queue" |
Note: request_client_input and route_to_backlog are suggestion-only outcomes — there is no corresponding status transition or downstream record creation for them in the current code; they surface as guidance text only.
What each routing action actually creates
Link to change request
Action: "Create Change Request" button (available from triaged).
Only allowed for feedbackType in bug, performance, friction, automation_gap, data_quality (CHANGE_REQUEST_TYPES). Attempting it on any other type throws Feedback type '{type}' does not route to a change request.
Calls createChangeRequest() in the ops-control module, creating a real change_requests row with requestType set to performance if the feedback type is performance, otherwise bug — carrying over title, summary, severity, moduleScope, privacyClassification, and requestSource: "feedback". The feedback report then transitions to linked_to_change_request with linkedChangeRequestId set to the new change request's ID.
Link to incident
Action: "Open Incident" button (available from triaged).
Only allowed if feedbackType = privacy_risk, or severity is high/critical. Otherwise throws Incident linking requires privacy risk or high/critical severity.
Calls createOpsIncident() in ops-control — incidentType is set to privacy if the feedback type is privacy_risk, otherwise reliability; detectedFrom: "feedback". Transitions to linked_to_incident with linkedIncidentId set.
Link to workflow block
Action: "Create Workflow Block" (inline form, available from triaged) — requires blockedEntityType, moduleScope, workflowStep, blockReason (all required text inputs, pre-filled with values from the report where available), and an optional requiredInput.
Creates a WorkflowBlock record scoped to the report and transitions the feedback report to linked_to_workflow_block.
Workflow blocks
A workflow block represents something actively stopping an operational flow — separate lifecycle from the feedback report that spawned it (a block can also exist without a linked report).
Statuses (WorkflowBlockStatus): open, acknowledged, waiting_for_input, in_progress, resolved, closed.
Allowed transitions (WORKFLOW_BLOCK_TRANSITIONS):
| From | Allowed to |
|---|---|
open | acknowledged |
acknowledged | waiting_for_input, in_progress |
waiting_for_input | in_progress |
in_progress | resolved |
resolved | closed |
closed | — (terminal) |
Open blocks (status not closed/resolved) show on the feedback dashboard under "Open Workflow Blocks", with block reason, module scope, workflow step, owner role, and — if set — the required input needed to unblock it.
transitionWorkflowBlockAction requires staff role (lower bar than the lead role required for feedback report transitions), reflecting that operational owners resolving a block may not be leads.
Friction signals
A separate, lighter record type: an aggregated counter of a recurring UX friction pattern, not an individually triaged report.
Signal types (FrictionSignalType): abandoned_form, repeated_validation_error, manual_override, backtrack, long_idle_time, support_request, unclear_step.
Each signal tracks moduleScope, routePath, workflowStep, signalType, a running count, sampleActorRole, firstSeenAt, lastSeenAt. The dashboard's "Top Friction Signals" list shows up to 10, sorted as returned by listFrictionSignals(), and a dedicated "Validation failures" tile filters specifically for repeated_validation_error signals.
Viewing and filtering feedback
Path: Admin → Feedback (/admin/feedback)
Dashboard stat tiles (computed client-side over open/non-terminal reports, where terminal = resolved, closed, rejected):
- Open by type — count of open reports per
feedbackType, sorted descending. - Aging by severity — oldest open report's age in days, per severity level.
- Affected modules — top 5
moduleScopevalues by open-report count. - Validation failures — top 5
repeated_validation_errorfriction signals by count.
Filters: Quick-filter links for All, and status shortcuts new, triaged, resolved (via ?status=), plus one link per feedback type (via ?type=). Filtering is server-side through listFeedbackReportsAction/listFeedbackReports, driven entirely by URL search params — there is no combined status+type filter UI beyond what the query string supports.
Roles and permissions
| Action | Required role |
|---|---|
| Create feedback report | staff |
| List feedback reports | staff |
| Classify / Triage / Link to change request / Link to incident / Link to workflow block / Mark mitigated / Resolve / Close / Reject | lead |
| Transition a workflow block | staff |
All checks use requireRole(role, { module: "feedback" }). Admin implicitly satisfies any lower-role check per the role hierarchy.
Best practices
- Fill in
entityType/entityIdandroutePathwhen reporting a bug tied to a specific record. These fields drive pre-filled workflow-block forms and make the report actionable without re-asking the reporter for context. - Trust the triage suggestion but verify severity first. The incident-routing threshold (
high/critical+production) is a hard gate on the "Open Incident" action itself — if severity doesn't qualify, the action throws regardless of what the suggestion box says. - Use friction signals for volume, feedback reports for individual action. A single friction report per user session is noise; the friction-signal counters exist specifically so recurring UX pain shows up in aggregate instead.
- Close the loop. A report linked to a change request or incident isn't done until it reaches
resolved→closed— the linked ID alone doesn't reflect downstream completion.
Warnings
Routing actions are hard-gated, not just suggested. "Create Change Request" throws if the feedback type isn't in the allowed list; "Open Incident" throws if severity/type doesn't meet the threshold. The blue suggestion box does not override these checks.
closed and rejected are both terminal. Neither has any further allowed transition — there is no "reopen" path in the state machine.
request_client_input and route_to_backlog have no dedicated action or status. They are triage-suggestion labels only; acting on them today means a lead manually following up outside the feedback UI (e.g., contacting the client, or adding an item to a separate backlog tool).
Troubleshooting
"Feedback transition '{from}' → '{to}' not allowed"
Symptom: Server error when submitting a transition action.
Cause: The target status isn't in the allowed-transitions list for the report's current status (canTransitionFeedback returned false).
Fix: Check the current status against the transition table above. The detail page only renders buttons for legal transitions, so this typically only happens if the report's status changed between page load and submission — reload the page and retry.
"Feedback type '{type}' does not route to a change request"
Symptom: Clicking "Create Change Request" fails with this message.
Cause: The report's feedbackType is not one of bug, performance, friction, automation_gap, data_quality (e.g., it's block, missing_requirement, or privacy_risk).
Fix: Use the routing outcome that matches the type instead — block → workflow block, privacy_risk → incident. missing_requirement has no dedicated action; follow up manually per the "Request client input" suggestion.
"Incident linking requires privacy risk or high/critical severity"
Symptom: Clicking "Open Incident" fails with this message.
Cause: The report is neither privacy_risk type nor high/critical severity.
Fix: If the underlying issue genuinely warrants incident handling, first reclassify the severity (Classify action) to high or critical, then retry.
FAQ
Who can submit a feedback report?
Any user with staff role or higher.
What is the difference between a feedback report and a workflow block? A feedback report is the intake record describing what happened. A workflow block is a separate, focused record for something actively stopping an operational flow right now — it can be created from a feedback report or independently, and has its own status lifecycle.
Does creating a change request or incident from feedback actually build anything?
It creates a real row in the change_requests or ops_incidents tables (ops-control module) — not just a label on the feedback report. See Ops control and shift readiness for what happens to that record next.
Can a rejected report be reopened?
No. rejected (like closed) has no allowed forward transitions in the current state machine.
Why does the dashboard show a report as "open" differently from its literal status?
"Open" on the dashboard means any status that isn't resolved, closed, or rejected — it's a computed grouping for the stat tiles, not a status value stored in the database.
Related articles
- Roles and access — Who can do what across the platform.
- Ops control and shift readiness — What happens to change requests and incidents after feedback links to them.
- Audit log and GDPR data governance — Every feedback status change and routing decision is also written to
audit_events.