ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
Practice times tables and track retention as a growing forest
Translation pending
AI summary · website content
EducationHTML · JavaScriptGitHub @si4starOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A times table practice app for children aged 7–9. Each fact is a tree on an 11 × 11 grid (2–12 × 2–12; 6 × 7 and 7 × 6 are the same tree, so 66 trees). A tree only grows when the fact is recalled correctly on the day it is due, so the forest shows retention rather than time played.
Runs on Cloudflare Pages, with Pages Functions for the API and D1 for storage. It's a PWA: installable, and it updates itself when a new version is published.
This repo is The Learning Forest (learn.thetreefella.co.uk), free learning modules from The Tree Fella:
| Path | Module |
|---|---|
/ | Portal and website for schools, school pack |
/trees/ | How does a tree work? (one page, no login; see How does a tree work? below) |
/app/ | The Learning Forest app: one login (Forest Pass for pupils, email for grown-ups), classes and class devices for every module. After logging in, a pupil picks a module. |
| (in the app) | Grow your times tables: the module described below |
/tables/ | Old address: forwards to /app/ |
/soil/ | What is soil? (coming soon) |
MightyOak42otter-river-lemon1234-5678#qr= (the part after # never reaches the server or its logs). Scanning it with the device's camera app opens the site logged in. Inside the installed app, Scan my Forest Pass on the login screen uses the camera directly (needed on iPhone/iPad, where the camera app opens Safari rather than the installed app). Only a hash of the key is stored. Grown-ups can make a New QR card (the password stays the same; the old QR stops working); a new Forest Pass or recovery also replaces it. Bad scans are limited to 30 per internet address in 15 minutes (higher than passwords, because a school shares one address). Children added before QR login get a code by making a New QR card.Data held: grown-up email; child first name or nickname; facts progress; settings (season); forest friends; practice check scores; for reminders, the device's push address, time and time zone; every answer (fact, answer given, right/wrong, time taken, kind). Failed login records are deleted after a day. The font is self-hosted, so the page makes no third-party requests.
A grown-up account can run classes (up to 10, up to 40 pupils each), alongside its own children.
On first login the child taps the tables they think they know.
npm run build copies public/ to dist/ and stamps a version (the Git commit on Cloudflare) into the page, the script and style URLs, and the service worker.sw.js and the manifest are never cached (see public/_headers).public/app/splash/, one per screen shape in portrait and landscape, matched by the apple-touch-startup-image tags in index.html.background_color and 512 px icon.public/app/icons/icon.svg: node scripts/make-images.mjs.There are four forest seasons, spring, summer, autumn and winter, plus Christmas. Each changes the colours, the trees and what drifts down the screen.
| Season | Look |
|---|---|
| Spring | Cherry Blossom: pink trees with white and deep-pink flowers, drifting petals, pink buttons |
| Summer | Strawberry Meadow: lush greens, strawberries and wild strawberries, floating dandelion seeds |
| Autumn | Warm cream, orange and gold trees, red apples, falling leaves |
| Winter | Darkest: night blue with dark pine trees, snow on the treetops and ground, red berries, dim falling snow |
| Christmas | Winter's night with decorated trees: twinkling red, gold and blue baubles on mature trees, gold stars on young and mature trees, holly-red buttons |
extra.xmasOff, the year it began); it comes back the next December. Picking Christmas again in that time keeps their own season for after 6 January. Outside that time Christmas is an ordinary choice.6 × ? = 42 or 42 ÷ 6 (a third each), so the whole fact family is practised. The two numbers come in either order, so 42 ÷ 6 and 42 ÷ 7 are both asked. A wrong answer walks through the multiplication; after a wrong division the last step is the division itself ("6 × 7 = 42. So how many 6s make 42?").lf-due-<username> in localStorage), so on a second device the bar starts again from what's left. With nothing due it becomes Extra practice.Pages Functions can't run on a schedule, so a small separate Worker sends the reminders (workers/reminders/, every 15 minutes). It's on the Workers Free plan.
workers/reminders/wrangler.toml: binding DATA and the database ID, the same database the Pages project binds as DATA. If the database changes, copy the new ID from Storage & Databases → D1.si4star/learning-forest. Set Root directory to workers/reminders. Leave the build command empty. Deploy.VAPID_PRIVATE_JWK with the private key (supplied separately, never committed). Redeploy.SITE_URL in wrangler.toml is the site's address (https://learn.thetreefella.co.uk). Change it if the site moves.The matching public key is in src/server/push.js and public/app/js/app.js (VAPID_PUBLIC_KEY). If the key pair is ever replaced, change both and existing devices must turn reminders off and on again.
HINT_ORDER (10, 2, 12, 5, 11, 4, 3, 9, 8, 6, 7). The next table unlocks once every fact in the unlocked tables is planted and 80% have reached sprout.10 × 7 = 70 → 70 − 7 = 63. 8 × 6 is three doubles: 12 → 24 → 48. A wrong step shows its answer and the child types it. Steps are defined in walkSteps() in public/app/js/app.js, using the same strategy choice as the text hints.Rounds are 20 questions (about 4 minutes); the first is shorter. ×1 facts are excluded.
The screen stays on during a round and the starting check, using the browser's Screen Wake Lock API. It's released as soon as the round ends or the app is closed. Devices without support (iPhone/iPad home-screen apps before iOS 18.4, some older browsers) dim as normal.
Progress is saved after every answer. If the connection drops, answers queue on the device and send when it's back.
si4star/learning-forest.
mainnpm run build. Build output directory: distlearning-forest).DATA, and the database. Add it for both Production and Preview.Flagged: move to Workers Paid ($5/month) and strengthen password hashing.
To upgrade:
PBKDF2_ITERATIONS = 100000 for Production and Preview. Then redeploy.No data migration is needed. Each stored hash records its own iteration count, so existing passwords still work and are re-hashed at the new strength the next time that person logs in. Recovery codes are re-hashed whenever a new card is issued.
npm install
npx playwright install chromium # once, for the tests
npm run dev # builds, then serves http://localhost:8788 with a local D1 database in .wrangler/
npm test # reminders unit test, then end-to-end tests against the running dev server
# (BASE=... to override; REBUILD="npm run build" also tests the update flow)
| Path | Contents |
|---|---|
public/index.html, public/site.css, public/site-grove.js | Website for schools |
public/school-pack/ | What we store, parents' privacy notice, data processing agreement (drafts) |
public/site.js | Sends old QR links (/#qr=) and old home-screen installs to /app/ |
public/sw.js, public/tables/sw.js | Retire the service workers from before the app's moves |
public/tables/index.html, public/moved.js | Send /tables/ links and QR cards on to /app/ |
public/trees/ | How does a tree work?: the imported page, its script, fonts (Atkinson Hyperlegible, SIL OFL) |
scripts/import-trees.mjs | Imports a new version of the How does a tree work? page |
public/_partials/header.html, footer.html, public/chrome.css | The Learning Forest header and footer. scripts/build.mjs puts them into every website page in place of <!-- lf:header --> and <!-- lf:footer -->, so they're changed in one place. |
public/404.html | Not-found page (without it, Pages serves the home page for unknown paths) |
public/app/index.html | App page shell and tree SVG symbols (the app's service worker only serves this page for /app/ itself) |
public/app/sw.js | Service worker: offline app shell, versioned caches |
public/app/manifest.webmanifest, public/app/icons/ | PWA manifest and icons |
public/_headers | Cache and security headers (Cloudflare Pages) |
scripts/build.mjs | Copies public/ to dist/ and stamps the version |
public/app/css/styles.css | Styles, the seasons and Christmas, print layout for the Forest Pass |
public/app/js/app.js |
/) is plain HTML and CSS sharing the app's font and colours. Each module is a row that expands to show what it is (<details>; /#tables opens that one). "For teachers" covers accounts, classes, Forest Passes, home use and end of year, since those are shared by every module. The school pack pages print cleanly and are drafts: highlighted placeholders (address, contact email, database region, retention periods) must be filled in, and the processing agreement needs legal review.renderHub in app.js); Grow your times tables opens the forest, which has a back button to the picker. The module screen's cog opens the app's settings (season, reminder, log out); the forest screen has the same cog. Modules without a login (How does a tree work?) open from the picker at /app/<module>/: the build makes that copy from the website page, with a back bar to the modules in place of the website header and footer (public/_partials/app-header.html), so an installed app stays in its own window. Guests see the website version at /<module>/. A new module with a login adds a card to the picker, its own screens, and its own progress tables in a new migration./, then /app/, then /tables/, and is back at /app/ (scope /app/). Old installs and cards keep working: public/sw.js (also served as /tables/sw.js) replaces an old service worker, deletes its ttf-/tables- caches, unregisters itself and reloads open pages; site.js and /tables/index.html send them on to /app/, keeping any #qr= key. Installs from the first /app/ days pick up the current service worker directly. The app's caches are named lf- so the retiring workers leave them alone (caches are shared across the whole site).The page is written outside this repo as a single HTML file. To publish a new version:
node scripts/import-trees.mjs path/to/index.html
The import adapts it to this site: the inline script moves to public/trees/trees.js (the security policy runs no inline scripts), Google Analytics and its cookie banner come out (the school pack promises no tracking), Google Fonts become self-hosted copies, addresses move from / to /trees/, the sharing image and logo come out, and The Learning Forest's header and footer replace the page's own. Its section links and progress bar stay as a sticky bar under the site header, and the book credits move into its closing section. Each change must match exactly once, so if the page's structure changes the import stops with the step that failed rather than publishing a half-converted page.
/admin/)Usage numbers and the privacy requests the school pack promises. Setup: Pages project → Settings → Variables and Secrets → add ADMIN_EMAILS (plain text) with the grown-up account email(s) allowed in, comma-separated, for Production. Redeploy. Then log in at /app/ as that grown-up and open /admin/. Anyone else gets "not an admin". Optional second lock: Cloudflare Zero Trust → Access → protect learn.thetreefella.co.uk/admin* with a one-time email code.
admin_log table)./api/admin/* in src/server/api.js; page: public/admin/. Tests: tests/admin.mjs (server), tests/admin-e2e.mjs (browser). npm run dev sets [email protected] locally.workers/reminders/, every 15 minutes) also runs purgeInactive(): a pupil who hasn't played for 12 months (or was added 12 months ago and never played) is deleted with their progress, answers, reminders and logins. A grown-up account with no login for 12 months (parents.last_seen, updated on login and on opening the app) is deleted once it has no pupils left.idleWarning() in app.js). There is no email warning, as the app doesn't send email.tests/retention.mjs.src/server/schema.js: a password_resets(token_hash, parent_id, expires_at, used_at) table. Store only a hash of the token, as sessions do.POST /api/parent/forgot {email} always replies the same way, so it doesn't reveal which emails have accounts, and is rate-limited with the existing guard()/fail() helpers. POST /api/parent/reset {token, password} sets a new hash and deletes the parent's sessions, as child resets already do./app/?reset=<token>). The service worker already serves the app for any URL.The first Cloudflare version started fresh: progress from the earlier device-only version was not imported. That old data is deleted from each device the first time it opens the new version.
extra.acorns, extra.acornDay). Every 7 plant a special tree in the special grove (Grove in the bottom bar): an oak, a rowan, a silver birch, then round again. They're kept if a grown-up redoes the starting check. Pupils already playing when acorns arrived were given 2 each (migration 11, extra.acornGift, counted with earned ones); only the server sets it, and it's kept whatever a device sends. Grown-ups see each child's count, and special trees, under the child's name (family and class lists).reset_at (migration 12); a save from a device that loaded before it is refused (409) and that device starts again from the server. Individual trees are saved as answered, so an old copy can still set a tree it answered back a stage; reloading on return makes that rare.| Client: screens, scheduling, rounds, starting check, sync queue |
public/app/fonts/ | Fredoka (SIL Open Font License) |
public/app/js/vendor/ | QR code drawing (qrcode-generator, MIT) and reading (jsQR, Apache-2.0), loaded only when needed |
functions/api/[[path]].js | Pages Functions entry for /api/* |
src/server/api.js | API routes |
src/server/auth.js | Password hashing and session tokens |
src/server/words.js | Username, password and recovery code generation |
src/server/schema.js | D1 schema and migrations |
src/server/push.js | Web Push (VAPID) signing and sending |
workers/reminders/ | Scheduled Worker that sends daily reminders |
tests/e2e.mjs | End-to-end test: accounts, starting check, rounds, sync, PWA |
tests/acorns.mjs | The gift of 2 golden acorns: added to earned ones, kept through device syncs, shown to grown-ups |
tests/sync.mjs | Two devices: an older copy never takes progress backwards; a fresh start by a grown-up is kept |
tests/features.mjs | End-to-end test: shapes, friends, streak, bests, practice check, tricky facts, reminders, offline |
tests/reminders.mjs | Reminders Worker: timing rules and VAPID signature |
public/admin/ | Admin dashboard page (see Admin dashboard) |
tests/admin.mjs, tests/admin-e2e.mjs | Admin dashboard: access, stats, export without secrets, deletion, log |
tests/retention.mjs | Deleting idle pupils and unused accounts; deleting your own account |
tests/qr.mjs | QR login on the server: keys, new QR card, old keys, rate limit |
tests/qr-login.mjs | QR login in the browser: card QR decodes, link login, camera scan, bad and replaced codes |
tests/classes.mjs | Classes on the server: class devices, picture login, lockouts, ownership |
tests/classes-e2e.mjs | Classes in the browser: add pupils, login cards, class device, picture login, I'm done |
tests/site.mjs | Website pages, old QR links, retiring the old service worker |
tests/d1-shim.mjs | D1 stand-in over node:sqlite for the Node tests |