# 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 <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:

```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 <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:

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

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