8.4 KiB
Doris Schoolhouse — Technical Spec
Goal
Build a desktop-first local web app on NOMAD for school intake and tracking.
Primary jobs:
- upload assignment turn-ins
- upload class recordings
- suggest metadata from D2L
- confirm metadata before Paperless submission
- transcribe and summarize recordings
- track assignment lifecycle through grading
- append grades and professor comments into Paperless notes
Deployment shape
- Source path:
home/doris-schoolhouse - Live NOMAD runtime target:
/opt/doris-schoolhouse - Access: local + Pangolin-authenticated
- Networks:
pangolin,ix-databases_shared-databases - DB: shared Postgres
- Archive/document system: Paperless-NGX
- Workflow helpers: existing n8n workflows for school intake and class recording processing when appropriate
Source of truth
| Domain | Source of truth |
|---|---|
| Course roster | D2L sync cache |
| Assignment roster / due dates | D2L sync cache with manual override support |
| Submission workflow state | Doris Schoolhouse DB |
| Uploaded files / transcripts / summaries | Paperless |
| Grades / professor comments | D2L on refresh, cached in Doris Schoolhouse DB |
The app DB owns workflow state and linkage. Paperless owns archived artifacts.
Existing reusable assets
D2L
Existing workspace assets already prove the login/scrape path:
~/.openclaw/workspace/school/d2l_scraper_final.js~/.openclaw/workspace/school/d2l_school_watcher.js~/.openclaw/workspace/school/d2l_current_snapshot.json~/.openclaw/workspace/school/d2l_grades.json
Current confirmed D2L coverage:
- courses
- grades
- grade items
- some material/date counts
- calendar/ICS-derived schedule
Gap to close for Schoolhouse:
- normalize D2L data into app DB tables
- stronger assignment/date extraction
- deterministic matching between D2L assignment rows and Paperless-backed submissions
Paperless / n8n
Existing repo assets:
n8n-workflows/08-class-recording-rag-v1.2.jsonn8n-workflows/18-school-paperless-intake-v1.2.jsonn8n-workflows/19-paperless-school-metadata-enrichment-v1.1.jsondocs/operations/SCHOOL_PAPERLESS_INTAKE.mddocs/reference/SCHOOL_INTAKE_POSTGRES_SCHEMA.sql
Current confirmed behavior from docs/testing notes:
- school uploads can already be pushed to Paperless with deterministic filename/title correlation
- class recording pipeline already has a webhook path and downstream Paperless/RAG behavior
- workflow 19 already does deterministic post-consumption enrichment
App architecture
Use a server-rendered Python app for the first build:
- Backend: FastAPI
- Templates: Jinja2
- Frontend: vanilla JS + server-rendered forms/pages
- DB access: SQLAlchemy / psycopg
- Background work: app jobs can hand off long-running audio processing to existing n8n recording/transcription pipeline
Why this split:
- easier large-upload handling
- easy form-heavy UI
- less frontend build nonsense
- plays nicely with existing Python-heavy repo bits
Core pages
1. Dashboard /
Shows:
- upcoming assignments
- unsubmitted assignments
- recent uploads pending confirmation
- newly graded work
- recording processing status
- sync health
2. Assignment Upload /assignments/upload
Form fields:
- class
- assignment
- submitted date
- version
- notes
- file upload / drag-drop / paste
Behavior:
- choose file
- app suggests class + assignment from D2L cache
- user confirms/edits metadata
- upload goes to Paperless intake path
- app stores linkage row
- assignment state becomes
submitted
3. Recording Upload /recordings/upload
Form fields:
- class
- session date
- optional related assignment
- notes
.wavupload
Behavior:
- accept large WAV
- persist upload job row
- convert WAV → MP3
- delete WAV only after MP3 success
- hand MP3 to transcription/summarization flow
- send transcript + summary to Paperless
- store resulting Paperless document links
4. Classes /classes and /classes/{id}
Shows:
- assignments
- due dates
- grades
- comments
- linked submissions
- linked recordings
5. D2L Sync /admin/d2l
Shows:
- last sync time
- sync status
- changed courses/assignments/grades
- unmatched rows requiring manual mapping
API surface
Health
GET /api/health
D2L
POST /api/d2l/syncGET /api/d2l/coursesGET /api/d2l/assignments?course_id=GET /api/d2l/grades?course_id=
Assignments
GET /api/assignmentsPOST /api/assignments/intakePOST /api/assignments/{id}/confirm-paperlessPOST /api/assignments/{id}/status
Recordings
GET /api/recordingsPOST /api/recordings/intakePOST /api/recordings/{id}/reprocess
Paperless linkage
POST /api/paperless/webhook/intake-enrichedPOST /api/paperless/webhook/recording-completePOST /api/paperless/assignments/{id}/append-grade-note
Data model
schoolhouse_course
- id
- d2l_course_id
- name
- code
- semester
- instructor_name
- active
- first_seen_at
- last_synced_at
schoolhouse_assignment
- id
- course_id
- d2l_assignment_key
- title
- category
- due_at
- status (
assigned|in_progress|submitted|graded|late|missing) - grade_text
- grade_numeric
- professor_comments
- source (
d2l|manual|template) - created_at
- updated_at
schoolhouse_submission
- id
- assignment_id
- version_label
- original_filename
- mime_type
- checksum_sha256
- submitted_at
- suggested_metadata_json
- confirmed_metadata_json
- paperless_document_id
- paperless_title
- paperless_note_appended_at
- status (
draft|uploaded|confirmed|failed)
schoolhouse_recording
- id
- course_id
- assignment_id nullable
- session_date
- original_filename
- mp3_filename
- duration_seconds
- transcription_status (
queued|processing|ready|failed) - summary_status (
queued|processing|ready|failed) - transcript_paperless_document_id
- summary_paperless_document_id
- recording_paperless_document_id nullable
- notes
schoolhouse_d2l_sync_run
- id
- started_at
- completed_at
- status
- raw_snapshot_path
- changes_summary_json
- error_text
schoolhouse_assignment_mapping
- id
- course_id
- d2l_grade_item_name
- normalized_key
- assignment_id
- confidence
- mapping_source (
auto|manual)
This table is the bastard that keeps D2L grade items tied to internal assignments cleanly.
Matching rules
Assignment matching priority
- exact D2L assignment key when available
- normalized title within course
- filename heuristics
- latest due date proximity
- manual confirmation
Grade/comment update policy
- D2L refresh updates cached grade/comment fields for mapped assignments
- if a submission has a Paperless document ID and grade/comment changed, append note once per distinct change hash
- never overwrite user notes in Paperless; append a dated Schoolhouse block
Recording pipeline contract
Input limits
- accept WAV up to at least 1 GB
- support up to 90 minutes
- reject unsupported formats with clear message
Processing steps
- store upload metadata row
- convert WAV → MP3
- delete WAV only after MP3 success
- submit MP3 to recording workflow
- receive transcript/summary callback
- archive transcript + summary into Paperless
Secrets/config
Required env:
SCHOOLHOUSE_HOSTSCHOOLHOUSE_PORTDATABASE_URLPAPERLESS_BASE_URLPAPERLESS_API_TOKENN8N_BASE_URLSCHOOLHOUSE_N8N_ASSIGNMENT_WEBHOOKSCHOOLHOUSE_N8N_RECORDING_WEBHOOKD2L_SCRAPER_WORKDIRD2L_SCRAPER_COMMANDUPLOAD_TMP_DIRMAX_UPLOAD_MBSESSION_SECRET
Do not commit live values. Use .env.example only.
Repo / deployment plan
Repo source path:
home/doris-schoolhouse
Expected live NOMAD runtime path later:
/opt/doris-schoolhouse
Expected live data path later:
/opt/doris-schoolhouse/data
Expected compose validation before deploy:
cd /opt/doris-schoolhouse
docker compose --env-file .env config
Build phases
Phase 1
- app scaffold
- schema
- D2L import command/service
- assignment upload UI
- Paperless confirmation flow
Phase 2
- recording upload flow
- MP3 conversion
- recording job tracking
- transcript/summary callbacks
Phase 3
- grade sync
- Paperless note append logic
- dashboard views
Phase 4
- manual mappings UI
- recurring assignment templates
- mobile cleanup
Verification checklist
- Python modules compile
- schema file present and readable
- compose file parses
- route map loads without import failure
- README explains runtime/deploy expectations
- no secrets committed