Developer Guide
This guide teaches you how to work on the zmNinjaNg codebase. It is written for developers who may not have React experience, explaining concepts from first principles with real examples from the code.
New to React? Start with React Fundamentals, then State Management with Zustand.
Adding a feature? Read Contributing to zmNinjaNg for the workflow, then Testing Strategy.
Debugging? Find the flow your bug sits on in Call Flows below, then read the chapter for the layer where it breaks: State Management with Zustand when state does not update, API and Data Fetching when server data is stale or missing, Application Lifecycle when the problem only appears at startup, on resume, or after backgrounding.
Understanding the architecture? Component Architecture explains how components are organized, and Application Lifecycle explains the runtime flow.
Working on the assistant? Assistant Internals covers the agent loop, the tool registry, and the provider backends.
Working on kiosk, TV mode, or the notification and assistant screens? Platform Surfaces covers those surfaces.
Returning to the codebase, or out of touch? Start with Call Flows below. It traces a few real user actions scene by scene through the actual code and links into the reference chapters.
- Introduction to zmNinjaNg Development
- React Fundamentals
- Mental shift
- JSX
- Components
- Props: data flowing in
- How this codebase writes a component
- State: data the component owns
- Render: what triggers it
- Hooks
- useEffect: doing things after render
- useRef: a value that survives renders without triggering one
- useMemo and useCallback: stable references
- Object identity: the bug that hides everywhere
- React.memo: skipping unnecessary renders
- React Query: Server State
- Component communication
- Putting it together
- Concepts taught elsewhere
- State Management with Zustand
- Call Flows
- How to read this
- Flow 1: Cold start to an authenticated session
- Flow 2: Montage opens and a live MJPEG stream runs
- Flow 3: A push notification, from registration to tap
- Flow 4: Adding a server profile
- Flow 5: Browse events and play a video
- Flow 6: The access-token lifecycle
- Flow 7: Live notifications over the Event Server websocket
- Flow 8: A go2rtc WebRTC live stream
- Flow 9: The Timeline view
- Flow 10: Downloading an event
- Flow 11: A bandwidth setting becomes polling cadence
- Flow 12: A Dashboard widget
- Flow 13: Kiosk lock and biometric unlock
- Flow 14: Capturing a snapshot
- Flow 15: Changing the ZoneMinder run state
- Flow 16: Editing and deleting a server profile
- Flow 17: Aiming a PTZ camera
- Flow 18: Seeing what happened while you were away
- Flow 19: Asking the assistant a question
- Flow 20: A Live Activity poll tick
- Flow 21: Switching into a virtual profile group
- Flow 22: Merged events and direct tap-through while aggregating
- Flow 23: Live notifications across every server in an aggregate
- Flow 24: Opening a monitor’s settings on a restricted account
- Pages and Views
- Component Architecture
- Testing Strategy
- API and Data Fetching
- Contributing to zmNinjaNg
- Key Libraries
- Application Lifecycle
- Shared Services and Reusable Components
- External Network Endpoints
- Agent development model
- Scope, platforms, and release guardrails
- Moving from code review to constraint enforcement and design review
- How a feature actually lands
- Rules, gates, and practices
- How the pieces fit
- A contract, end to end
- Monthly scorecard review
- Mining history for lessons
- Token economics
- Using this in your own project
- Where everything lives
- What this asks of a contributor
- Assistant Internals
- Turn loop (
agent.ts) - Keeping a long conversation inside a finite window
- Token accounting per backend
- Tools and the gates in front of them
- Picking the backend (
providers/provider.ts) - llama.cpp on device (
providers/native-llm.ts) - Apple Intelligence (
providers/apple-intelligence.ts) - Gemini Nano on Android (
providers/gemini-nano.ts) - WebLLM models and their context windows
- Turn loop (
- Platform Surfaces
- Go2RTC WebRTC Streaming
- Choosing go2rtc or MJPEG
- Where
go2rtcUrlcomes from - WebSocket URL and stream name
- Protocol negotiation runs in parallel
- STUN servers
- Starting and stopping the connection
- Falling back to MJPEG
- Tearing the stream down
- Settings that affect go2rtc
- Video element
- Type definitions
- Security
- Testing
- Troubleshooting
- References
State Types
Type |
Where |
Example |
Pick it when |
|---|---|---|---|
Local |
|
Form inputs, UI toggles |
Only one component reads the value, and throwing it away on unmount loses nothing |
Global |
Zustand stores |
Current profile, settings |
Two unrelated subtrees read it, or non-React code has to write it |
Server |
React Query |
Monitor list, events |
The ZoneMinder server is the authority, and the app holds a copy that can go out of date |
The three are not interchangeable, and picking the wrong one is the most common
structural mistake in this codebase. useState and Zustand are taught in
React Fundamentals and State Management with Zustand. React
Query, the cache that holds everything fetched from a ZoneMinder server, is
also introduced in React Fundamentals; this app’s use of it is in
API and Data Fetching.
File Organization
app/
├── src/
│ ├── api/ # ZoneMinder API wrappers (thin, built on lib/http.ts)
│ ├── assets/ # Static images imported by the bundler
│ ├── components/ # React components, grouped by feature
│ ├── contexts/ # React context providers (PipContext)
│ ├── hooks/ # Custom React hooks (component logic)
│ ├── lib/ # Non-React utilities, grouped by domain
│ ├── locales/ # i18n translations, one directory per locale
│ ├── pages/ # Route-level views
│ ├── plugins/ # Custom Capacitor plugins (pip, safe-area, ssl-trust)
│ ├── services/ # Long-lived singletons (notifications, bootstrap)
│ ├── stores/ # Global state (Zustand)
│ ├── styles/ # CSS that Tailwind cannot express
│ ├── tests/ # Vitest setup and global mocks
│ ├── types/ # Ambient type declarations
│ ├── App.tsx # Providers, routes, bootstrap overlay
│ └── main.tsx # Entry point: createRoot + StrictMode
└── tests/ # End-to-end tests (features, steps, helpers, native)
Unit tests do not live in src/tests/. They sit in a __tests__/ folder
next to the code they cover, so lib/security/crypto.ts is tested by
lib/security/__tests__/crypto.test.ts. src/tests/ holds only the Vitest
setup file and the plugin mocks it registers.
Development Quick Start
Run the first npm install at the repository root, not in app/. That is
the one that wires the husky git hooks. Skipping it silently disables every
hook; CI re-checks what the hooks enforce, but only after you push.
npm install
cd app
npm install
npm run dev
npm test
npm run build
Also see the AGENTS.md file for the full development guidelines and checklists.