Conversational IT support automation for organisations. Employees describe an issue in plain language; the agent classifies it, opens and tracks a ticket, walks through curated troubleshooting where available, and hands the case to IT staff when human attention is needed.
This is a B.Sc. (Hons) Computer Science Final Year Project at Asia Pacific University (APU): Designing Artificial Intelligence Help Desk Agent for Organisational IT Support Automation.
Project status: active development. Feature 004 — account authentication, the two-role model, staff dashboard, assignment, profiles, settings, and Excel import workflows — is implemented and its quality gates pass. Feature 005 — constrained automated remediation against a whitelisted set of read-only and state-changing actions, with consent, staff approval, an immutable audit trail, a kill switch, and outcome metrics — is implemented and its quality gates pass. Interfaces may change before the project is finalised.
Reporting and troubleshooting
- Natural-language IT issue reporting with automatic ticket creation and a quotable reference such as
HD-0012. - Six seeded support categories — password/login, network, printer, peripherals, performance, and service status — plus
unclassifiedas the safety fallback. Categories are stored in the database, not hardcoded: a maintainer can add, edit, or retire one from the maintainer console at/maintainerand classification picks it up without a code change. - Deterministic, versioned troubleshooting guides. The language model interprets the user’s reply, but never invents, reorders, or skips a troubleshooting step.
- Clarification and escalation safeguards: uncertain classifications, missing guides, and explicit requests for a person all preserve the case for staff rather than silently guessing.
- Live chat and ticket updates through Server-Sent Events (SSE).
- Optional local speech-to-text input; audio is transcribed locally and discarded after transcription.
Accounts and roles
- Two account roles:
user(default for every registration) andstaff. Registration, sign-in, sign-out, password changes, and role-gated routes are implemented on both the API and the SPA. - Sessions are opaque server-side tokens carried in an HTTP-only cookie. Changing a password invalidates every other session for that account.
- The
staffrole is granted only by the maintainer-runseed:staffscript. No HTTP endpoint, including the staff and maintainer surfaces, can promote an account.
Employee self-service
- Personal ticket history at
/tickets, scoped to the signed-in account, with a per-ticket detail view and its own SSE stream. - A self-service support profile (remote-access IDs, location, hardware notes) that staff see on escalated tickets, plus an account settings page. Every field names who last set it and when; a field IT staff have set is shown read-only with the reason stated on the field itself, and the rest stay self-service.
Staff workspace
- A staff-only ticket dashboard with status/category filters, sorting, and a separate escalated-ticket group.
- Staff ticket detail with the conversation, classification context, status history, permitted status changes, takeover, reassignment, and any available reporter support profile.
- Staff availability controls and advisory workload-aware assignment suggestions. Assignment always requires a deliberate staff confirmation.
- An account directory at
/staff/accountsthat reaches any account by name or email, including one that has never raised a ticket. - Staff-authoritative profile editing: a value staff save on
location,hardware, or the remote-access list becomes the account's value everywhere the profile is shown, carrying who set it and when. Control of that field passes to staff until a staff member releases it back, the previous values stay readable in a staff-only field history, and two staff saving at once conflict per field rather than per profile. Free-text staff notes and an initial-password reset that revokes that account's sessions remain available alongside. - Excel (
.xlsx) user import with column mapping, a dry-run preview, and a transactional apply step. Every staff action is written to an append-onlyStaffActionRecordaudit trail.
Maintainer administration
- A maintainer console at
/maintainerfor the category and guide administration that previously existed only as an API: create a category with its first guide, reword a classification description, publish a new guide version with a change note, read the attributed version history, and retire a category that is no longer used. The six mandated categories offer no retire action at all. - The console is not a role and not an account. It is a shared
MAINTAINER_KEYplus a self-declared name, held in memory only for as long as the page is open, sent per request, and never written to browser storage. Nothing links to the console from anywhere in the application, and it can reach no ticket, conversation, account, or role. - Repeated wrong keys are throttled from the same client for a configurable cooling-off period, checked before the key is compared. Every refused attempt is recorded with a timestamp and a hashed client identity; the supplied key is never stored.
Constrained automated remediation
- A whitelisted set of read-only and state-changing diagnostic actions, defined as versioned policy data rather than executor code, each scoped to a specific issue category and a specific test endpoint.
- Every proposed action is matched against that whitelist by a default-deny policy engine before anything runs. There is no code path from an LLM proposal straight to execution.
- Read-only actions execute the moment the reporter consents; state-changing actions additionally wait on a staff approval decision. A decline, an expiry, or a policy mismatch is refused and recorded, never a silent no-op or a hard error.
- Every executed and every refused action is written to an append-only
ActionRecord, visible per-ticket to the reporter (plain language) and cross-ticket to staff (full detail, filterable) at Audit trail (/staff/audit). - An asymmetric kill switch — global or per-endpoint — at Automation (
/staff/remediation) lets staff disable remediation instantly; a disabled state is visible everywhere it matters and blocks new proposals immediately. - An ordered LLM provider fallback chain: if the primary provider is unavailable, the request retries against the next configured provider. No action is ever executed on a classification produced while the model was in a degraded fallback state — that proposal is refused and audited instead of offered.
- Outcome metrics (attempted/succeeded/refused/failed, by category and endpoint) for a selectable period at Metrics (
/staff/metrics), including an explicit no-data state.
The application does not execute arbitrary commands. Automated remediation is limited to a small, versioned whitelist of actions (backend/src/policy/action-policy.json) against dedicated, isolated test endpoints (backend/test-endpoints/) — never against employee devices or production infrastructure. Every action is default-denied unless it matches the whitelist exactly; read-only actions still require reporter consent, and state-changing actions additionally require staff approval before anything runs. The feature is off by default (REMEDIATION_ENABLED=false) and can be disabled globally or per-endpoint at any time from the staff automation kill switch. LLM output is treated as untrusted input throughout: it is schema-validated, and a returned category is accepted only if it matches an active category in the database — anything else falls back to unclassified and escalates. Ticket transitions are validated and recorded in append-only history.
Staff-only endpoints require both an authenticated session and the staff role; no HTTP endpoint can grant that role. The maintainer admin surface is a separate axis: it is mounted only when MAINTAINER_KEY is configured, and is protected by a constant-time key comparison rather than by a session — it does not touch accounts or roles at all.
Only information required to support a case is stored: account details, session information, the reported issue, ticket context, and optional support-profile data. Local voice audio is not retained after transcription.
backend/ Express API (TypeScript, strict)
├── src/
│ ├── api/
│ │ ├── middleware/ Validation, session/role guards, maintainer key, errors
│ │ ├── routes/ Auth, chat, tickets, own tickets/profile, staff tickets,
│ │ │ staff roster/users/imports, staff actions/approvals/
│ │ │ remediation/metrics, admin guides, health
│ │ └── sse/ Reporter, own-ticket, and staff event streams
│ ├── models/ Mongoose schemas: accounts, auth sessions, tickets,
│ │ conversations, categories, guides, support profiles,
│ │ profile imports, staff-action records, action records,
│ │ approval requests, remediation settings, provider
│ │ fallback events
│ ├── policy/ Versioned action whitelist and endpoint registry
│ │ (action-policy.json, test-endpoints.json) plus schema
│ │ validation and the runtime loader
│ ├── services/
│ │ ├── llm/ Ollama, OpenAI-compatible, mock, and chained
│ │ │ (ordered-fallback) providers — the only module
│ │ │ permitted to call a model directly
│ │ ├── classification/ Category classification against the active category set
│ │ ├── conversation/ Chat orchestration and guided troubleshooting
│ │ ├── guidance/, guide/ Step interpretation and versioned guide administration
│ │ ├── category/ Runtime category registry
│ │ ├── escalation/ Escalation rules and reasons
│ │ ├── ticket/ Ticket lifecycle, history, and notifications
│ │ ├── auth/ Password hashing and opaque session management
│ │ ├── session/ Chat-session lifecycle and inactivity expiry
│ │ ├── profile/ Self-service and staff-appended support profiles
│ │ ├── import/ Excel parsing, mapping, preview, transactional apply
│ │ ├── staff/ Dashboard queries, takeover, reassignment, roster
│ │ ├── stt/ Local and OpenAI-compatible speech-to-text
│ │ ├── agent/ Tool-calling loop and remediation tool definitions
│ │ ├── remediation/ Default-deny policy engine, SSH executor, consent,
│ │ │ approvals, availability (kill switch), audit
│ │ └── metrics/ Outcome metrics aggregation
│ └── scripts/ Guide/category and staff-account seeding
├── test-endpoints/ Isolated Dockerised SSH endpoints remediation runs
│ against (never employee devices), plus the reset
│ and host-key-capture scripts used before a demo
└── tests/ Vitest + Supertest integration and unit tests
frontend/ React + Vite + Tailwind CSS SPA
├── src/
│ ├── context/ Authentication state
│ ├── components/ Navigation, route guards, dashboard/assignment/profile UI
│ │ └── staff/ Approval queue, audit trail, metrics band/summary,
│ │ remediation (kill switch) controls, ticket list
│ ├── pages/ Chat, login, registration, my tickets, profile, settings
│ │ └── staff/ Dashboard, ticket detail, user profile, Excel import,
│ │ approvals, audit, remediation (kill switch), metrics
│ ├── lib/ Shared types
│ └── services/ Typed API client and SSE subscriptions
└── tests/ Testing Library component/page tests
specs/ Feature specifications, plans, tasks, and API contracts
docs/ Design diagrams, test traceability, implementation evidence
| Concern | Choice |
|---|---|
| Language | TypeScript 5 (strict) on Node.js 20+ |
| Backend | Express, Mongoose, Zod, Pino |
| Frontend | React, Vite, Tailwind CSS |
| Database | MongoDB Community Edition |
| LLM runtime | Ollama, OpenAI-compatible server, or deterministic mock, in an ordered fallback chain |
| Realtime | Server-Sent Events |
| Remediation | ssh2 against a versioned action whitelist and endpoint registry |
| Testing | Vitest, Supertest, mongodb-memory-server, Testing Library |
| Requirement | Check |
|---|---|
| Node.js 20+ | node --version |
| MongoDB | mongosh --eval "db.runCommand({ping:1})" |
| LLM runtime (optional for mock-backed development) | ollama pull llama3.1:8b |
| Docker Desktop with the WSL2 backend (only if exercising remediation) | docker compose version |
Feature 004's Excel Import Apply operation uses a MongoDB transaction. Run the
local demo database as a single-node replica set (not a standalone mongod):
docker run -d --name helpdesk-mongo -p 27017:27017 -v helpdesk-mongo-data:/data/db mongo:7 --replSet rs0 --bind_ip_all
docker exec helpdesk-mongo mongosh --quiet --eval "rs.initiate({_id: 'rs0', members: [{_id: 0, host: '127.0.0.1:27017'}]})"
docker exec helpdesk-mongo mongosh --quiet --eval "db.hello().isWritablePrimary"The final command must print true before starting the backend. The reference
MONGODB_URI includes ?replicaSet=rs0; use the same suffix for a locally installed
MongoDB service after starting it with --replSet rs0. Existing standalone databases
can still run ordinary chat and dashboard flows, but Import Apply deliberately returns
MongoDB code 20 because atomic imports are not safe without transactions.
Automated remediation targets a pair of isolated, disposable Docker containers rather than any real machine. Bring them up (and reset them to a known baseline, e.g. before a demo) with the single reset script, which also generates the SSH client keypair and captures each container's host key fingerprint into the pinned endpoint registry:
powershell -File backend/test-endpoints/reset.ps1Set REMEDIATION_SSH_KEY_PATH and REMEDIATION_ENABLED=true as printed by the script
(see Configuration). Skip this step entirely if you only want the
chat/dashboard/import feature set — remediation is off by default.
git clone https://github.com/6rzan/ai-helpdesk-agent.git
cd ai-helpdesk-agent
cd backend; npm install; cd ..
cd frontend; npm install; cd ..
# Optional: the reference configuration has sensible defaults.
Copy-Item .env.example backend/.env
# Terminal 1: API on http://localhost:3000
cd backend; npm run dev
# Terminal 2: SPA on http://localhost:5173
cd frontend; npm run devOpen http://localhost:5173. The health endpoint is available at http://localhost:3000/api/health.
| Role | How it is obtained | What it unlocks |
|---|---|---|
user |
Assigned automatically to every registration. POST /auth/register hardcodes it and ignores any role supplied in the request body. |
Chat, own ticket history, own support profile, account settings |
staff |
The maintainer-run npm run seed:staff script only. |
Everything a user can do, plus /staff dashboard, ticket detail, takeover/reassignment, roster, the account directory, authoritative profile editing on any account, per-field history, credential resets, and Excel import |
| maintainer | Not an account. A shared MAINTAINER_KEY sent as a request header, entered with a display name in the console at /maintainer. No session is created and the key is never stored in the browser. |
Category and guide administration at /api/maintainer — cannot read tickets, conversations, or accounts, and cannot alter roles |
- Register an account or sign in at
/registeror/login. - Open the chat and describe one IT problem in everyday language.
- Follow the offered troubleshooting steps, ask for a person, or ask for a ticket update. Status changes appear in the conversation without a page refresh.
- When the agent proposes a whitelisted diagnostic action, it explains in plain language what will run and where, and waits for explicit consent in the chat before doing anything. Declining is always safe: it is refused and recorded, never a hard error. Read-only actions run the moment consent is granted; state-changing actions additionally wait on a staff approval decision, and the reporter is told which is happening.
- Review past cases under My tickets (
/tickets), which lists only tickets reported by the signed-in account. Each ticket's detail view includes its own action history: every attempted action and its outcome, in plain language. - Fill in the support profile at
/profile— remote-access tool IDs, location, hardware notes — so staff have that context on escalation. Change the account password at/settings.- Each field shows who last set it and when. If IT staff have set a field, it is shown read-only with a one-line explanation on the field itself: staff hold it, and asking in the chat is how to get it corrected or handed back. Fields staff have not touched stay self-service exactly as before.
The conversation flow remains deliberately conservative. If the agent cannot confidently classify the issue, a guide is unavailable, or the user asks for staff, it escalates rather than improvising.
Staff accounts are created only by a maintainer-run backend script. The script creates a new staff account with a generated initial password, or promotes an existing account to the staff role.
cd backend
npm run seed:staff -- staff@example.com "Support Engineer"The generated initial password is written to the backend log. Share it through a secure channel; the staff member should change it after first sign-in. No browser API can assign the staff role.
- Sign in with a provisioned staff account and open Dashboard (
/staff). - Filter tickets by status or category, choose a sort order, and check the amber escalated group first.
- Open a ticket to review the transcript, classification, ticket history, and reporter profile when one exists.
- Take over an unassigned escalation, or choose a roster colleague and explicitly confirm reassignment. The suggested assignee is advisory only.
- Update the ticket status. The service records the staff action and sends a plain-language update to the reporter’s conversation.
Staff can also set their availability to available, busy, or away in the top navigation. Roster entries expose availability and current open-case counts to make reassignment decisions visible.
-
Decide state-changing action requests at Approvals (
/staff/approvals): approve to execute, or decline with an optional reason. Both surfaces update live for every open staff tab the instant anyone decides. -
Review every attempted and refused action across all tickets, filterable by ticket, endpoint, and outcome, at Audit trail (
/staff/audit). It is append-only: there is no edit or delete affordance anywhere in this surface. -
Turn automated remediation off — globally or for one endpoint — at Automation (
/staff/remediation). The toggle takes effect immediately and blocks new proposals across every session. -
Review attempted/succeeded/refused/failed action counts by category and endpoint, for a selectable period, at Metrics (
/staff/metrics). -
Reach any account — not only reporters of an open ticket — at Accounts (
/staff/accounts). Search by part of a name or email and open that person's profile directly, including an account that has never raised a ticket.
On a profile page (/staff/users/:accountId/profile) staff set the location, hardware specification, and remote-access entries authoritatively: what staff save becomes the profile's value everywhere it is shown, including on the owner's own /profile page, rather than a note sitting beside a stale entry. Each field carries who last set it and when, and the previous values stay readable in a staff-only history behind a disclosure. Once staff set a field the owner can no longer edit it — the field stays visible to them with a plain explanation of why and how to get it changed — until a staff member releases it back. The remote-access list counts as one field: it is locked, released, and recorded as a whole. Two staff saving at once conflict per field, so an edit to a field nobody else touched still saves, and the one that moved underneath is refused by name with the value it would have overwritten. Free-text notes and any corrections recorded before this behaviour shipped remain readable exactly as they were. Staff can also issue a new initial password from this page — which immediately revokes that account's sessions and forces a change at next sign-in. Bulk profile data is loaded through Import (/staff/import): upload an .xlsx workbook, map its columns, review the dry-run preview, then apply. Apply runs in a MongoDB transaction and therefore requires a replica-set deployment (see Prerequisites). Every one of these actions is recorded in the staff-action audit trail.
Category and guide administration has its own screen at /maintainer. It is deliberately unlinked: no navigation anywhere in the application points at it, and it renders outside the application shell, because it is not a role an account can hold.
- Set
MAINTAINER_KEYinbackend/.envand restart the backend. - Open
/maintainer, and enter the key together with your own name. The name is what the guide version history attributes changes to. - The category list shows every category with its classification description, whether it is one of the six mandated ones, whether it is retired, and its active guide version.
- Add a category with its first guide, reword a description so classification picks up new phrasing, publish a corrected guide version with a change note, or read the version history. A rejected guide reports the problem on the step and field at fault.
- Retire a category that is no longer used. The confirmation states the consequence first: existing tickets keep the category, only future classification stops. The six mandated categories offer no retire action at all.
The console refuses in three distinct ways, and each says which it is: a wrong key, administration switched off (MAINTAINER_KEY unset — no sign-in form is shown at all), and cooling off after repeated wrong keys. The key is held in memory for as long as the console is open and is never written to browser storage, so reloading the page means entering it again. If the key is rotated mid-session, the next action returns you to the sign-in form with an explanation rather than a dead screen.
Copy .env.example to backend/.env to override defaults. The important settings are:
| Variable | Default | Purpose |
|---|---|---|
MONGODB_URI |
mongodb://127.0.0.1:27017/helpdesk?replicaSet=rs0 |
Replica-set-capable MongoDB connection string required for transactional import Apply |
PORT |
3000 |
Backend HTTP port |
APP_MODE |
development |
development, test, or demo |
LLM_PROVIDER |
ollama |
ollama, openai_compat, or mock. Used when LLM_PROVIDERS is unset. |
LLM_PROVIDERS |
unset | Comma-separated ordered fallback chain, e.g. openai_compat,ollama,mock. Falls back to LLM_PROVIDER if unset, empty, or naming an unrecognised provider. A single unrecognised entry is treated the same way (no silent partial chain). |
LLM_MODEL |
llama3.1:8b |
Model identifier for the configured provider |
OLLAMA_URL |
http://127.0.0.1:11434 |
Ollama endpoint |
LLM_TIMEOUT_MS |
10000 |
LLM timeout before fallback behaviour |
CONFIDENCE_THRESHOLD |
0.7 |
Minimum accepted classification confidence |
MAX_CLARIFICATION_ROUNDS |
2 |
Clarification limit before escalation |
SESSION_INACTIVITY_MINUTES |
30 |
Chat session expiry period |
STT_PROVIDERS |
local |
Ordered speech-to-text provider list |
STT_MODEL_DIR |
./models/stt |
Local speech-to-text model directory |
VOICE_MAX_SECONDS |
120 |
Voice-recording duration cap |
REMEDIATION_ENABLED |
false |
Master kill switch for automated remediation. Staff can additionally disable it at runtime, globally or per-endpoint, without restarting. |
AGENT_MAX_STEPS |
3 |
Maximum tool-calling steps the remediation agent takes per proposal |
REMEDIATION_SSH_KEY_PATH |
./.keys/remediation_id_ed25519 |
Path to the SSH client private key used against the whitelisted test endpoints |
REMEDIATION_SSH_KEY_PASSPHRASE |
unset | Passphrase for the SSH client key, if it has one |
REMEDIATION_CONNECT_TIMEOUT_MS |
5000 |
SSH connection timeout |
REMEDIATION_COMMAND_TIMEOUT_MS |
15000 |
Remote command execution timeout |
REMEDIATION_APPROVAL_TTL_MINUTES |
30 |
How long a state-changing action waits for staff approval before it expires and is refused |
MAINTAINER_KEY |
unset | Enables and protects /api/maintainer/*. Leave unset and those routes are never mounted — absent, not merely guarded. GET /api/maintainer/status is the one exception and stays mounted either way, so the console can say administration is switched off instead of showing a generic error. |
MAINTAINER_SIGNIN_MAX_FAILURES |
5 |
Consecutive failed console sign-ins from one client before further attempts are refused for a cooling-off period |
MAINTAINER_SIGNIN_COOLDOWN_SECONDS |
300 |
Length of that cooling-off period, and the window the failure count is measured over |
For an OpenAI-compatible server, set LLM_PROVIDER=openai_compat, LLM_BASE_URL, LLM_API_KEY, and LLM_MODEL. The complete annotated reference, including speech-to-text options, is in .env.example.
For the local reference setup, LM Studio can serve qwen2.5-7b-instruct at http://127.0.0.1:1234/v1 for chat and text-embedding-nomic-embed-text-v1.5 for future semantic retrieval. The current application does not yet make embedding requests, so the embedding model should remain configured separately from the chat model.
Seed the six baseline categories and their curated guides with:
cd backend
npm run seed:guidesThis populates the categories collection. Further categories and guide versions are managed at runtime rather than by editing the seed script — set MAINTAINER_KEY, restart the backend, and open the maintainer console at /maintainer. The /api/maintainer endpoints listed below are what the console calls, and remain available directly.
The remediation action whitelist and endpoint registry are versioned data files, not code: backend/src/policy/action-policy.json and backend/src/policy/test-endpoints.json. No SSH key material is ever committed — backend/.keys/ and the test endpoints' authorized_keys files are git-ignored, regenerated build inputs produced by backend/test-endpoints/reset.ps1.
All routes are prefixed with /api. Authenticated browser requests use the session cookie and must include credentials. Access column: public = no session, user = any signed-in account, staff = signed-in and staff role, maintainer = x-maintainer-key header.
| Endpoint | Access | Purpose |
|---|---|---|
POST /auth/register |
public | Create a user account and start a session |
POST /auth/login |
public | Sign in and set the session cookie |
POST /auth/logout |
user | End the current session |
GET /auth/me |
user | Read the current account and role |
POST /auth/change-password |
user | Change password and invalidate all other sessions |
| Endpoint | Access | Purpose |
|---|---|---|
POST /sessions |
public | Start or resume a chat session |
POST /conversations/:conversationId/messages |
public | Send a chat message |
POST /sessions/:sessionId/transcriptions |
public | Upload audio for local speech-to-text |
GET /tickets?sessionId=… |
public | List tickets belonging to the current chat session |
GET /tickets/:reference?sessionId=… |
public | Read a ticket available to the current chat flow |
GET /tickets/:reference/actions?sessionId=… |
public | Plain-language action history and any approval requests for one ticket |
POST /tickets/:reference/actions/consent?sessionId=… |
public | Grant or decline consent for one proposed action. A read-only grant executes immediately; a decline is refused and recorded, never a hard error. |
GET /events?sessionId=… |
public | Reporter SSE stream for replies and ticket updates |
GET /health |
public | Liveness plus LLM/database readiness |
| Endpoint | Access | Purpose |
|---|---|---|
GET /my/tickets |
user | Ticket history for the signed-in account |
GET /my/tickets/:reference |
user | Own-ticket detail; other accounts' tickets are refused |
GET /my/profile |
user | Read the own support profile |
PUT /my/profile |
user | Update remote-access IDs, location, or hardware notes |
GET /my/events |
user | SSE stream for the signed-in account's tickets |
| Endpoint | Access | Purpose |
|---|---|---|
GET /staff/tickets |
staff | Dashboard list; supports status/category/escalated/sort filters |
GET /staff/tickets/:reference |
staff | Full ticket detail |
POST /staff/tickets/:reference/status |
staff | Status change |
POST /staff/tickets/:reference/takeover |
staff | Atomic takeover of an unassigned ticket |
POST /staff/tickets/:reference/assignee |
staff | Explicit reassignment |
GET /staff/roster |
staff | Roster, availability, workload, and suggested assignee |
PUT /staff/availability |
staff | Set the signed-in staff member’s availability |
GET /staff/accounts?q= |
staff | List every user account — display name, email, role, and nothing else — narrowed by a case-insensitive substring of name or email. No match is a 200 with an empty array, not a 404 |
GET /staff/users/:id/profile |
staff | Read a reporter’s support profile, with each field’s author, timestamp, and controller. An account with no profile yet returns an empty, owner-controlled profile rather than a 404 |
PUT /staff/users/:id/profile/fields |
staff | Set location, hardware, or remoteAccessIds authoritatively. Always 200 with a per-field outcome map, including when one field was refused |
POST /staff/users/:id/profile/fields/:field/release |
staff | Hand one field back to the account owner. 409 FIELD_NOT_STAFF_CONTROLLED if the owner already controls it |
GET /staff/users/:id/profile/fields/:field/history |
staff | One field’s previous values and control transfers, newest first. Staff only — there is no owner-facing equivalent route |
POST /staff/users/:id/profile/entries |
staff | Append a free-text staff note to a profile. The pre-007 correction entry kind is retired and refused (CORRECTION_WRITE_RETIRED); staff now set the field’s value itself |
GET /staff/users/:id/credentials |
staff | Whether the account is still on its initial password |
POST /staff/users/:id/credentials/reset |
staff | Set a new initial password and revoke that account's sessions |
POST /staff/imports |
staff | Upload an .xlsx workbook and detect its columns |
PUT /staff/imports/:id/mapping |
staff | Map spreadsheet columns to profile fields |
POST /staff/imports/:id/preview |
staff | Dry-run the import and report per-row outcomes |
POST /staff/imports/:id/apply |
staff | Commit the import inside a MongoDB transaction |
GET /staff/actions |
staff | Cross-ticket audit trail; filterable by ticket, endpoint, outcome, and date range, paginated |
GET /staff/approvals?status=… |
staff | Approval queue; pending by default, or filtered by status. Past-due pending rows lazily transition to expired first. |
POST /staff/approvals/:id/approve |
staff | Approve a pending state-changing action; executes it |
POST /staff/approvals/:id/decline |
staff | Decline a pending state-changing action; never executes |
GET /staff/remediation |
staff | Read the current automation kill-switch state, global and per-endpoint |
POST /staff/remediation/toggle |
staff | Enable or disable automation, globally or for one endpoint |
GET /staff/metrics?period=… |
staff | Attempted/succeeded/refused/failed action counts by category and endpoint for a selectable period |
GET /staff/events |
staff | SSE stream for ticket created/updated events, action recorded, approval pending/decided, and remediation availability changed |
Mounted at /api/maintainer and only when MAINTAINER_KEY is set — otherwise the routes are absent, not merely guarded. Requires x-maintainer-key and x-maintainer-name headers; the name is attribution for the version history, never authentication. Session cookies are ignored here, and no maintainer route can reach a ticket, a conversation, an account, or a role.
| Endpoint | Access | Purpose |
|---|---|---|
GET /maintainer/status |
public | Whether maintainer administration is enabled. Mounted even when MAINTAINER_KEY is unset, and returns only { "enabled": boolean } |
GET /maintainer/categories |
maintainer | List categories with their active guide version |
POST /maintainer/categories |
maintainer | Create a category and its first guide |
PUT /maintainer/categories/:name |
maintainer | Update category metadata |
DELETE /maintainer/categories/:name |
maintainer | Retire a category — it stays readable and keeps its existing tickets, it is not deleted. Refused with 403 MANDATED_CATEGORY_UNDELETABLE for the six mandated ones |
POST /maintainer/categories/:name/guide |
maintainer | Publish a new guide version. A rejected guide names the offending step and field (GUIDE_STEP_INVALID with stepIndex and field) |
GET /maintainer/categories/:name/guide/versions |
maintainer | List guide version history. Versions are immutable — there is no revert, edit, or delete route |
Sign-in is throttled: after MAINTAINER_SIGNIN_MAX_FAILURES refusals from one client inside MAINTAINER_SIGNIN_COOLDOWN_SECONDS, further attempts return 429 MAINTAINER_SIGNIN_THROTTLED with retryAfterSeconds, checked before the key is compared so the throttle cannot be used as an oracle. Every refused attempt is recorded with a hashed client identity and a timestamp; the supplied key is never written, to the record or to any log. An invalid key always returns the same 401 MAINTAINER_KEY_INVALID message regardless of its length or shape.
A PATCH /test-support/... router is additionally mounted when APP_MODE is test or demo. It is absent in development and production.
The detailed contracts are maintained in specs/004-staff-dashboard/contracts/api.md and, for the remediation routes, specs/005-constrained-remediation/contracts/api.md.
Run the checks from their respective application folders:
cd backend
npm run typecheck
npm run lint
npm test
cd ..\frontend
npm run typecheck
npm run lint
npm testThe backend suite excludes tests/benchmark/ by default; run those separately with npm run test:benchmark. Last observed on 2026-08-19:
| Gate | Result |
|---|---|
| Backend typecheck / lint | PASS / PASS (0 errors) |
| Backend Vitest | PASS — 76 files, 420 tests |
| Frontend typecheck | PASS |
| Frontend Vitest | PASS — 133 tests |
Role and access control specifically are covered by tests/integration/access-control.test.ts, tests/integration/my-tickets.test.ts (own-ticket isolation), and tests/integration/test-support-guard.test.ts. The default-deny remediation policy engine, the immutability of the audit trail, and the degraded-model refusal path are covered by tests/unit/policy-engine.test.ts, tests/integration/audit-trail-view.test.ts, and tests/integration/degraded-model-remediation.test.ts, among others.
Feature 004 staff-dashboard evidence and test traceability:
- Dashboard and ticket-detail evidence
- Takeover and reassignment evidence
- Staff takeover sequence diagram
- Chapter 5 test-case traceability
Feature 004 covers account authentication, staff-role enforcement, dashboard ticket management, live events, takeover/reassignment, roster availability, account-linked ticket history, self-service profiles/settings, staff profile actions, and Excel user import. See specs/004-staff-dashboard/tasks.md and the UAT record.
Feature 005 covers constrained automated remediation: the versioned action whitelist and default-deny policy engine, the SSH executor against isolated test endpoints, per-proposal reporter consent, staff approval for state-changing actions, the immutable action audit trail, the global/per-endpoint kill switch, outcome metrics, and the ordered LLM provider fallback chain with degraded-model refusal. See specs/005-constrained-remediation/tasks.md.
Feature 007 covers the maintainer console and staff-authoritative account editing: the /maintainer screen for category and guide administration with its enabled-status probe, sign-in throttle, and refused-attempt record; staff-set profile values that become the account's values, with per-field provenance, control transfer and release, staff-only field history, and per-field conflict detection; and the staff account directory that reaches any account rather than only reporters of an open ticket. See specs/007-admin-console-account-editing/tasks.md.
| Symptom | Likely cause | Fix |
|---|---|---|
| API does not start | MongoDB is unavailable | Start MongoDB and verify mongosh --eval "db.runCommand({ping:1})" succeeds. |
| Import Apply reports MongoDB code 20 or “Transaction numbers are only allowed…” | MongoDB is running standalone | Start the single-node rs0 setup above, confirm db.hello().isWritablePrimary is true, then set MONGODB_URI with ?replicaSet=rs0 and restart the backend. |
| Chat requests fail | Backend is not running | Start npm run dev in backend and check /api/health. |
| A staff route returns 401 or 403 | 401 = no valid session cookie; 403 = signed in but the account role is user |
For 401, sign in again (the cookie may have expired, or the request omitted credentials). For 403, provision or promote the account with npm run seed:staff and sign in again so the session reflects the new role. |
/staff shows “This area is only available to IT staff.” |
The SPA route guard read role: "user" from GET /auth/me |
Same fix as above. The guard mirrors the server check; the API refuses the call regardless of what the SPA renders. |
/api/maintainer/... returns 404 |
MAINTAINER_KEY is unset, so the maintainer router was never mounted |
Set MAINTAINER_KEY in backend/.env and restart. A 401 instead of 404 means the key is set but the x-maintainer-key header did not match. GET /api/maintainer/status answers either way, which is how the console tells the two apart. |
/api/admin/... returns 404 |
The namespace moved to /api/maintainer (feature 007) |
Use /api/maintainer/.... There is no admin role and never was; the path was renamed so it stops implying one. |
The console at /maintainer says administration is not enabled |
MAINTAINER_KEY is unset in the running backend |
Set it in backend/.env and restart. No sign-in form is offered until it is set, which is deliberate rather than a failure to render. |
| The console refuses a correct key and reports a cooling-off period | MAINTAINER_SIGNIN_MAX_FAILURES refusals came from this client inside MAINTAINER_SIGNIN_COOLDOWN_SECONDS |
Wait the reported number of seconds. The pause applies before the key is compared, so a correct key is refused too while it is in force. |
| A staff profile save reports a conflict on one field but saved the others | Another staff member changed that field after this page was opened | Expected behaviour. The refused field shows the current value, who set it, and when; re-enter the change over that value if it is still right. Nothing typed is discarded. |
An employee cannot edit a field on their own /profile |
A staff member set that field, which moves control to staff | Expected behaviour. Staff either correct the value or release the field back on /staff/users/:accountId/profile. |
| All issues escalate as unclassified | LLM provider is unavailable | Check /api/health, then verify LLM_PROVIDER, LLM_MODEL, and provider URL settings. |
| Microphone is unavailable | Permission, device, or local model issue | Allow browser microphone access; verify STT_MODEL_DIR if local transcription fails. Typing remains available. |
| Tests initially fail while downloading MongoDB binaries | mongodb-memory-server is preparing its binary |
Run the suite again after the download completes. |
Every proposed action is refused as remediation_disabled |
REMEDIATION_ENABLED=false, or a staff member disabled the kill switch (globally or for that endpoint) |
Check Automation (/staff/remediation) and re-enable it, or set REMEDIATION_ENABLED=true and restart the backend. |
An action is refused as no_matching_entry, unregistered_target, or endpoint_not_permitted |
The test endpoint containers are not running, or the endpoint registry is stale relative to them | Run powershell -File backend/test-endpoints/reset.ps1 and confirm the containers are up with docker compose -f backend/test-endpoints/docker-compose.yml ps. |
| A remediation action fails with an SSH host key or authentication error | The client keypair or authorized_keys is stale relative to the running containers |
Re-run backend/test-endpoints/reset.ps1 — it regenerates the keypair (if missing), restages authorized_keys, and recaptures host-key fingerprints into the pinned endpoint registry after docker compose down -v. |
A proposed action is refused as degraded_model |
The primary LLM provider was unavailable and the request fell back to a later provider in LLM_PROVIDERS |
Expected behaviour, not a bug (FR-025): no automated action ever executes on a classification made while the model was degraded. Check /api/health and the primary provider's own status; the proposal can be retried once it recovers. |
| A state-changing action never runs even after the reporter consents | It is a state_changing tier action, which additionally requires a staff decision at Approvals (/staff/approvals) |
Expected behaviour. If it is missing from the queue, it may have expired past REMEDIATION_APPROVAL_TTL_MINUTES and was auto-refused. |
This project is not open source. It is published for educational, reference, portfolio, and demonstration purposes only. All rights are reserved by the copyright holder; see LICENSE for the complete terms.