ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
A web-based classroom banking and behavior management system using Flask + PostgreSQL
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A classroom management platform that uses a simulated token economy to drive student engagement and participation. Built with Flask + SQLAlchemy + PostgreSQL, designed for multi-tenant deployment across multiple schools and class periods.
Version: v2.0 (pre-launch; unreleased changes are tracked under [Unreleased] in the changelog) — Branch: main
License: PolyForm Noncommercial 1.0.0README 전문 보기 ↗A classroom behavior management and reward platform with built-in simulated financial products and economic engines (and an absurd amount of documentation, pytests, and opinions.)
Students earn tokens for the time they work, and spend them on rent, insurance and a class store. Teachers run the class economy without handing over student email addresses, phone numbers or school SSO. Built with Flask, SQLAlchemy and PostgreSQL. Each class period is its own isolated economy.
In production: v2.4.1 (44b869e, deployed 2026-10-29 UTC, release record).
Latest tag:
Branch: main
License: Polyform Noncommercial 1.0.0
| Website | classroomtokenhub.com |
| Application | app.classroomtokenhub.com |
| Service status | status.classroomtokenhub.com |
| Developer docs | classroomtokenhub.com/docs |
| Changelog | CHANGELOG.md |
flask create-sysadmin) and manage their own passkeysNo email is involved. A teacher who loses access starts recovery by entering the usernames of students in their classes:
A class with fewer students than that counts if every claimed student is entered. The system then picks students at random in each class to confirm the request from their own accounts, and each confirming student gets a code to hand back. With all the codes, the teacher resets their credentials. Recovery needs at least 3 claimed students in each class, so that the random pick can't be predicted. See DOM-IDEN-003 §IX and FEAT-IDEN-103.
class_id. One user can hold seats in several classes, and each class is its own economyPENDING and becomes posted history only when the scheduled settlement job admits it. The database rejects any change to a posted rowapp/scheduler_ownership.py). flask run, flask commands, migrations and scripts never run them; to exercise jobs locally, start gunicorn wsgi:apptimedelta arithmetic in services, FEATs and routes/docs serves the user guides in docs/user-guides/, with search and a choice of teacher or student view/health for liveness, and /health/status for bounded status signals that expose no tenant data or raw exceptions[!IMPORTANT]
CTH collects as little PII as it can: no email, no phone number, no SSO, no physical location. Student names are encrypted at rest. Usernames are stored only as keyed digests, never as plaintext. If the database leaks, it can't be linked to real people without information held somewhere else.
CTH doesn't support native SSO. A district that needs SSO can fork the project and add its own auth layer; see PRN-SNP-001 for why.
Route ──▶ FEAT (app/feats/) ──▶ Domain services (app/services/) ──▶ commit
User is the global sign-in account. Seat is that user's place in one class, and every activity record keys off seat_id. IdentityProfile holds display names. ClassEconomy is the tenant boundary, identified by class_id, with join_code as its public alias. INV-ARC-019 and DOM-IDEN-001 govern this modelapp/feats/. Routes never call db.session.commit() on domain models, and GET handlers never write to the database (INV-ARC-007)| Component | Where | What it does |
|---|---|---|
| Flask application | app/, templates/, static/, wsgi.py | Teacher, student and sysadmin surfaces; served by gunicorn behind nginx |
| Status service | status_service/, status/ | Public status page and an IAP-protected operator console; deployed by deploy-status.yml |
| Developer docs site | docs-generator-proof/ | Hugo + Hextra build of the documentation tree |
| Public site | github-pages/ | Marketing pages and the public timeline, published by github-pages.yml |
The application doesn't serve the marketing site, and the marketing site doesn't serve the application.
runtime.txt pins 3.10; CI runs 3.10, 3.11 and 3.13)redis-server) for student account setup. Tests that need it start their own private instance; in production it is a dedicated, non-persistent service (infra/student-setup/README.md)git clone https://github.com/timwonderer/classroom-token-hub.git
cd classroom-token-hub
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# The heredoc delimiter is deliberately unquoted so the $(...) calls run.
# With 'EOF' quoted, each key gets the literal command text as its value.
cat > .env << EOF
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
DATABASE_URL=postgresql://user:password@localhost:5432/classroom_economy
ENCRYPTION_KEY=$(openssl rand -base64 32)
PEPPER_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
AUDIT_HMAC_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
FLASK_ENV=development
EOF
flask db upgrade
flask create-sysadmin # follow the prompts and scan the QR code with an authenticator
flask run # http://localhost:5000
The app won't start without the six keys above. Everything else is optional, except that student account setup also needs STUDENT_SETUP_REDIS_URL:
| Variable | Purpose |
|---|---|
CONFIGURATION_EXPORT_KEY | Dedicated random signing key for class configuration files and confirmations; unset disables transfers. Rotation immediately invalidates all outstanding files and confirmations; no fallback keys |
TEST_DATABASE_URL | PostgreSQL database that the test suite rebuilds |
CSRF_SECRET_KEY | A separate CSRF signing key (defaults to SECRET_KEY) |
SECRET_KEY_FALLBACKS | Previous secret keys, so a rotation doesn't sign everyone out |
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY | Cloudflare Turnstile; verification is skipped when these are unset |
PASSWORDLESS_API_KEY, PASSWORDLESS_API_PUBLIC, PASSWORDLESS_API_URL | Passkey sign-in |
REDIS_URL, RATELIMIT_STORAGE_URI, DEV_ENABLE_RATELIMIT | Rate-limit storage; rate limits are off in development unless DEV_ENABLE_RATELIMIT=1 |
STUDENT_SETUP_REDIS_URL | Dedicated, non-persistent Redis for student account setup (infra/student-setup/README.md). Required: student claim and recovery setup refuse to run without it. Never point it at the rate-limit store |
EXTERNAL_DOCS_BASE_URL, MARKETING_SITE_URL, STATUS_PAGE_URL, GRAFANA_URL, SUPPORT_EMAIL | External links |
LOG_LEVEL, LOG_FILE | Logging |
Don't rotate ENCRYPTION_KEY or PEPPER_KEY casually. ENCRYPTION_KEY protects PII and TOTP secrets and needs a re-encryption to change. PEPPER_KEY keys the username digests, so changing it resets identities. To run your own instance, start with docs/self-hosting/.
Tests run against a real PostgreSQL database named by TEST_DATABASE_URL; there's no SQLite path. conftest.py builds the schema through the real migration chain once per session and gives every test a fresh copy of it, so triggers and constraints are live in every test. The test database must be local and named as a test database, and the role needs CREATEDB for the fast path; see CONTRIBUTING.md.
pytest tests/dom/obligations/ -v # one domain
pytest tests/test_status_contracts.py -v # one file
pytest -k recovery # by pattern
pytest # full suite: 3,900+ tests, over an hour
pytest --cov=app tests/ # with coverage
Tests provision a whole classroom through tests/helpers/classroom_initializer.py rather than creating rows by hand (SPEC-TEST-001).
Templates are held to the design-token contract. While editing one, check just that file:
python scripts/lint_design_tokens.py templates/your_page.html
flask db heads # must show exactly one head
flask db migrate -m "Add X to Y"
flask db upgrade
flask db history # find the revision to go back to
flask db downgrade <revision> # always pass an explicit revision
python scripts/lint_migrations.py --baseline migrations/lint_baseline.txt
[!WARNING]
A bare
flask db downgradestops withAmbiguous walkwhen the current revision is a merge point, and rolls nothing back. Always name the target revision, then confirm it withflask db current.
Every migration needs the idempotency helpers from migrations/migration_template.py.mako and has to pass the linter. migrations/lint_baseline.txt records older debt, can only shrink, and is never the place for a new migration. SOP-DB-001 is the governing specification.
Production is released by the Release v2 to Production workflow (release-v2.yml), which is started by hand with an exact 40-character commit SHA. The workflow:
main)flask db upgrade, and restarts the service/health on gunicorn (127.0.0.1:8000) until it answers okcurl http://localhost:5000/health # 200 "ok" if the database answers SELECT 1
curl http://localhost:5000/health/status # bounded status signals; no tenant data
gunicorn wsgi:app --workers 4 --bind 0.0.0.0:8000
The full procedure is SOP-DEP-002. Each release has a dated record under docs/ops/audits/, except six untagged releases from before the recording procedure was settled: bc5c07a2, eaca2a7e, 29b99b14 and 00166e56 (2026-09-29), and 35bad089a and 1cdbf03da (2026-10-05 and 2026-10-06). All six are covered by the CHANGELOG, and the later records note the gap. Each tagged release has GitHub release notes and a section in CHANGELOG.md. The release in production is named at the top of this page.
Evidence that was run is not the same as coverage that was inferred (INV-ARC-017). This page does not keep a snapshot of what production has and hasn't exercised, because a snapshot goes stale with the next release. The current release record (linked at the top of this page) carries it: what that release verified, its browser validation under SOP-DEP-001B, and its known gaps and follow-ups.
When two documents disagree, the one higher in this order wins, and the lower one gets corrected:
INV-CORE → INV-ARC → DOM-* → FEAT-*
Normative (these govern):
| Document | Purpose |
|---|---|
| docs/INVARIANT/ | Core runtime invariants and architectural rules |
| docs/DOMAIN/ | Per-domain authority specs |
| docs/FEATURE-EXECUTION/ | FEAT mutation contracts |
| docs/SPEC/ | Technical contracts |
| REF-TERM-001 | Developer vocabulary |
| docs/STANDARD_OPERATING_PROCEDURES/ | Operational procedures |
Descriptive (these summarize and can drift):
| Document | Purpose |
|---|---|
| docs/TRACKING/ | Working state: the post-launch tracker and open decisions |
| docs/ops/ | Production host notes and dated release and audit records |
| docs/PRINCIPLES/ | Why a design was chosen |
| docs/REFERENCE/ | Interface references, including REF-API-001 for HTTP endpoints, and the user-facing vocabulary REF-TERM-002 (recommended, subject to accessibility) |
| DEVELOPMENT.md | Roadmap and current priorities |
| CHANGELOG.md | Version history |
| .claude/CLAUDE.md and .claude/rules/ | Working guidance for AI coding agents |
Nothing under .claude/ is authoritative. It helps agents find their way around the codebase, and it should never be cited to justify a design decision. Cite the INV, DOM, FEAT, SPEC or SOP document instead.
The user guides in docs/user-guides/ are served inside the app at /docs. The developer docs site at classroomtokenhub.com/docs, built with Hugo from docs-generator-proof/, publishes the normative tree plus principles, references, maps and the self-hosting guide. It leaves out the user guides, dated release and audit records (docs/ops/), working-state tracking files, and docs/archive/, which holds superseded material kept for history.
| Line | Status | Where it lives |
|---|---|---|
| v2 | Current. v2.0.0 released 2026-09-26; later releases are on the releases page and in docs/ops/audits/ | main |
| v1 | Retired. v1.10.0 (2026-06-14) was the final v1 release | Branch main_legacy_v1.10.0 and the v1.* tags |
v2 is a ground-up rebuild. It's a clean break: no v1 accounts or data carry over. Upgrading every 2.0.0 deployment to 2.0.1 or later is recommended: 2.0.1 ties each passkey to the account it was registered to.
Read the invariants first, then the domain spec for the area you're changing, then the FEAT contracts that execute it. CONTRIBUTING.md has the details, and pull requests use .github/PULL_REQUEST_TEMPLATE.md.
PolyForm Noncommercial License 1.0.0
Allowed: classrooms, clubs, nonprofits, research and personal learning. Not allowed: commercial products, SaaS, paid services and other for-profit use.
See LICENSE for the full terms and the third-party notices.
This project is developed, deployed, maintained, operated and tested by one full-time high school teacher, who lives by the motto "fine, I'll build one myself."