ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
EduCore is a multi-tenant School Management System: schools subscribe
per-student-per-month, and each school gets an isolated workspace (its own
subdomain, users, data, and branding) inside a single shared deployment.
This repo currently implements Phase 0 — Foundation. See
CHANGELOG.md for what shipped and DEPLOYMENT.md
for step-by-step free-tier deployment instructions.README 전문 보기 ↗EduCore is a multi-tenant School Management System: schools subscribe per-student-per-month, and each school gets an isolated workspace (its own subdomain, users, data, and branding) inside a single shared deployment.
This repo currently implements Phase 0 — Foundation. See CHANGELOG.md for what shipped and DEPLOYMENT.md for step-by-step free-tier deployment instructions.
| Layer | Choice |
|---|---|
| Frontend | Next.js 14 (App Router), TypeScript, Tailwind CSS, shadcn/ui-style primitives, TanStack Query, React Hook Form + Zod |
| Backend | Next.js Route Handlers, TypeScript |
| Database | PostgreSQL (Supabase, free tier) + Prisma ORM |
| Auth | Auth.js (NextAuth v5) — credentials + optional Google/Microsoft OAuth, JWT sessions |
| i18n | next-intl (English + French seeded) |
| Monorepo | pnpm workspaces + Turborepo |
| Testing | Vitest (unit/integration), Playwright (e2e) |
apps/web/ Next.js application (marketing site + tenant app)
packages/db/ Prisma schema, migrations, seed script, tenant-scoping
Prisma Client Extension, RLS helper, audit-log helper
packages/auth/ RBAC permission matrix (single source of truth),
password hashing, ORM-independent Role mirror
packages/ui/ Design-system primitives (shadcn/ui-style): button,
input, select, table, dialog, alert-dialog, dropdown,
empty state, skeleton. Presentational only.
searchParams with parseListParams() (apps/web/src/lib/list-params.ts,
which whitelists sort columns and filter values), then render
ListToolbar + ListSearch / ListFilter + SortableHeader +
ListPagination from components/list/, an EmptyState when there are
no rows, and a loading.tsx with TableSkeleton. See /audit-log.lib/format.ts
(formatMoney, formatDate, formatDateTime, todayInTimeZone) with
the school's settings from getSettingsForUser() — never toLocaleString()
or a hard-coded ₦. School settings are validated by
lib/tenant-settings.ts and fall back to safe defaults field by field.ActionResult (lib/action-result.ts) — never
throw expected errors. Destructive actions use ConfirmAction.auditedMutation()
(packages/db/src/audit.ts) with auditContextFor(ctx) from
lib/guard.ts: the change and its audit entry commit in one RLS-bound
transaction, and secrets (passwordHash, tokens) are redacted from the
before/after snapshots.FormField
(components/form/form-field.tsx), which wires labels, hints, inline
errors and aria-* attributes.apps/web/src/lib/imports/: an Importer (see
types.ts) only validates one row and writes one row; the engine does
parsing (UTF-8/Windows-1252, ,/;), header aliases, limits, in-file
duplicates, per-row SAVEPOINTs and auditing. Upload = validate only
(/imports → report); confirming sends educore/import.requested to
Inngest, which imports in batches of 50 (lib/inngest/run-import.ts).
Imports upsert on natural keys, so re-importing never duplicates./api/exports/{students|staff|parents}) use the list page's
filters and row scope, and the same column names as the import templates.Two independent layers enforce tenant isolation, so a bug in one does not break the other:
packages/db/src/tenant-scope.ts exports
forTenant(tenantId), a Prisma Client Extension that transparently
injects tenantId into every where/data clause for every
tenant-owned model. Request-handling code never uses the bare prisma
client directly — it calls forTenant() (via requireUser() in
apps/web/src/lib/guard.ts) and gets back a client that literally
cannot cross a tenant boundary by omission.packages/db/prisma/migrations/0002_rls enables
PostgreSQL Row Level Security with FORCE ROW LEVEL SECURITY on every
tenant-owned table, keyed on the session variable app.tenant_id. This
protects against a bug in layer 1, a future service that talks to
Postgres directly, or a hand-written raw query. See the known
limitation below.PLATFORM_ADMIN is the only role that operates above tenant isolation
(via platformPrisma()), and every platform-admin action must be paired
with an audit log entry.
How the database layer is enforced: tenant requests run as the
educore_app Postgres role, which has no BYPASSRLS (migration
0004_rls_app_role). forTenant() wraps every operation in a short
transaction that sets app.tenant_id and SET LOCAL ROLE educore_app;
withRls() does the same for multi-statement work (jobs, imports, raw SQL).
Both settings are transaction-local, so they are safe on pgbouncer/Supavisor
pooling. The postgres role (which does bypass RLS on Supabase) is used only
by platformPrisma() for audited platform-admin work, by Auth.js, and by
migrations. Don't call $transaction on a forTenant() client — use
withRls().
Platform tables (tenants, subscriptions) are read-only for a school's
session, with one exception: tenants."onboardingDismissedAt" (column-level
grant + own-row policy, migration 0008). Anything else on a school's
tenant row — plan, status, settings, branding — changes only through the
audited platform path.
Roles: PLATFORM_ADMIN, SCHOOL_ADMIN, TEACHER, STUDENT, PARENT,
ACCOUNTANT. The single source of truth is
packages/auth/src/permissions.ts (PERMISSION_MATRIX + can()), unit
tested in packages/auth/src/permissions.test.ts. Every role — including
PLATFORM_ADMIN — has read-only access to the audit log; nobody can
edit or delete an audit entry, by design.
pnpm install # installs deps, generates Prisma client (postinstall)
cp .env.example .env # fill in DATABASE_URL / DIRECT_URL / NEXTAUTH_SECRET
pnpm db:migrate # applies migrations to your database
pnpm db:seed # seeds demo data (Greenfield Academy)
pnpm dev # starts the Next.js app
See DEPLOYMENT.md for how to get a free Postgres database (Supabase or Neon) and the rest of the free-tier stack.
The seed script creates one demo school, Greenfield Academy, with the
2026/2027 academic year, 2 classes (Grade 5-A, Grade 6-A), 12 students, 3
teachers, 1 school admin, 1 bursar, 1 parent, plus a platform admin — and
fee items, a three-term fee schedule, discounts and bus/lunch sign-ups. Every seeded user
shares the password Passw0rd!23.
| Role | |
|---|---|
| Platform admin | [email protected] |
| School admin | [email protected] |
| Teacher | [email protected] |
| Bursar (accountant) | [email protected] — fees, billing, payments |
| Parent | [email protected] |
pnpm test # Vitest — RBAC permission matrix, tenant-isolation, business logic
pnpm --filter web test:integration # invoicing & payments end to end (needs DATABASE_URL)
pnpm test:e2e # Playwright — login + dashboard smoke tests
Database-backed suites (packages/db/tests/*, apps/web/src/**/*.integration.test.ts)
need a Postgres with every migration in packages/db/prisma/migrations
applied, reached through DATABASE_URL as the database owner. A throwaway
local Postgres 16 is enough: createdb educore_test, then
for f in packages/db/prisma/migrations/0*/migration.sql; do psql educore_test -f "$f"; done.
The tenant-isolation suite (packages/db/tests/tenant-isolation.test.ts) is
the most important test in this repo: it proves Tenant A cannot read or
write Tenant B's data through the scoped Prisma client.
lib/onboarding.ts, pure + unit-tested): steps
are computed from counts gathered in lib/onboarding-data.ts, never
ticked by hand. To add a step: add its id, done-rule, prerequisites and
links there, plus onboarding.steps.<id> text in both message files.lib/grading.ts (gradeFor, roundScore, validators) and
lib/current-term.ts / lib/terms.ts. Score components are
AssessmentType rows whose weight is marks out of 100.lib/attendance.ts; who may take which register is
registerSectionIdsFor / loadRegisterSection in
lib/attendance-data.ts — use them for any new attendance screen or
export. Write attendance only through saveRegister (audits each change).lib/results.ts (pure) via lib/gradebook.ts (one gradebook) and
lib/class-results.ts (a class's term results — also for report cards).
Families may only see a term that has a ResultPublication row. Write
scores only through saveScores (audited, refuses published terms).ReportCard.snapshot
(versioned, lib/report-card.ts) by the generate-report-cards Inngest
function; PDFs are drawn from snapshots (lib/report-card-pdf.tsx, Noto
Sans in assets/fonts). Never render a card from live data. Families may
only download when the class's term results are published./api/students/{id}/profile, Phase 10.0): one
branded PDF of a pupil's whole time at school, built live. Which sections a
viewer gets is decided only by profileSections() in lib/pupil-profile.ts
(role × plan module, unit-tested); the data comes from
lib/pupil-profile-data.ts through the viewer's tenant-scoped client and
studentScopeFor(), and every download is audited (PupilProfileReport).
Families get published results and released homework marks only; health is
alerts only, never the record.fixed footer anchored with bottom when
the page sets a lineHeight, so place footers with
top: footerTop(PAGE_HEIGHT.A4, …) from lib/report-card-pdf.tsx.lib/timetable.ts;
the database enforces the same slot rules (migration 0013), so map a
unique-violation (P2002) on save to "just booked by someone else".parseFloat for amounts. Parse with
toMinor() and compute in integer minor units (kobo/cents) with
lib/fees.ts; store with fromMinor(); display with formatMoney().
What a student is billed for a term comes only from computeBill() via
billsFor() (lib/fees-data.ts) — the bill preview, the billing preview
and the background billing run all use it.lib/invoice-writer.ts
(createInvoice, recordPayment, reversePayment, cancelInvoice,
addAdjustment) inside the school's RLS transaction: it locks the invoice
row, issues gap-free numbers from number_sequences, recomputes
amountPaid/status from the payments and writes the audit entry. Rules are
pure in lib/invoicing.ts. Payments are append-only in the database (the
app role has no UPDATE/DELETE): a mistake is undone with a reversal row.
OVERDUE is derived (displayStatus), never stored.lib/payments/): startOnlineCheckout records the
attempt, then sends the parent to Paystack; money is applied ONLY by
settleOnlinePayment(reference), which asks Paystack server-to-server and
is idempotent (the return page, the signed webhook and "check again" can
all call it). Rules are pure in settle-rules.ts. Anything that doesn't
match exactly — or arrives after the invoice was settled — becomes
NEEDS_REVIEW for the bursar instead of being applied.ctx.isPlatformAdmin and use platformPrisma(); every platform action
writes recordPlatformAudit(). Who a request acts as comes ONLY from
getEffectiveSession() (lib/session.ts): while a platform admin is signed
in as a school admin, it returns the school admin and requireUser() gives a
tenant-scoped client — so never read auth() directly. Things support must
not do (credentials, billing) call forbidWhileImpersonating(ctx).apps/web/messages/{en,fr}.json; a unit test fails
if the two files ever have different keys or placeholders.