ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Kenning: a vocabulary and translation PWA. One answer with its alternatives, a plain explanation and example sentences, over open data.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A vocabulary and translation web app. You type a word or a phrase. The app
returns a translation, a plain explanation, and example sentences. You can
collect terms into learning lists. The front door is public: a signed-out visit
to / lands on /welcome, which names the product and offers sign-up and
sign-in, and a signed-in reader sees a real worked example from the dictionary
on /. The public explanation pages at
/browse/explanations are open as well: answered questions their askers did
not hide, readable without an account.README 전문 보기 ↗A vocabulary and translation web app. You type a word or a phrase. The app
returns a translation, a plain explanation, and example sentences. You can
collect terms into learning lists. The front door is public: a signed-out visit
to / lands on /welcome, which names the product and offers sign-up and
sign-in, and a signed-in reader sees a real worked example from the dictionary
on /. The public explanation pages at
/browse/explanations are open as well: answered questions their askers did
not hide, readable without an account. A typed search, entry pages, lists,
history, review, and voice input all need one. Signup is
open: create an account with an email address and a password, because every
explanation the app writes costs the operator a language-model call, and the
account is what the spending caps are measured against. Your lists and history
live on your own device, and what you sync between your devices is stored on
the server as plain JSON, which the operator can read. This repository
contains the entire codebase under the MIT licence. You can host it yourself.
See Self-hosting below, and
ADR-0011 for why the
account model looks the way it does.
worker.ts runs them.openplate-sync, as explained in
ADR-0008; the account model
above it is plain, as explained in
ADR-0011.sources table stores
attribution data..nvmrc file specifies version 24 for development,
and the production image (Dockerfile.pnpm) runs on version 22.packageManager, so
corepack installs the correct version
automatically.pg_trgm extension enabled. Search uses
trigram similarity, which measures text overlap, for the "did you mean"
feature.You do not need a private registry token. All four @sprqvntrs/* dependencies
exist on npmjs.com under the MIT licence. The .npmrc file routes this scope
directly to https://registry.npmjs.org/. Installs work even if your global
~/.npmrc file redirects the scope.
These packages publish uncompiled TypeScript. Because of this, vite.config.ts
includes them in ssr.noExternal. Vite compiles these dependencies instead of
passing them directly to Node.
git clone https://github.com/AltanS/kenning-altan-fyi.git
cd kenning-altan-fyi
pnpm install
# Install the pre-push gate. A fresh clone has none until this runs.
make hooks # or: pnpm install, whose `prepare` script sets core.hooksPath
# Bring up a Postgres. Skip this if you already have one.
docker compose up -d
cp .env.example .env
# Edit .env. At minimum set the DB_* variables. SESSION_SECRET is required in
# production and has a development default otherwise.
#
# Mail is optional in development: with no PIGEON_API_KEY the confirmation and
# reset mails are logged to the console instead of being sent.
pnpm drizzle:migrate # create the schema
pnpm dev # dev server and worker together
The app runs at http://localhost:${PORT}, which defaults to 3000.
Sign up at /sign-up with an email address and a password, or from the
"Create account" button on the front page. The app mails a confirmation link
to that address, and you must click it before your first sign-in. Sign in at
/sign-in on any device with the same address and password, and your lists
stay in sync between them.
/forgot-password mails a link that lets you set a new password, and setting
one signs every other device out.
Grant yourself the two operator screens under /super/, which are the
language-model configuration and an IP echo for checking TRUST_PROXY:
pnpm cli account grant-superadmin <your-email>
The command reads the database directly, so run it where .env is readable.
It does not need a superadmin account, which is what makes the first grant
possible at all. On the hosted instance, /super is also fenced to the
operator's VPN, so the grant alone is not enough to reach it there.
For developers: without PIGEON_API_KEY set, the verification and reset links
are printed to the server console instead of being mailed.
The pnpm dev command starts two processes: dev:server (Express running React
Router SSR) and dev:worker (the pg-boss job processor). You can run each
process separately with pnpm run dev:server or pnpm run dev:worker.
The container entrypoint script is scripts/start.sh. It executes three tasks
in sequence:
pnpm drizzle:migrate.pnpm cli data-migration run. These
one-time TypeScript scripts are tracked in a data_migrations table. See
ADR-0002.You must run the worker process. Word enrichment tasks depend on pg-boss queues. If no worker processes those tasks, page requests will hang indefinitely.
Build and run the container image:
docker build -f Dockerfile.pnpm -t kenning-altan-fyi .
docker run --env-file .env -p 3000:3000 kenning-altan-fyi
To run outside Docker, build with pnpm build. Start the web server with pnpm start, and start the worker with pnpm worker. You must run pnpm drizzle:migrate manually before your first run.
You can populate the database quickly without processing raw dumps. Building the dictionary from scratch requires downloading large dumps from Wikidata and Tatoeba, which takes hours. Instead, you can use a pre-built seed dump of the shared dictionary tables:
dictionary-seed-2026-09-02.dump
(45 MB, custom pg_dump format), with an md5
checksum
file.sources rows.The seed dump includes data from three sources. The application presents
attribution directly from the sources table:
| Source | Licence | What it is |
|---|---|---|
| Wikidata lexicographical data | CC0 1.0 | headwords, senses, glosses |
| Tatoeba (CC0 sentences) | CC0 1.0 | example sentences |
| Tatoeba | CC BY 2.0 FR | example sentences |
The CC BY 2.0 FR records require clear attribution. If you publish this
data, you must credit Tatoeba and link to the source sentence. The app does this
automatically through the attribution column. Keep the sources records in
your database and keep the UI attributions intact to stay compliant.
The seed dump contains table rows without any schema definitions. Run database migrations first, then run these commands:
curl -LO https://github.com/AltanS/kenning-altan-fyi/releases/download/seed-2026-09-02/dictionary-seed-2026-09-02.dump
scripts/dictionary-restore.sh --truncate-first dictionary-seed-2026-09-02.dump
The script uses your existing DB_* environment variables. It uses
--single-transaction during restore. This flag is critical: without it,
pg_restore --data-only can return an exit code of 0 even when all rows fail to
load. The --truncate-first flag clears the nine dictionary tables before
loading. This cleanup prevents conflicts with migration scripts that insert an
initial sources row. The restore command requires database superuser access
because it uses --disable-triggers.
The import takes several minutes. Verify the imported row counts directly instead of relying on exit codes:
psql -d "$DB_NAME" -c 'SELECT count(*) FROM headwords;'
The scripts/verify-seed-restore.sh script tests this process against a
temporary database and outputs record counts. The scripts/make-seed-dump.sh
script exports a new seed dump from your current database instance.
The application can generate explanations and example sentences via a language
model through OpenRouter. This feature is optional.
Add OPENROUTER_API_KEY to your .env file to enable it. Without this key, the
application works normally using local dictionary data: translations, senses,
and imported Tatoeba examples. Entries show less text, but pages load cleanly
without errors.
The UI identifies generated text clearly. It links AI text to the Generated explanations source so users can distinguish it from imported database entries.
The system enforces spending limits. The app_settings table stores daily
spending caps alongside warning thresholds. When spending exceeds these
thresholds, the system sends an HTTP POST request to ALERT_WEBHOOK_URL. If you
do not configure a webhook, the system logs alerts to standard output.
A signed-in search for a word the dictionary cannot yet translate into the
chosen language queues one job. The model returns the senses and the
translations. They are written as ordinary dictionary rows under the source
llm-generated, released under CC0 1.0 like the imported data, and marked
"Generated" in the app. Every later reader of that pair gets the rows straight
from the database, with no second model call.
Three limits guard the corpus: a daily money cap, a daily cap on the number of
runs (MAX_TRANSLATION_RUNS_PER_DAY, so a script that found the search box
cannot flood the dictionary even at a fraction of a cent per call), and a
per-session rate limit.
Operator commands:
pnpm cli translation runs [--limit N] [--json] # list recent runs
pnpm cli translation retract <runId> # undo one run
pnpm cli dictionary stats --json # generated counts beside imported
retract deletes the rows one run created and keeps any row still referenced
elsewhere, stamping retracted_at on the run instead. Generated rows can
contain mistakes, and retraction is the correction path today.
The .env.example file outlines every supported variable with commentary and
placeholder values. Application configuration logic lives in app/config/.
Check that directory and .env.example for details.
import { CONFIG } from '#config';
const port = CONFIG.server.port;
const dbUrl = CONFIG.database.url;
Review two important secrets before deploying:
SESSION_SECRET encrypts and signs the user session cookie. Rotating it signs
everybody out; nothing else depends on it.PIGEON_API_KEY and PIGEON_BASE_URL are what let the confirmation and reset
mails leave the instance. Production refuses to send without them rather than
dropping a link silently.| Variable | Default | Description |
|---|---|---|
DB_POOL_MAX | 10 | Maximum number of connections |
DB_POOL_MIN | 2 | Minimum connections to maintain |
DB_IDLE_TIMEOUT_MS | 30000 | Idle connection close timeout |
DB_CONNECTION_TIMEOUT_MS | 5000 | Connection acquisition timeout |
In production environments, the application logs connection pool metrics every
60 seconds. It logs a warning whenever waitingCount > 5.
The drizzle/schema.ts file acts as the single source of truth for the database
schema. Run pnpm drizzle:push to test local schema modifications. Run pnpm drizzle:generate and pnpm drizzle:migrate to create and save migration files.
Production deployments run only drizzle:migrate.
The drizzle:* npm scripts execute drizzle-kit via tsx. Node does not strip
TypeScript inside node_modules, and the database schema imports types from
TypeScript packages.
Signup is open. The front page is public and carries a worked example.
Everything past it needs an account: a typed search, entry pages, lists,
history, review, and voice input. A signed-out visitor who searches is sent to
/sign-in, and the front page, /account and the app header all carry a
"Create account" button beside a "Sign in" link.
An account is an email address and a password. The address has to be confirmed by a mailed link before the first sign-in, a forgotten password is replaced by a second mailed link, and replacing it signs every other device out. Sign-up, sign-in and forgot-password answer the same way for an address that is on file and one that is not, so none of them can be used to find out who has an account here.
An account is also what carries state between a person's devices. Word lists, notes, review state and history are written on the device that made them. All of them except the search history are pushed to the server as one JSON document, under a compare-and-swap version that stops two devices overwriting each other.
The operator can read that document. It is stored as ordinary JSON in
sync_blobs.payload. The search history is the one collection that never
leaves the device
(app/lib/local-store/BLOB-CONTENTS.md
says what travels and what does not).
Until M191 the document was encrypted with a key the server could not derive, and an account was identified by a random opaque name plus a one-time recovery code, with no address on file at all. That bar cost the only thing it was protecting: a reader had no way to be told who they were signed in as and no way to reset a password. The claim in this README and on the privacy page is what is true now, not what used to be.
The CLI communicates with the same HTTP REST API that powers the web interface.
Provide a TRANSLATE_API_KEY to run commands through standard HTTP
authentication and audit logging.
pnpm cli api-key list
pnpm cli --remote=http://localhost:3000 db check
TRANSLATE_PROD_URL=https://app.example.com pnpm cli --prod db tables
See .claude/cli.md for full command documentation, and see
AGENTS.md for API conventions.
Run the test suites with these commands:
pnpm lint # oxlint, anti-slop plus correctness
pnpm typecheck # react-router typegen && tsc
pnpm test:unit # node --test over tests/unit
pnpm test:integration # node --test against a live server
The repository does not use a cloud CI pipeline. The .githooks/pre-push script
provides the only testing gate. It executes linting, type checks, unit tests,
and production build checks before pushing commits. You must
run make hooks or pnpm install in your local clone to install this hook.
The integration test suite executes CLI commands against an active application
server. Tests skip automatically when TEST_API_KEY is not present, allowing
safe runs without test credentials.
The project records major architectural choices as Architecture Decision Records
(ADRs) in .adr/. Review the relevant record before editing an
architectural subsystem. Add a new ADR when making major architectural changes.
| # | Title |
|---|---|
| 0001 | CLI wraps the API |
| 0002 | Data migrations alongside schema migrations |
| 0003 | App-enforced multi-tenancy (no RLS), superseded by 0010 |
| 0004 | The custom server.ts is the production entrypoint |
| 0005 | oxlint plus anti-slop is the lint gate |
| 0007 | One linter (oxlint), and TypeScript 7 |
| 0008 | The E2EE sync code is copied from openplate-sync, not shared, superseded by 0011 |
| 0009 | Invite-only accounts, bootstrapped by a one-shot token, superseded by 0011 |
| 0010 | Drop the inherited tenancy, org and CMS surfaces |
| 0011 |
We welcome issues and pull requests. Keep two details in mind before contributing:
AGENTS.md. This file outlines our coding standards for
both human contributors and automated agents.make hooks after cloning the repository. The pre-push hook acts as the
sole test runner, and local clones do not include it by default.All commit messages must follow the Conventional Commits format.
MIT. See LICENSE.
The application code uses the MIT licence. The dictionary data in the seed dump uses CC0 and CC BY 2.0 FR licences. The CC BY data requires attribution to Tatoeba. See the seed dump section for complete licensing terms.
| Plain accounts replace the encrypted layer |