Dual-role civic UX on one shared store
Civic Desk is a local educational demo of how citizen-facing complaint submission and staff-facing triage can share one datastore — including priority, department assign, internal notes, an offline map pin, up to three photos in IndexedDB, named templates, in-page notifications. It is not a production 311 system, not connected to any municipality, and not a substitute for official channels.
Last updated 25 August 2026
1. Two apps, one desk
Real municipalities often ship separate citizen portals and staff dashboards. Civic Desk collapses both into one static page with two role tabs:
- Citizen — pick a demo identity (Resident A or Resident B), submit with category, priority, visibility, location, optional offline map pin, description, and up to three photos; search and filter own tickets; edit or delete while pending; use named templates; after a reply, rate 1–5 and see a local notification. Assigned department is read-only when staff set it.
- Staff — see every complaint; filter by status, category, priority, and department; read a queue summary strip; assign department; keep internal notes (never shown to the citizen); update status; write an official reply (first reply on a still-pending ticket sets
in_progressunless you chose completed or rejected); export the filtered queue as CSV; print a staff summary; delete pending or rejected tickets; view attached photos when present.
Both roles read and write the same complaints array. Switching tabs is like opening
two desktop applications that point at one shared database file — except here the
“database” is localStorage under the key kt-civic-desk when you opt in to save.
2. What this is not
Not a production 311 system
- No authentication server, case routing, SLA timers, or GIS integration.
- No SMS or email — in-page notifications are a local demo feature only.
- No connection to a council backend, CRM, or national open-data feed.
- Demo identities only — no passwords, no real resident records.
Use it to teach or prototype dual-role civic workflows. For a real report, use your municipality’s official channels.
3. Recovered from the desktop pair
The original citizen and staff apps were two desktop windows over one store. This port keeps the dual-role idea in the browser and restores the workflow pieces that do not need a login server or a live 311:
- Priority — stored as
low,medium(default),high,urgent. Citizen submit and list; staff list and default sort urgent → high → medium → low, then date descending. - Visibility —
private(default) orpublic. Citizens still see only their own tickets; staff sees all in this demo. - Optional coordinates + offline map pin — collapsed lat/lng with Athens defaults (37.9838 / 23.7275). Click an SVG Athens sketch to set the pin (affine mapping, no tile server, no CDN). Stored and shown in detail with a read-only mini-pin for staff and citizens.
- Greek status and priority labels — English keys, Greek labels with English subtitles (same pattern as categories).
- Citizen search and filters — search category, description, and location; filter by category, priority, and status; count and clear.
- Staff triage extras — priority filter; first official reply on a still-pending ticket auto-sets
in_progressunless staff chose completed or rejected. - Department assign — teaching field (
departmentkeys such ascleaning,roads, defaultunassigned). Staff select + filter; queue badges; citizen sees the label only when assigned. Not CRM routing. - Internal notes — staff-only
internal_notesstring, saved with the response form. Never rendered on the citizen side. - Queue summary strip — counts for the current filtered staff set by status, priority, and department.
- CSV export (filtered) — staff download of the filtered queue (no photo blobs). Complements full JSON export/import.
- Printable staff summary — Print staff summary (or Print while on the Staff tab) hides the citizen panel and prints the summary + queue + detail.
- Deletes — citizen: own pending only. Staff: pending or rejected, with confirm.
- Citizen edit while pending — category, priority, visibility, location, description, optional lat/lng (and map pin), manage up to three photos. Not after in progress, completed, or rejected.
- Local notifications — a demo in-page list when staff saves a reply or status change. Unread count, mark read, mark all read. Not SMS.
- Named templates — a local demo feature that saves and prefills the citizen form (upsert by identity + name).
- 1–5 rating after a reply — a local demo field (
citizen_rating) once a municipality reply exists. - Up to three photos (IndexedDB) — complaint field
photos(data URLs in memory) plusphotoCountmetadata. With “Save desk data” on, text stays inlocalStorage(kt-civic-desk) and photo blobs go to IndexedDB (kt-civic-desk-photos). Cap 3 × 400 KB, JPG/PNG/WebP. Click a thumbnail to enlarge. Queue lists and the staff summary show photo counts; CSV includesphoto_count. If metadata says photos exist but IndexedDB is empty (other browser / cleared storage), the desk warns and detail explains how to re-attach or re-import. Legacy singlephotoimports migrate intophotos[0]. JSON export still carries full photos for backup (with a size hint when large). - JSON import — complementary to export; merge or replace with confirm. Older exports get defaults for new fields (
department→unassigned, emptyinternal_notes,photosfrom legacyphoto). String or colliding ticket ids (e.g."legacy-1") are assigned unused numeric ids instead of being dropped. - Dates — display as DD/MM/YYYY HH:MM.
- Richer sample — mixed priorities and departments, one private ticket, one with coordinates, internal notes on some tickets, one completed ticket with a staff reply, a local notification, and a rating.
4. Still not in this port
These remain out on purpose — never a working desktop feature, or incompatible with a static no-network demo:
- Login, passwords, or hashed user tables — demo identities only.
- SQLite — the shared file held users, complaints, photos, templates, and notifications. This page stores demo tables as JSON in
localStorageplus photos in IndexedDB. sql.js would be extra wasm for the same rows. There is no Anvil-hosted database. - Live municipality 311, CRM, or open-data feeds.
- Leaflet / OSM tiles — the desktop map sat on the edit dialog only, loaded Leaflet from a CDN, and never attached Qt’s web channel, so map clicks did not save coordinates. This port uses an offline SVG sketch instead of live tiles, so the no-network claim holds.
- SMS — never implemented (no phone column, no carrier). Staff “notify the citizen” wrote an in-app inbox row. That inbox is already here. Real SMS to residents must not ship.
- Heatmap / generated PDF reports — CSV of the filtered queue and print staff summary cover the teaching “reports” case. No heatmap or server-side PDF builder.
- Unity — C# scripts for a 2021.3 project, no scenes and no WebGL build to host.
- Unlimited photo galleries — capped at three attachments so IndexedDB quotas stay teachable.
- The Streamlit prototype.
5. Greek category, status, and priority labels
Categories follow a typical Greek municipal taxonomy. The desk UI shows the Greek label with an English subtitle so readers who do not read Greek can still navigate. Status and priority use the same pattern: English keys in the store, Greek-first labels in the UI.
| Greek label | English meaning |
|---|---|
| Καθαριότητα | Cleanliness & waste |
| Φωτισμός | Street lighting |
| Δρόμοι | Roads & pavements |
| Πάρκα | Parks & green space |
| Ύδρευση | Water supply |
| Άλλο | Other |
| Stored key | Greek label | English |
|---|---|---|
pending | Σε εκκρεμότητα | Pending |
in_progress | Σε εξέλιξη | In progress |
completed | Ολοκληρώθηκε | Completed |
rejected | Απορρίφθηκε | Rejected |
low | Χαμηλή | Low |
medium | Μεσαία | Medium |
high | Υψηλή | High |
urgent | Επείγουσα | Urgent |
private | Ιδιωτικό | Private |
public | Δημόσιο | Public |
6. Privacy and local-only storage
Nothing you type or attach is uploaded. Photos are read with FileReader
into data URLs. With “Save desk data” on, complaint text and metadata go to
localStorage (kt-civic-desk) and photo blobs go to IndexedDB
(kt-civic-desk-photos) so large images do not blow the typical 5 MB
localStorage quota. Clearing the desk or turning off save removes both.
User-rendered complaint text is inserted with textContent — not
innerHTML — so descriptions and replies cannot execute script
in the page.
Notifications, named templates, and 1–5 ratings are local demo features in the same store. They never leave this device as SMS, email, or a council API.
7. Data shape
{
"complaints": [
{
"id": 1,
"citizenId": "resident-a",
"category": "Φωτισμός",
"location": "Patision Ave & Tositsa",
"description": "…",
"photos": ["data:image/jpeg;base64,…"],
"priority": "high",
"visibility": "private",
"lat": 37.9838,
"lng": 23.7275,
"department": "lighting",
"internal_notes": "Coordinate with DEH temporary lighting request.",
"status": "in_progress",
"reply": "…",
"citizen_rating": 4,
"createdAt": "2026-08-16T10:00:00.000Z",
"updatedAt": "2026-08-17T14:30:00.000Z"
}
],
"templates": [],
"notifications": [],
"nextId": 2,
"persist": true,
"photosInIndexedDB": true
}
JSON export includes full photos arrays for backup. On-device persist
strips photo blobs from localStorage and writes them to IndexedDB instead.
Import accepts legacy single photo fields (migrated to photos[0]).
String ticket ids such as "legacy-1" get a new unused number; they are not dropped.
Older exports without triage fields get defaults
(priority: "medium", visibility: "private",
department: "unassigned", empty internal_notes).
8. How to use
- Open Civic Desk and optionally load the sample queue.
- As a citizen, pick Resident A or B and submit a complaint (category, priority, visibility, location, optional offline map pin, description, up to three photos ≤ 400 KB each). Save a named template if you want to prefill later — a local demo feature.
- Search or filter your list. Edit while the ticket is still pending.
- Switch to Staff, filter the queue (status, category, priority, department), assign a department, keep internal notes, update status, and write a reply. Export CSV or print the staff summary.
- Switch back to Citizen to see the local notification and, after a reply, a 1–5 rating control. Both are local demo features, not live 311. Assigned department appears as a read-only badge.
- Opt in to save on this device (text → localStorage, photos → IndexedDB), export or import JSON, or print.
9. Questions
Is Civic Desk connected to a real municipality?
No. It is an educational in-browser demo. Complaints live in localStorage when you opt in. Nothing is sent to a council backend.
How do citizen and staff views share data?
Both roles read and write the same complaints array under kt-civic-desk. Role tabs are a UX convenience, not separate databases.
Why are categories in Greek?
The desk mirrors common Greek municipality complaint taxonomies. Status and priority use the same pattern: English stored keys with Greek labels and English subtitles.
Does text or photos leave this page?
No. Photos stay on this device (memory + IndexedDB when you opt in). Complaint text uses localStorage. There is no upload endpoint. The map sketch is offline SVG — no tile server.
Why IndexedDB for photos instead of localStorage?
Typical localStorage quotas are about 5 MB per origin. Three JPEG data URLs would burn that quickly. IndexedDB holds the blobs; localStorage keeps text, triage fields, and a photoCount so the desk can warn if blobs are missing.
Can citizens delete any complaint?
Citizens can delete only their own pending complaints. Staff can delete pending or rejected tickets in the shared store.
Are notifications, templates, and ratings live 311 features?
No. In-page notifications, named templates, and 1–5 ratings after a reply are local demo features in this browser. They are not SMS, email, or a council backend.
Is department assign real routing?
No. Department and internal notes are teaching fields on the ticket. They do not call a CRM or notify a real office. Citizens never see internal notes.
Open Civic Desk
Try citizen submission and staff triage in Civic Desk. Local, private, not a production 311 system.