Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Grassmere Incident Desk

A demo guest-incident reporting dashboard for a zoo Security & EMT team, modeled on the Nashville Zoo at Grassmere guest map. Officers and EMTs pick their role, log an incident against a real location on the park map, attach timestamped photos or video from the device camera, and supervisors review, follow up, and print reports.

Built as a single-page app with no framework and no backend: plain HTML, CSS, and JavaScript, with the browser's IndexedDB as the database. Open index.html and it works.

Demo status. This is a portfolio / proof-of-concept build. It is not the zoo's system of record, and sample incidents are clearly tagged as samples.


Purpose

Incident reports at attractions are usually paper forms or a generic ticketing tool. Neither is fast on a phone in the field, neither knows the park's geography, and neither ties the photo you took to the exact moment you took it. This project shows what a purpose-built tool looks like:

  • Two reporter roles (Security, EMT) with fields that match how each team actually writes a report
  • Tap-the-map location picker using the park's own guest map, so "where" is a pin, not free text
  • Camera capture with the incident ID, timestamp, and badge burned into every photo
  • A local database that stores every record and capture with time and date

Goals

  1. Fast enough to use on a phone mid-incident (big targets, autosaving drafts, sensible defaults)
  2. Complete enough that a supervisor could act on the report without calling the officer
  3. Structured data from day one (typed fields, controlled vocabularies, IDs) so it can feed a real backend later
  4. Zero setup for a demo: one file, one link, no accounts

Features

Area What it does
Sign-on gate Choose Security Officer or EMT, enter name and badge. Every record and capture carries the author.
Dashboard Open / high-critical / logged-today / awaiting-follow-up counts, recent incidents, open incidents pinned on the map, "needs attention" list, top categories for your team.
Location picker ~75 locations from the guest map (exhibits, dining, attractions, event spaces, restrooms, first aid, ATM, parking, front gate), grouped by zone, searchable, and clickable as pins on the map image.
Security report Incident type, severity, narrative, people involved, outcome (warning → ejection → trespass ban → MNPD), officer / report #, CCTV camera IDs, statements, evidence, property, vehicles, supervisor.
Medical report Patient details, chief complaint, mechanism / HPI, SAMPLE history, multi-set vitals (BP, HR, RR, SpO₂, temp, BGL, pain, AVPU, skin), treatment checklist, disposition, EMS handoff (911 time, agency, unit, arrival, departure, destination), refusal-of-care attestation.
Evidence Live camera (photo + video, front/back flip) or native device camera. Photos get INCIDENT-ID YYYY-MM-DD HH:MM:SS BADGE stamped in the corner. Video gets a poster frame and duration.
Incident detail Full report, evidence gallery with lightbox, status changes, timestamped follow-up log, print / save-as-PDF, JSON export.
Map view Filter open / today / all-time; click any pin to list its incidents.
Drafts The form autosaves per role; a discarded draft also deletes its captures.
Themes Light and dark, following the OS or an explicit data-theme.

Workflow

Sign on (role, name, badge)
   └─▶ Dashboard
         ├─▶ New report ─▶ When & where ─▶ What ─▶ People ─▶ Role section ─▶ Evidence ─▶ Actions ─▶ Submit
         │                                   (map picker)                 (camera / file)
         ├─▶ Incident log ─▶ Incident detail ─▶ status · follow-up notes · add evidence · print · export
         └─▶ Map ─▶ pin ─▶ incidents at that location

Incident IDs are minted per team per day: SEC-260905-003, MED-260905-001 (YYMMDD + sequence).

Inputs

  • Reporter identity (name, badge, role) — kept in localStorage
  • Form fields per section (see blankDraft() in src/app.js for the full schema)
  • Photos / video from getUserMedia + MediaRecorder, or <input type="file" capture> on phones
  • The park map (assets/zoo-guest-map.webp) with pin coordinates in LOC_RAW

Outputs

  • Incident records and media in IndexedDB (grassmere-incident-desk database: incidents, media stores)
  • Printable report (browser print / save as PDF)
  • JSON export of one incident or the whole log (when the host provides a download hook)
  • dist/index.html — the single-file bundle for sharing

Project structure

index.html              # dev entry — loads src/ and assets/ directly, no build needed
src/
  app.js                # all application code (data, storage adapter, views, camera, map)
  styles.css            # design tokens (light + dark), layout, components
assets/
  zoo-guest-map.webp    # guest map used for the location picker and dashboards
scripts/
  build.py              # bundles everything into dist/index.html (map inlined as data URI)
dist/
  index.html            # generated single-file build (committed for convenience)
docs/
  data-model.md         # incident and media record shapes

Running it

No install. Either:

  • Open index.html in a browser (Chrome/Edge/Safari), or
  • Serve the folder (python3 -m http.server 8000) and open http://localhost:8000 — required for the live camera on some browsers, since getUserMedia needs a secure or localhost origin

Rebuild the single-file bundle after changes:

python3 scripts/build.py

Dependencies

  • Google Fonts (Barlow, Barlow Condensed, JetBrains Mono) — falls back to system fonts offline
  • Browser APIs: IndexedDB, getUserMedia, MediaRecorder, Canvas, IntersectionObserver
  • No npm packages, no framework, no server

Architecture notes

  • Storage adapter. All persistence goes through one object (Store) with a small interface: listIncidents, putIncident, deleteIncident, listMedia, addMedia, getMedia, deleteMedia. Swap it for a REST client (e.g. Django + DRF + Postgres, S3 for media) without touching the views. A MemStore fallback kicks in when IndexedDB is unavailable (private mode, restricted iframes).
  • Rendering. Hash-based router (#/dashboard, #/new, #/incidents, #/incident/<id>, #/map) and template-string views. Deliberately framework-free so the whole thing reads top to bottom.
  • Locations. LOC_RAW holds [zone, name, x, y, kind] with x, y in pixels on the 2000×1250 map; they're converted to percentages so pins stay put at any size. Add a location by adding a row.
  • Evidence integrity. Photos are re-encoded through a canvas with a burned-in stamp; the record also stores takenAt, by, mime, size, and an optional caption. Videos keep the original blob plus a poster frame.

Maintenance notes

  • Vocabularies (SEC_CATS, EMT_CATS, TREATMENTS, DISPOSITIONS, NOTIFY, …) live at the top of src/app.js. Change wording there, not in the templates.
  • Sample records are seeded once per browser (gid.seeded flag). Delete them from the detail page, or clear site data to reseed.
  • Design tokens are CSS custom properties on :root; dark mode redefines only the tokens.
  • The IndexedDB schema is version 1. Bump the version in Store.open() and add an upgrade path before changing store shapes.

Future improvements

  • Shared backend (Django + DRF + Postgres) behind the same adapter, with per-user auth and supervisor sign-off
  • Media upload to object storage with server-side timestamps and hashes for chain of custody
  • Shift handoff summary and daily PDF digest
  • Offline queue: capture in the field with no signal, sync when back on Wi-Fi
  • Role-based permissions (officer / EMT / supervisor / read-only)
  • Body-cam and radio-log references

Success criteria

  • An officer can complete a lost-child report on a phone, with a photo and location, in under three minutes
  • A supervisor can read any report and know what happened, who was involved, what was done, and what is still owed — without a phone call
  • Every photo and video carries a trustworthy capture time
  • The code stays readable enough that a new developer can add a field or a location in one sitting

License

MIT — see LICENSE.

The guest map is the property of the Nashville Zoo at Grassmere and is included here only as reference material for a demo.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages