# Doris Schoolhouse Desktop-first school operations app for John. ## What it is Doris Schoolhouse is the front door for John's college workflow. It pulls scattered school chores into one place so Doris can handle intake, metadata, archiving, and later grade follow-up without bouncing between D2L, file uploads, recordings, and Paperless by hand. In plain English, this stack exists to answer: - what classwork is due - where a submission or recording should be filed - how that item should be archived and tagged - whether grading/comments later made it back onto the archived record ## What it does Schoolhouse gives John a 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 ## Why it exists The point is not just "another web app." The point is to make school intake boring and repeatable. - D2L is the source for classes, assignments, grades, and comments. - Schoolhouse is the operational layer where uploads, recordings, and metadata are managed. - Paperless is the long-term archive for receipts, transcripts, summaries, and school documents. - n8n handles the workflow glue between intake events and downstream processing. That split keeps John from doing the same clerical nonsense over and over, and gives Doris one consistent place to reason about schoolwork. ## Runtime model - **Repo source:** `home/doris-schoolhouse` - **Live NOMAD runtime:** `/opt/doris-schoolhouse` - **Published app port:** `8091` - **Public path:** `schoolhouse.paccoco.com` behind Pangolin OAuth - **Storage:** shared Postgres in production, JSON fallback for local iteration - **Archive path:** Paperless + n8n school workflows Do not run production from an agent workspace. Repo source is for editing; `/opt/doris-schoolhouse` is the live runtime. ## How it works - FastAPI - Jinja2 templates - shared Postgres - Pangolin-authenticated public route - existing Paperless and n8n school workflows - bundled `ffmpeg` for recording conversion inside the container Typical flow: 1. D2L data is synced into Schoolhouse. 2. John uploads an assignment or recording through Schoolhouse. 3. Schoolhouse normalizes the intake payload and keeps local/runtime state. 4. n8n receives the handoff and pushes the right artifacts into Paperless. 5. Paperless becomes the durable archive. 6. Later grade/comment updates can be reflected back onto the archived record. Programming-class uploads get special handling: multiple source files are bundled into one canonical source archive, with manifest and receipt/listing artifacts so the repo of what was turned in stays coherent. ## Current state Schoolhouse is live on NOMAD and already covers the real workflow, not just a mockup. 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. This repo directory contains: - the FastAPI app and templates - schema/bootstrap helpers - compose/env examples - D2L, Paperless, and 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.30.7:8786/v1/audio/transcriptions` succeeded with a tiny MP3. - Direct LiteLLM testing against `http://10.5.30.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.30.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