# Doris Schoolhouse Desktop-first local web app for school intake, recordings, D2L sync, and Paperless linkage. ## Purpose This app gives John a fast local UI to: - upload assignment turn-ins, with programming-class source files bundled into one archived submission artifact - upload class recordings - confirm suggested metadata - track assignment status through grading - sync classes/assignments/grades/comments from D2L - archive submissions/transcripts/summaries in Paperless ## Source vs runtime - **Repo source:** `home/doris-schoolhouse` - **Expected live NOMAD runtime:** `/opt/doris-schoolhouse` Do not run production from an agent workspace. ## Stack - FastAPI - Jinja2 templates - shared Postgres - Pangolin network - existing Paperless and n8n school workflows - container build now includes Python deps + `ffmpeg` for recording conversion ## Current state The app is live on NOMAD at `/opt/doris-schoolhouse` and uses shared Postgres in production. For local iteration, the source tree still supports a JSON-backed mode: - default state dir: `/data/state` - default upload temp dir: `/data/uploads` For local host-side testing, override with env vars such as: ```bash export SCHOOLHOUSE_STATE_DIR=/tmp/doris-schoolhouse-state export UPLOAD_TMP_DIR=/tmp/doris-schoolhouse-uploads ``` Production runtime uses shared Postgres plus Paperless/n8n integration. `SCHOOLHOUSE_STORAGE_BACKEND=auto` prefers Postgres when psycopg and a PostgreSQL DSN are available, and falls back to JSON state for local iteration. Repo contents still include: - route handlers and templates - schema/bootstrap helpers - compose/env examples - D2L / Paperless / n8n integration code ## Layout ```text home/doris-schoolhouse/ ├── .env.example ├── docker-compose.yaml ├── requirements.txt └── app/ ├── main.py ├── config.py ├── models.py ├── db/schema.sql ├── routes/ ├── services/ ├── static/ └── templates/ ``` ## Environment and integration helpers Useful helpers now included: ```bash bin/check-env.sh bin/apply-postgres-schema.sh bin/test-assignment-handoff.sh [more_files ...] [assignment_name] bin/test-recording-handoff.sh ``` These are safe prep helpers. The handoff scripts are dry-run previews; they print the exact curl commands rather than firing them automatically. Assignment intake accepts either a legacy single `file` field or the newer multi-file `files` field. Programming-class uploads are normalized into one source-bundle zip per submission version, with a manifest and source-listing sidecar kept alongside the bundle locally. For Paperless archival, Schoolhouse uploads a generated PDF submission receipt/listing while keeping the real source bundle zip as the local canonical artifact. Browser submits now auto-confirm into Paperless before redirecting back to `/assignments/upload`. The success notice reports the resulting Paperless queue state, so a normal web upload should no longer stop at `uploaded-local` unless archival actually failed. ## Serenity backup On NOMAD, the canonical local Schoolhouse artifacts live under `/opt/doris-schoolhouse/data`. The helper `bin/backup-to-serenity.sh` mounts Serenity's NFS export at `/mnt/serenity-data` if needed and mirrors that data tree into: `/mnt/serenity-data/backups/nomad/doris-schoolhouse/data` Recommended daily cron on NOMAD: ```cron 40 2 * * * /opt/doris-schoolhouse/bin/backup-to-serenity.sh >> /opt/doris-schoolhouse/logs/backup-to-serenity.log 2>&1 ``` This keeps the real source bundles, receipt PDFs, manifests, and other Schoolhouse runtime data copied onto Serenity without depending on broken SSH trust from NOMAD. ## Helper scripts Local iteration helpers: ```bash bin/bootstrap-local.sh bin/run-local.sh bin/process-recording.sh ``` These default to the JSON backend and `/tmp` paths unless you override the env vars. ## Bootstrap local state quickly For local iteration without live Postgres: ```bash cd /home/fizzlepoof/repos/truenas-stacks/home/doris-schoolhouse export SCHOOLHOUSE_STORAGE_BACKEND=json export SCHOOLHOUSE_STATE_DIR=/tmp/doris-schoolhouse-state export UPLOAD_TMP_DIR=/tmp/doris-schoolhouse-uploads PYTHONPATH=. python3 app/scripts/bootstrap_state.py ``` That seeds local state from the latest D2L snapshot. ## Recording worker helper There is now a local worker entrypoint for WAV → MP3 processing: ```bash cd /home/fizzlepoof/repos/truenas-stacks/home/doris-schoolhouse export SCHOOLHOUSE_STORAGE_BACKEND=json export SCHOOLHOUSE_STATE_DIR=/tmp/doris-schoolhouse-state export UPLOAD_TMP_DIR=/tmp/doris-schoolhouse-uploads PYTHONPATH=. python3 app/scripts/process_recording.py ``` If `ffmpeg` is not installed, the worker records a clean `blocked` result with the exact conversion command it wanted to run. ## Local validation ```bash cd /home/fizzlepoof/repos/truenas-stacks/home/doris-schoolhouse python3 -m py_compile app/main.py app/config.py app/models.py app/routes/*.py app/services/*.py python3 - <<'PY' import yaml from pathlib import Path print(yaml.safe_load(Path('docker-compose.yaml').read_text())['services'].keys()) PY ``` ## Container build The source now includes a `Dockerfile` so the app can run with its own dependencies instead of relying on the host. That image includes: - Python dependencies from `requirements.txt` - `ffmpeg` - `curl` for healthchecks So even if the host is missing `ffmpeg`, the containerized runtime can still do WAV → MP3 conversion. ## Deploy shape Expected live deploy flow on NOMAD: ```bash sudo mkdir -p /opt/doris-schoolhouse/data sudo rsync -a --delete --exclude '.env' --exclude 'data/' /home/fizzlepoof/repos/truenas-stacks/home/doris-schoolhouse/ /opt/doris-schoolhouse/ cd /opt/doris-schoolhouse sudo cp .env.nomad.example .env docker compose --env-file .env config docker compose --env-file .env up -d --build ``` That flow is the reference for refreshing the live runtime from repo source. ## Live integration notes Current verified live behavior: - `https://n8n.paccoco.com/webhook/school/intake/upload` accepts Doris Schoolhouse multipart assignment uploads and returned a successful live smoke-test response on 2026-05-15. - `https://n8n.paccoco.com/webhook/class/upload` now succeeds in live smoke testing after upstream workflow and LiteLLM fixes on 2026-05-15. - Direct Whisper testing against `http://10.5.1.16:8786/v1/audio/transcriptions` succeeded with a tiny MP3. - Direct LiteLLM testing against `http://10.5.1.6:4000/v1/chat/completions` now succeeds for model `medium` after the PD route fix. The app now captures webhook HTTP failures as structured responses instead of crashing blindly. ## NOMAD-specific deployment notes On NOMAD, Schoolhouse should run as a standalone service from `/opt/doris-schoolhouse`. Unlike PD stacks, this host does **not** currently use the PD Docker external networks for Pangolin or shared databases. The practical deploy model here is: - publish host port `8091` - point Pangolin/Newt at that host port - reach shared Postgres on PD over `10.5.1.6:5432` Use `.env.nomad.example` as the starting point for the live runtime. ## Deployment references - `DEPLOYMENT.md` — clean NOMAD rollout notes - `.env.nomad.example` — compose-oriented runtime example for NOMAD