Files
TicketingSystem/README.md
T
2026-04-18 16:42:47 -04:00

584 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TicketingSystem
Internal ticketing system with CTI-based routing, severity levels, role-based access, and automation integration.
## Features
- **CTI routing** — tickets categorised by Category → Type → Item, reroutable at any time
- **Severity 15** — SEV 1 (critical) through SEV 5 (minimal); dashboard sorts by severity
- **Status lifecycle** — Open → In Progress → Resolved → Closed; resolved tickets auto-close after 14 days
- **Full-text search** — Postgres-native tsvector search across tickets and comments
- **Saved views** — per-user filter presets on the tickets list
- **Pagination + bulk actions** — page through large result sets and reassign / close / reprioritise in batches
- **Attachments** — upload files on tickets and comments (25 MB each, local disk)
- **Notifications** — in-app bell + optional SMTP email on assignment, @mention, and resolution
- **Outgoing webhooks** — HMAC-signed POSTs on ticket/comment events, with per-endpoint secret and retries
- **@mentions** — autocomplete in comments; triggers notifications and deep-links to the mentioned user's queue
- **Analytics dashboard** — open-by-severity, aging tickets, resolution time, queue load
- **CSV export** — stream any filtered ticket view to CSV
- **Command palette + keyboard shortcuts** — ⌘K palette, `j/k` navigation, `g d/t/m/n/s` leaders, `?` help
- **PWA** — installable on mobile, offline app shell
- **Comments** — GitHub/Gitea-style threads with markdown, draft autosave, Ctrl+Enter submit
- **Roles** — Admin, Agent, User, Service (API key auth for automation)
- **Audit log** — every action tracked with actor, timestamp, and expandable detail
- **Admin panel** — manage users, CTI hierarchy, and webhooks via UI
- **n8n ready** — service accounts authenticate via `X-Api-Key` header
---
## Roles
| Role | Access |
| ----------- | ---------------------------------------------------------------------------- |
| **Admin** | Full access — manage users, CTI config, webhooks, close and delete tickets |
| **Agent** | Manage tickets — create, update, assign, comment, change status (not Closed) |
| **User** | Basic access — view tickets and add comments only |
| **Service** | Automation account — authenticates via API key, no password login |
> Only **Admins** can manually set a ticket status to **Closed**.
---
## Production Deployment
### Prerequisites
- Docker + Docker Compose
- Nginx Proxy Manager pointed at the host port (default `3080`)
- Access to your Gitea container registry
### 1. Copy files to your server
```bash
scp docker-compose.yml .env.example user@your-server:~/ticketing/
```
### 2. Configure environment
```bash
cd ~/ticketing
cp .env.example .env
```
Edit `.env`:
```env
REGISTRY=gitea.thewrightserver.net
TAG=latest
POSTGRES_PASSWORD=<strong password>
JWT_SECRET=<output of: openssl rand -hex 64>
CLIENT_URL=http://tickets.thewrightserver.net
PORT=3080
# Optional — email notifications
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=<smtp password>
SMTP_FROM=tickets@thewrightserver.net
SMTP_SECURE=false
```
Point NPM at `http://<host-ip>:3080` for the proxy host.
### 3. Deploy
```bash
docker compose pull
docker compose up -d
```
The server exposes an unauthenticated `GET /healthz` that returns `{"status":"ok"}` and is polled by the compose healthcheck.
### 4. Seed (first deploy only)
```bash
docker compose exec server npm run db:seed
```
This creates:
- `admin` user (password: `admin123`) — **change this immediately**
- `goddard` service account — API key is printed to the console; copy it now
### Upgrading from v0.9
v1.0 is drop-in for existing deployments — the schema changes are all additive (new tables for attachments, webhooks, notifications, saved views; new columns for notification prefs and search vectors). No data migration is required.
```bash
docker compose pull
docker compose up -d
```
`npm run start:prod` runs `prisma db push` on boot, which applies the new tables and the search-index migration automatically. Set the new `SMTP_*` env vars if you want email notifications — otherwise they're silently skipped.
---
## Development
### Requirements
- Node.js 22+
- PostgreSQL (local or via Docker)
### Start Postgres
```bash
docker run -d \
--name ticketing-pg \
-e POSTGRES_DB=ticketing \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
postgres:16-alpine
```
### Server
```bash
cd server
cp .env.example .env # set DATABASE_URL and JWT_SECRET
npm install
npm run db:push # creates tables + search indexes
npm run db:seed # seeds admin + Goddard + sample CTI
npm run dev # http://localhost:3000
npm test # vitest (service layer)
npm run typecheck
```
### Client
```bash
cd client
npm install
npm run dev # http://localhost:5173 (proxies /api to :3000)
npm test # vitest + testing-library
npm run typecheck
```
CI runs typecheck + tests on both packages before building Docker images.
---
## API Reference
All endpoints (except `/api/auth/*` and `/healthz`) require authentication via one of:
- **JWT**: `Authorization: Bearer <token>` (obtained from `POST /api/auth/login`)
- **API Key**: `X-Api-Key: sk_<key>` (Service accounts only)
Base URL: `https://tickets.thewrightserver.net/api`
`POST /api/auth/login` is rate-limited to 10 attempts per 15 minutes per IP.
---
### Authentication
#### `POST /api/auth/login`
Authenticate and receive a JWT.
**Body:**
```json
{ "username": "string", "password": "string" }
```
**Response:**
```json
{
"token": "eyJ...",
"user": {
"id": "...",
"username": "admin",
"displayName": "Admin",
"email": "...",
"role": "ADMIN"
}
}
```
#### `GET /api/auth/me`
Returns the currently authenticated user.
---
### Tickets
#### `GET /api/tickets`
List tickets, sorted by severity (ASC) then created date (DESC).
**Query parameters:**
| Parameter | Type | Description |
| ------------ | ------ | --------------------------------------------------------------------- |
| `status` | string | Filter by status: `OPEN`, `IN_PROGRESS`, `RESOLVED`, `CLOSED` |
| `severity` | number | Filter by severity: `1``5` |
| `categoryId` | string | Filter by category (queue) |
| `assigneeId` | string | Filter by assignee user ID |
| `createdById`| string | Filter by author |
| `search` | string | Full-text search (tsvector) on title, overview, and display ID |
| `page` | number | Page number, 1-indexed. When omitted the endpoint returns a bare array (v0.9 shape) |
| `pageSize` | number | Page size (default 25, max 100) |
**Response (paginated):**
```json
{
"data": [ /* tickets */ ],
"total": 134,
"page": 1,
"pageSize": 25
}
```
**Response (unpaginated — `page` omitted):** Array of ticket objects with nested `category`, `type`, `item`, `assignee`, `createdBy`, and `_count.comments`. This preserves compatibility with v0.9 clients and the Goddard integration.
---
#### `GET /api/tickets/:id`
Fetch a single ticket by internal ID or display ID (e.g. `V325813929`). Includes full `comments` array.
---
#### `POST /api/tickets`
Create a new ticket. Requires **Agent**, **Admin**, or **Service** role.
**Body:**
```json
{
"title": "string",
"overview": "string (markdown)",
"severity": 1,
"categoryId": "string",
"typeId": "string",
"itemId": "string",
"assigneeId": "string (optional)"
}
```
**Response:** Created ticket object (201).
---
#### `PATCH /api/tickets/:id`
Update a ticket. Accepts any combination of fields. Requires **Agent**, **Admin**, or **Service** role.
> Setting `status` to `CLOSED` requires **Admin** role.
All changes are recorded in the audit log automatically.
---
#### `DELETE /api/tickets/:id`
Delete a ticket and all associated comments, audit logs, and attachments. **Admin only.**
---
#### `POST /api/tickets/bulk`
Apply an action to many tickets at once. **Agent** or **Admin**.
```json
{ "ids": ["..."], "action": "reassign" | "close" | "setSeverity", "value": "..." }
```
---
#### `GET /api/export/tickets.csv`
Stream the matching tickets (same filter params as `GET /api/tickets`) as CSV.
---
### Search
#### `GET /api/search?q=<query>&limit=10`
Cross-resource search over tickets and comments using Postgres `plainto_tsquery` with rank ordering.
**Response:**
```json
{
"tickets": [ /* ticket summaries */ ],
"comments": [ /* comment snippets with ticket context */ ]
}
```
---
### Analytics
#### `GET /api/analytics/summary?window=30`
Returns aggregates for the given window (days — 14 / 30 / 90):
```json
{
"openBySeverity": { "1": 2, "2": 5, "3": 14, "4": 8, "5": 3 },
"aging": { "over7d": 6, "over14d": 2 },
"queueLoad": [ { "categoryId": "...", "name": "TheWrightServer", "open": 12 } ],
"medianResolutionHours": 9.4
}
```
---
### Saved Views
Per-user filter presets.
| Method | Endpoint | Description |
| -------- | ------------------------- | ------------------------ |
| `GET` | `/api/saved-views` | List the caller's views |
| `POST` | `/api/saved-views` | Create — `{ name, filters }` |
| `PATCH` | `/api/saved-views/:id` | Rename / edit filters |
| `DELETE` | `/api/saved-views/:id` | Delete |
---
### Notifications
| Method | Endpoint | Description |
| -------- | ------------------------------- | ---------------------------------------- |
| `GET` | `/api/notifications` | Caller's notifications, newest first |
| `POST` | `/api/notifications/read` | Mark specific or all — returns `{ updated }` |
Notifications are created on assignment, @mention, and status → RESOLVED. Email delivery is attempted when SMTP is configured and the target user has email notifications enabled in `User.notificationPrefs`.
---
### Attachments
| Method | Endpoint | Description |
| -------- | ------------------------------------------ | ---------------------------------- |
| `POST` | `/api/tickets/:id/attachments` | Upload (multipart, field `file`) |
| `POST` | `/api/comments/:id/attachments` | Upload on a comment |
| `GET` | `/api/attachments/:id` | Stream with original filename |
| `DELETE` | `/api/attachments/:id` | Uploader or Admin |
Size limit 25 MB per file. Storage path is controlled by `UPLOADS_DIR` (defaults to `./uploads` in dev, `/data/uploads` in the Docker image backed by the `uploads` volume).
---
### Webhooks
Admin-only. Outgoing webhooks fire on `ticket.created`, `ticket.status_changed`, `ticket.assigned`, and `comment.created`.
| Method | Endpoint | Description |
| -------- | --------------------------------------- | -------------------------- |
| `GET` | `/api/webhooks` | List |
| `POST` | `/api/webhooks` | Create — `{ name, url, events, secret? }` |
| `PATCH` | `/api/webhooks/:id` | Update |
| `DELETE` | `/api/webhooks/:id` | Delete |
| `POST` | `/api/webhooks/:id/rotate-secret` | Rotate secret |
**Signature:** each delivery includes `X-Ticketing-Signature: sha256=<hex>`, which is the HMAC-SHA256 of the raw request body using the webhook's secret. Retries up to 3× with exponential backoff on non-2xx responses.
---
### Comments
#### `POST /api/tickets/:id/comments`
Add a comment. Markdown body. @mentions are parsed and fire notifications to matched users.
```json
{ "body": "string (markdown)" }
```
#### `DELETE /api/tickets/:id/comments/:commentId`
Delete a comment. Authors may delete their own; Admins may delete any.
---
### Audit Log
#### `GET /api/tickets/:id/audit`
Retrieve the full audit log for a ticket, ordered newest first.
**Action types:**
| Action | Detail |
| --------------------- | -------------------------------------------------------------- |
| `CREATED` | — |
| `STATUS_CHANGED` | e.g. `Open → In Progress` |
| `SEVERITY_CHANGED` | e.g. `SEV 3 → SEV 1` |
| `ASSIGNEE_CHANGED` | e.g. `Unassigned → Josh` |
| `REROUTED` | e.g. `OldCat OldType OldItem → NewCat NewType NewItem` |
| `TITLE_CHANGED` | New title |
| `OVERVIEW_CHANGED` | — |
| `COMMENT_ADDED` | Comment body |
| `COMMENT_DELETED` | Deleted comment body |
| `ATTACHMENT_ADDED` | Filename |
| `ATTACHMENT_REMOVED` | Filename |
---
### CTI (Category / Type / Item)
#### `GET /api/cti/categories`
#### `GET /api/cti/types?categoryId=<id>`
#### `GET /api/cti/items?typeId=<id>`
Read the CTI hierarchy. Used to resolve IDs when creating/rerouting tickets.
**Admin-only write operations:**
| Method | Endpoint | Body |
| -------- | ------------------------- | ---------------------------------------------- |
| `POST` | `/api/cti/categories` | `{ "name": "string" }` |
| `PUT` | `/api/cti/categories/:id` | `{ "name": "string" }` |
| `DELETE` | `/api/cti/categories/:id` | — |
| `POST` | `/api/cti/types` | `{ "name": "string", "categoryId": "string" }` |
| `PUT` | `/api/cti/types/:id` | `{ "name": "string" }` |
| `DELETE` | `/api/cti/types/:id` | — |
| `POST` | `/api/cti/items` | `{ "name": "string", "typeId": "string" }` |
| `PUT` | `/api/cti/items/:id` | `{ "name": "string" }` |
| `DELETE` | `/api/cti/items/:id` | — |
Deleting a category cascades to all child types and items.
---
### Users
#### `GET /api/users`
List all users (id, username, displayName, email, role). Used to populate assignee dropdowns.
**Admin-only operations:**
#### `POST /api/users`
Create a user.
```json
{
"username": "string",
"email": "string",
"displayName": "string",
"password": "string (not required for SERVICE role)",
"role": "ADMIN | AGENT | USER | SERVICE"
}
```
Service accounts receive an auto-generated API key returned in the response. Copy it immediately — it is not shown again.
#### `PATCH /api/users/:id`
Update a user — including `notificationPrefs` and `regenerateApiKey`.
#### `DELETE /api/users/:id`
Delete a user. Cannot delete your own account.
---
## n8n Integration (Goddard)
The `goddard` service account authenticates via API key — no login flow needed.
**Create a ticket from n8n:**
```
POST /api/tickets
X-Api-Key: sk_<goddard api key>
Content-Type: application/json
{
"title": "[Plex] Backup - 2026-03-30T02:00:00",
"overview": "Automated nightly Plex backup completed.",
"severity": 5,
"categoryId": "<TheWrightServer category ID>",
"typeId": "<Automation type ID>",
"itemId": "<Backup item ID>",
"assigneeId": "<Goddard user ID>"
}
```
CTI IDs can be fetched from:
- `GET /api/cti/categories`
- `GET /api/cti/types?categoryId=<id>`
- `GET /api/cti/items?typeId=<id>`
To regenerate the Goddard API key: Admin → Users → refresh icon next to Goddard.
---
## Environment Variables
| Variable | Required | Description |
| ------------------- | -------- | ---------------------------------------------------- |
| `DATABASE_URL` | Yes | PostgreSQL connection string |
| `JWT_SECRET` | Yes | Secret for signing JWTs — use `openssl rand -hex 64` |
| `CLIENT_URL` | Yes | Allowed CORS origin (your domain) |
| `PORT` | No | Server port (default: `3000`) |
| `UPLOADS_DIR` | No | Attachment storage path (default `/data/uploads` in Docker) |
| `SMTP_HOST` | No | SMTP host — when set, email notifications are sent |
| `SMTP_PORT` | No | SMTP port (default `587`) |
| `SMTP_USER` | No | SMTP username |
| `SMTP_PASS` | No | SMTP password |
| `SMTP_FROM` | No | From address (default `noreply@localhost`) |
| `SMTP_SECURE` | No | `true` for implicit TLS (port 465), else `false` |
| `REGISTRY` | Deploy | Container registry hostname |
| `POSTGRES_PASSWORD` | Deploy | Postgres password |
| `TAG` | Deploy | Image tag to deploy (default: `latest`) |
---
## Ticket Severity
| Level | Label | Meaning |
| ----- | ----- | ------------------------------------ |
| 1 | SEV 1 | Critical — immediate action required |
| 2 | SEV 2 | High — significant impact |
| 3 | SEV 3 | Medium — standard priority |
| 4 | SEV 4 | Low — minor issue |
| 5 | SEV 5 | Minimal — informational / automated |
Tickets are sorted SEV 1 → SEV 5 on the dashboard.
---
## Ticket Status Lifecycle
```
OPEN → IN_PROGRESS → RESOLVED ──(14 days)──→ CLOSED
re-opens reset
the 14-day timer
```
> CLOSED status can only be set manually by an **Admin**. The auto-close job runs hourly.
---
## Keyboard Shortcuts
| Context | Keys | Action |
| -------- | --------------------------------- | ---------------------------- |
| Global | `⌘K` / `Ctrl+K` | Command palette |
| Global | `?` | Shortcuts help |
| Global | `c` | Create ticket (Agent/Admin) |
| Navigate | `g` then `d` / `t` / `m` / `n` / `s` | Dashboard / Tickets / My Tickets / Notifications / Settings |
| List | `j` / `k` | Move cursor |
| List | `Enter` | Open focused ticket |
| List | `x` | Toggle selection |
| Detail | `e` | Edit title |
| Detail | `r` | Reply (focus composer) |
| Detail | `Ctrl+Enter` / `⌘Enter` | Submit comment |