Files
truenas-stacks/home/doris-schoolhouse

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:

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

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:

bin/check-env.sh
bin/apply-postgres-schema.sh
bin/test-assignment-handoff.sh <file> [more_files ...] <class_name> [assignment_name]
bin/test-recording-handoff.sh <file> <class_name>

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:

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:

bin/bootstrap-local.sh
bin/run-local.sh
bin/process-recording.sh <recording_id>

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:

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:

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 <recording_id>

If ffmpeg is not installed, the worker records a clean blocked result with the exact conversion command it wanted to run.

Local validation

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:

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