โ† Kevin Yoder
Personal, Family & Home ยท Flagship

O.D.D. Companion

A calm, mobile-first place for a caregiver to track behaviors, log escalations, and keep the documents, notes, and contacts that a child's behavioral-health care generates โ€” with one search across all of it.

In use Built May 2026 Offline-capable PWA

โš™ How it works ๐Ÿ–ผ Screenshot

01 Overview

O.D.D. Companion is a self-hosted organizer for a parent or caregiver supporting a child with Oppositional Defiant Disorder (ODD). It gathers the day-to-day work of that care โ€” behavior tracking, escalation logging, documents, notes, appointments, and contacts โ€” into one mobile-first app, with a single search that reaches across every module. The design goal was modest and specific: make logging a hard day take under five minutes, and turn scattered records into the kind of longitudinal picture a clinician can actually read.

The clearest example of that intent is the escalation log's paired what helped and what didn't help fields. Those are the highest-value entries for spotting patterns across visits, and the reason the exports are formatted as plain, clinician-ready artifacts rather than a private diary.

Track & log

Capture

A daily behavior tracker (8 metrics, rated 1โ€“5, with trends) and a severity-coded escalation log built around the paired "what helped / what didn't help" fields.

Keep in one place

Organize

A 10-category document cabinet with full-text search, rich notes (voice, image, and clipboard capture), an appointments calendar, and categorized contacts.

Make it usable

Surface

One global search across every module, plus server-compiled, clinician-formatted PDF or Word reports assembled from the whole record.

02 Why I built it

Care like this generates a steady stream of small records โ€” a rating here, an incident there, a document from a visit, a phone number โ€” and they tend to scatter across notes apps, photos, and email until the moment you need them in front of a clinician. I wanted one quiet place to hold all of it, designed so that logging in the middle of a difficult moment is fast enough to actually happen, and so that the accumulated detail reads as a pattern rather than a pile. The consistent thread through the interface copy is that the daily log builds a longitudinal record, and that "what helped and what didn't" is the field a clinician can do the most with.

03 What I built & how it works

It began as a single-file browser app and became a self-hosted, offline-capable one.

The first generation was a roughly 3,600-line single-file React app built inside a Claude.ai artifact, persisting through the sandbox's key-value store. The second ported it to my home server: a Vite React progressive web app with an IndexedDB cache in front of a small FastAPI + SQLite service, so any device on the home network stays in sync and the app keeps working offline. Because all storage had been funneled through two functions from the start, the port left every module component untouched and swapped only the persistence layer underneath.

Browser ยท iPhone PWA / laptop home server ยท Ubuntu ยท LAN-only Browser client React 18 SPA ยท Vite ยท 6 modules + search IndexedDB kv cache ยท dirty-flagged sync.js ยท boot pull ยท 60s poll ยท flush service worker ยท offline app shell Docker ยท oddapp FastAPI ยท uvicorn /api/store ยท opaque-JSON kv /api/export ยท PDF ยท DOCX /api/extract-text ยท search SQLite ยท /data/odd.db HTTP ยท :8260 Backups ยท 30-day retention cron 03:00 nightly ยท sqlite3 .backup laptop scheduled pull 03:15

Fig. 1 โ€” the server stays schema-agnostic (opaque JSON per key); the client owns the shapes. Reports and attachment-text extraction run server-side; two backup tiers keep 30 days each.

  1. Capture โ€” log the day's behaviors, an escalation, a note, or a document in a few taps; the target is under five minutes on a phone.
  2. Store โ€” each module writes an opaque-JSON blob to a local IndexedDB cache and marks it dirty.
  3. Sync โ€” dirty keys flush to the server on a 250 ms debounce; a 60 s poll and a boot pull keep two devices in step, last-write-wins.
  4. Search โ€” attachment text is extracted server-side, so one query reaches across every module, including inside files.
  5. Export โ€” the server compiles a clinician-formatted PDF or Word report from the whole record, module by module.

04 ๐Ÿ›  Skills & tech used

Languages
JavaScript / React 18Python 3.12CSS (no framework)BashPowerShellSQL / SQLite
Frontend
Viteoffline-first PWAWorkbox service workerIndexedDB (idb)MediaRecorder voice memosclipboard paste-to-fileWeb Share APIresponsive dual-nav
Backend / API
FastAPIopaque-JSON KV storeWeasyPrint (HTMLโ†’PDF)python-docxpypdf text extractionpytest
Infra / Ops
Dockerdocker-composetar-over-ssh deploytwo-tier backups (cron + scheduled task)SQLite volume
Techniques
last-write-wins syncdirty-flag reconciliationeffect-echo suppression (React)idempotent background backfillspec โ†’ plan โ†’ execute

No machine-learning models run in the product itself. Its AI angle is the build: it was authored with Claude, then migrated from a browser artifact into a production self-hosted app.

05 Notable challenges & decisions

Most of the real work was making a browser-sandbox app survive on real machines and real browsers.

Migration

From an artifact sandbox to real machines

Everything persisted through a Claude.ai-only key-value API, so the app would silently fail to save anywhere else. Because all reads and writes had been centralized in two functions from the start, the port became a two-function swap and the module components didn't change. The move did surface a latent bug the sandbox had tolerated โ€” a component used under an aliased import that never crashed in the artifact but did under a real Vite build.

Sync

Stopping the app from echoing itself

The first deploy produced a write storm: state pulled from the server re-triggered each module's save effect and got written straight back as new local changes. A single guard flag, set while remote data is being applied, gates those effects so a fresh load doesn't fan out into a round-trip per module.

Sync authority

Deciding which side wins, and when

Earlier buggy sessions had left keys marked dirty locally that could overwrite fresh server data on the next boot. The resolving rule is deliberately asymmetric: the boot pull is server-authoritative, so stale local edits can't clobber good remote data, while in-session polls still defer to unsaved local changes. Server-stamped timestamps keep ordering correct even when a device's clock is off.

Browser quirks

PDFs that refuse to render

Chrome and Firefox both decline to show a PDF served as a data: URL inside an iframe, for security reasons. The inline attachment viewer converts to same-origin blob URLs instead, which render โ€” a small fix that came from reading the actual browser behavior rather than assuming a data URL "should" work.

Deliberately small. No login, no HTTPS, no multi-child support, and no conflict-resolution UI โ€” the app trusts the home network and assumes a single user, where sync conflicts are rare and benign. It also starts fresh: none of the artifact-era data was migrated. Taking it off-LAN would mean adding HTTPS and a shared secret, which the design notes but I have not built.

06 Results

6
integrated modules
8
daily behaviors, rated 1โ€“5
10
document-cabinet categories
~3,600
lines in the original single-file app
12
passing backend tests
2
generations ยท artifact โ†’ self-hosted PWA

Sources: the project repository, its CLAUDE.md brief, and the design spec / implementation plan.

07 Screenshot

Shown with a de-identified clone โ€” every name and record in it is synthetic.

The O.D.D. Companion dashboard, showing stat tiles, quick actions, recent escalations with severity stripes, and a weekly trend, all populated with fictional data.
The dashboard, shown with a fully synthetic patient ("Ann T. Gonist") โ€” the de-identified demo clone.

08 Honest status

O.D.D. Companion is a private, single-user tool in real use, not a product. It runs on the home LAN with no login and no HTTPS โ€” a deliberate home-trust scope, not production-grade security; taking it off-network would need HTTPS and a shared secret, which the design notes but I have not built. iOS install-and-offline behavior over plain HTTP was flagged as the one likely rough edge and is best resolved by that same HTTPS step. The screenshot above comes from a separate de-identified clone whose data is entirely synthetic โ€” no real records are shown here or shipped. Because the underlying context is a family's private health information, I have kept this page deliberately general about the person it supports.

O.D.D. Companion โ€” Build Recipe

Take a bare machine to a running copy of O.D.D. Companion โ€” an offline-first companion app a caregiver uses to track a child's behavior over time: a daily behavior tracker (eight rated dimensions with trend averages), an escalation log (severity, triggers, what helped), a file cabinet, a calendar, a contacts book, and notes โ€” all in one place, installable as a PWA and usable with no network. React 18 (Vite PWA) front end + a small FastAPI / SQLite back end that stores everything as opaque JSON and serves the built SPA.

Status: โœ… Verified 2026-07-31 โ€” built from this clean clone on a laptop with a single docker compose up --build. The image builds (multi-stage: Node builds the front end โ†’ Python serves it), the container comes up healthy, seed_demo.py loaded the synthetic demo, and a real browser at http://localhost:8260 rendered the full dashboard โ€” brand mark, "Ann T. Gonist's overview", the correct module counts (8 daily / 4 escalations / 4 documents / 3 notes / 5 contacts), recent escalations with severities, the computed 7-day averages, upcoming appointments, and key contacts โ€” with 0 console errors. The 12-test backend suite passes in the shipped runtime. See demo-dashboard.png for the rendered result. The clean clone lives in clone/. All three tiers are covered โ€” prebuilt container, build-from-source, and bare-metal; the prebuilt image tarball is ~279 MB (verified 2026-08-01).

Sensitive data โ€” read this. The real app holds a real child's behavioral-health records. None of that is here. The clone ships zero data (no SQLite DB, no backups, no exported reports); the demo you see is entirely fictional โ€” "Ann T. Gonist" (say it aloud), the providers, the school, and every phone number are invented by seed_demo.py. The app is a personal, single-caregiver tool with no authentication and no encryption at rest โ€” it is not a HIPAA-compliant system. Run it for yourself, on your own machine or a private network.


What it is

Two pieces:

  • Front end โ€” a React 18 single-page app built by Vite into a PWA (installable, offline via a service worker). State lives in IndexedDB first, so the app works with no network; a small sync engine (sync.js) pushes local writes to the back end and, on boot, pulls the server's copy so a fresh device/browser hydrates from the server.
  • Back end โ€” a FastAPI app over SQLite exposing a tiny key-value API (GET/PUT/DELETE /api/store/{key}, plus /api/store/since/{ts} for incremental sync), a text extractor, a PDF/DOCX report exporter, and a static-file mount that serves the built SPA. It stores each of the app's seven records as opaque JSON โ€” it never parses the domain model, which keeps the server trivial and the data format entirely owned by the front end.

There is no training and no dataset โ€” it's an application, not a model. "Reproducing" it is building the front end and running the server.

Prerequisites

New machine? Install the base tools first โ€” see ../SETUP.md. Then, per tier:

Tier You need
1 โ€” prebuilt container (recommended) Docker (see ../SETUP.md). Nothing to build โ€” load the tarball (or pull the image) and run.
2 โ€” build from source Docker (see ../SETUP.md). Nothing else โ€” the image builds the front end itself.
3 โ€” bare-metal Node 20+ (to build the front end) and Python 3.11+ (to run the server).

The prebuilt tarball and the from-source build reproduce the same single artifact by different routes โ€” Tier 1 loads an image someone already built; Tier 2 builds that image in one command.


Tier 1 โ€” Run the prebuilt container(s) (recommended)

Availability: the prebuilt image bundle is available on request โ€” it is not published or linked anywhere. Ask Kevin for it, or build everything from source via Tier 2/3 below.

Skip the build entirely: load an image someone already built and start it. There's a single image, clone-odd-companion (the multi-stage build bakes in the Vite SPA and the FastAPI server).

# --- Produce the tarball once, on a machine that has built the image (see Tier 2): ---
docker save clone-odd-companion -o odd-companion-image.tar   # ~279 MB

# --- Consume it anywhere Docker runs (no build needed): ---
docker load -i odd-companion-image.tar          # loads clone-odd-companion
cd clone
docker compose up -d                            # no --build โ€” the image is already loaded
# โ†’ http://localhost:8260
curl http://localhost:8260/api/health           # โ†’ {"ok":true}
python seed_demo.py                             # load the fictional "Ann T. Gonist" demo (optional)

Open http://localhost:8260 โ€” an empty app on first run, or the demo if you seeded it. The SQLite DB persists in ./data/ (git-ignored); delete it to start clean.

The tarball is ~279 MB (verified 2026-08-01) and holds just the one clone-odd-companion image โ€” there are no opt-in profiles or extra images to bundle separately.

Tier 2 โ€” Build the image from source

cd clone
docker compose up -d --build      # stage 1 builds the SPA, stage 2 serves it
# โ†’ http://localhost:8260
curl http://localhost:8260/api/health          # โ†’ {"ok":true}
python seed_demo.py                             # load the fictional "Ann T. Gonist" demo (optional)

Open http://localhost:8260 โ€” an empty app on first run, or the demo if you seeded it. The SQLite DB persists in ./data/ (git-ignored); delete it to start clean.

Tier 3 โ€” Bare-metal (no Docker)

cd clone

# 1) build the front end  (outputs frontend/dist)
cd frontend
npm ci
npm run build

# 2) put the build where the server serves it, then run the server
cd ../backend
#   copy the built SPA into src/static  (any copy works; e.g.)
#   Windows PowerShell:  Copy-Item -Recurse ..\frontend\dist\* src\static\
#   bash:                mkdir -p src/static && cp -r ../frontend/dist/* src/static/
python -m venv .venv && . .venv/bin/activate    # Windows: .venv\Scripts\activate
pip install -r requirements.txt
uvicorn src.main:app --host 0.0.0.0 --port 8260

Then seed if you like: python ../seed_demo.py http://localhost:8260.

(The server also runs fine with an empty src/static โ€” the API works and the SPA fallback just returns a "frontend bundle not built" 404 until you build it. The DB path is configurable via the ODD_DB_PATH env var; it defaults to ./odd.db bare-metal, /data/odd.db in the container.)


Verify

curl http://localhost:8260/api/health
# โ†’ {"ok":true}

# after seeding, the server returns all seven records:
curl -s http://localhost:8260/api/store | python -m json.tool | head
# โ†’ {"odd-app:profile": {"value": {"childName": "Ann T. Gonist"}, "updated_at": ...}, ...}

Then open the page โ€” the dashboard should show the child's name, the module counts, recent escalations, the 7-day behavior averages, upcoming appointments, and key contacts.

Run the backend tests. They exercise the DB layer and the store API (12 tests). Easiest in the shipped runtime (has everything installed):

docker compose cp ../backend/tests odd-companion:/app/tests    # or: docker cp
docker compose exec odd-companion sh -c "pip install -q pytest httpx && cd /app && python -m pytest tests -q"
# โ†’ 12 passed

Bare-metal equivalent (Python 3.11+): from backend/, pip install pytest httpx then pytest -q.

Notes & gotchas

  • The server is the source of truth at boot; the browser is offline-first after that. A fresh browser (empty IndexedDB) pulls GET /api/store on load and renders it โ€” which is exactly why seeding the server is enough to populate the UI. In-session, local edits go to IndexedDB immediately and flush to the server in the background.
  • PDF export needs WeasyPrint's native libs. They're in the Docker image; on bare-metal Windows they usually aren't. WeasyPrint is lazy-imported inside the export path, so this never affects startup, the API, the UI, or DOCX export (pure-Python) โ€” only POST /api/export with format=pdf would fail there.
  • No auth, no multi-tenant. One app = one child's records. Point different ODD_DB_PATH values (or different containers/ports) at different databases if you need more than one.
  • data/ and *.db are git-ignored. The clone intentionally carries no database; that's the whole point โ€” you bring your own data (or the synthetic seed).
  • Front-end bundle: Vite emits ~250 kB JS (~72 kB gzipped) + ~32 kB CSS and a 10-entry PWA precache โ€” small enough to install and run fully offline.

Provenance (no training data, no GPU, no model)

O.D.D. Companion trains nothing and calls no external service โ€” there are no datasets, no model weights, and no GPU anywhere in the build. It is a self-contained local application; the only network traffic is between the browser and its own back end on the same origin.

What was stripped from the personal version (de-identification + recipe minimalism)

  • All real data โ€” the live SQLite database, the backups/*.db snapshots, and any exported sample-report.pdf (which could carry the child's real name) โ†’ excluded entirely. The clone ships no data; the demo is synthetic (seed_demo.py).
  • The child's initial in the brand mark โ€” the sidebar glyph and the generated PWA icons showed the real child's first initial โ†’ changed to a neutral "A", and the icons were regenerated. The rest of the UI already reads the child's name from the (now-synthetic) profile record, so nothing else was hardcoded.
  • Server-specific deploy tooling โ€” deploy.sh, the scripts/ bootstrap, CLAUDE.md, and the docs/ folder (all describing the author's private home-server host) โ†’ removed. The clone adds a clean, self-contained Dockerfile + docker-compose.yml instead.
  • A backup-to-server UI string that named the author's host โ†’ genericized.
  • Git history (.git) and build artifacts (node_modules, dist, src/static) โ†’ excluded; the image rebuilds them from source.