ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
Loopky는 무료 플래시카드 앱으로 간격 반복 학습을 지원합니다
Loopky is a free flashcards app with spaced repetition. Import Anki decks, learn languages by listening and speaking, prep for exams and med school, share decks. No ads.
교육HTML · JavaScriptGitHub @jvsena42온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Loopky landing page
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
The landing page for Loopky, a flashcards app
with spaced repetition. Static HTML, CSS and JavaScript. No build step: GitHub Pages
serves this repository as it is.
It is a port of DiscoveryRepositoryImpl.decksByTagGlobalPage, the app's global
browse, and it reads what that reads. Two public sources, both CORS-open and both
unauthenticated:
• Pubky Nexus answers which URIs carry the
loopky-deck label, via /v0/stream/resources. That is all it answers: a deck
manifest is a generic resource to the indexer, so there is no title in there.
• The homeserver serves the maniREADME 전문 보기 ↗The landing page for Loopky, a flashcards app with spaced repetition. Static HTML, CSS and JavaScript. No build step: GitHub Pages serves this repository as it is.
index.html the landing page
deck/index.html where a shared deck link lands
profile/index.html where a shared profile link lands
recap/index.html where a shared yearly recap lands
dashboard/index.html adoption numbers, read live from Nexus and the homeserver;
noindex, unlinked, kept out of the sitemap
.well-known/assetlinks.json lets Android open those links in the app
.well-known/apple-app-site-association the same for iOS
assets/css/style.css the app's own palette, from LoopkyColors.kt
assets/js/i18n.js copy in the 11 languages the app ships
assets/js/discover.js the live Discover feed, and the deck and profile reads
assets/js/app.js language switching, filtering, rendering
assets/js/link.js the deck and profile pages
assets/js/recap-core.js the recap's arithmetic, the same rules as the app's
assets/js/recap.js the recap page: what it reads and how it draws it
assets/img/ icon, screenshots, share image
anki-alternative/ … faq/ the guide pages in English, one per kind of learner
pt-br/ es/ fr/ de/ it/ ja/ ko/ vi/ zh-hans/ zh-hant/
the same guides in the other ten languages
tools/guides/*.json the guides' words, one file per language
tools/build-guides.mjs builds the guide pages and sitemap.xml from them
assets/img/guides/ app screenshots the guides show
privacy/ terms/ support/ the privacy policy, the terms and the support page, English only
tools/legal/*.md their text; privacy.md and terms.md are copies from the app repository
tools/legal.mjs turns that Markdown into the three pages
tools/sync-legal.mjs refreshes the two copies, and reports when they have drifted
llms.txt a summary of Loopky for language models
llms-full.txt every guide as plain text, for language models
tools/check.mjs the pre-flight CI runs
tools/layout-check.mjs fails if the page scrolls sideways
tools/stamp-assets.mjs content hashes on the asset URLs
tools/set-site-url.mjs moves every published URL at once
It is a port of DiscoveryRepositoryImpl.decksByTagGlobalPage, the app's global
browse, and it reads what that reads. Two public sources, both CORS-open and both
unauthenticated:
loopky-deck label, via /v0/stream/resources. That is all it answers: a deck
manifest is a generic resource to the indexer, so there is no title in there.pubky://
address names a homeserver and a path, and the homeserver takes the pubky in a
pubky-host header rather than in the URL. Title, card count, topics and cover all
live here. This is the app's deckRepository.fetchRemote, and the reason the page
can show a real card count next to a real title.Three details are carried over from the app because each one is load-bearing:
skip advances by the window asked for, never by what came back. It is an
offset into the indexer's raw sorted set, and Nexus drops a subject whose details no
longer resolve, so a page is routinely shorter than the limit with more behind it.
Only an empty page means the end.loopky-deck, so the URI has to
parse as a deck manifest, the deck's own author has to be among that label's
taggers, and the manifest has to fetch and parse. A forged entry is then useless
rather than merely unlikely.That last check is also what keeps deleted decks out. The first version of this section read deck announcements from the post index instead, which was wrong three ways at once: it found 15 of the 37 published decks, it had covers for 7 of them, and five of the decks it showed had been deleted by their authors, because a post outlives the deck it announces. Reading manifests gives all 36 that resolve, 33 covers, and a card count for every one.
Decks are fetched six at a time and the grid fills as they land. Everything read back
was written by a stranger, so no network value reaches innerHTML. If the indexer is
unreachable the section says so, offers a retry, and the rest of the page is
unaffected.
A deck without a picture has none to find. A cover reaches the web only when its
author published one as an https:// URL; an emoji cover, or a picture imported as a
blob from an .apkg, never leaves the homeserver. Those decks get their emoji on a
tint picked from the deck's own address, so a grid of them reads as a design rather
than as images that failed to load.
One homeserver is hardcoded. The app resolves each author's own through pkarr,
which needs a signed-packet parse and a second resolution this page should not carry.
Every account on the network today is on the public homeserver; a deck hosted
elsewhere is left out, the same as one that was deleted. If that stops being a
rounding error, HOMESERVER in assets/js/discover.js is the line to fix.
The home page is one page in eleven languages, which gives a search engine one page to rank for everything. The guides give it one page per question people actually type: Anki alternative, flashcards for language learning, flashcards for medical students, flashcards for teachers, AI flashcard maker, what is spaced repetition, and a FAQ. The home page links to all of them from its Made for how you learn section.
Each guide is short and visual: a screenshot of the app, a few feature cards, one
or two screenshot-and-text blocks, a table where one helps, and a few questions. They
exist in all eleven languages, each at its own address (/anki-alternative/,
/pt-br/anki-alternative/, /ja/anki-alternative/ …) with hreflang between them,
because a language that only appears after a script runs is one a search engine
barely sees. The home page's guide links follow the chosen language.
The words live in tools/guides/{lang}.json and the layout (which screenshot, which
icon) in tools/build-guides.mjs, so a translation cannot move a picture. After
editing either, rebuild:
node tools/build-guides.mjs
That writes every guide page and sitemap.xml. tools/check.mjs runs the same
build in --check mode and fails when the output is stale, and it fails when a
language's JSON drifts from en.json (a missing key, a question too few). Adding a
guide means a slug in tools/guides.mjs, a layout entry, and the text in all eleven
files. The pages carry no script beyond the theme and the language picker. Each has
its own canonical, Open Graph card and JSON-LD (Article or WebPage,
BreadcrumbList, FAQPage).
Every claim in a guide was checked against the app repository, including the ones that make Loopky look worse: fixed intervals rather than FSRS or SM-2, no audio or LaTeX from Anki, decks are public. Assistants quote these pages, and a wrong answer quoted with confidence costs more than an honest limit. Keep them true when the app changes.
For language models there is llms.txt (the llms.txt
convention: a summary, key facts, and links) and llms-full.txt (the long version
of every guide, written by hand; the pages stay short, this keeps the detail). robots.txt names the AI crawlers as welcome. The home page also
ships the English topic list and AI prompt in its markup rather than only
painting them from script, so a reader that runs no JavaScript still sees them.
tools/check.mjs holds all of this in place: each guide is stamped, listed in
the sitemap, indexable, has one <h1>, a title and a description; every JSON-LD
block parses; every loopky.app address in the two llms files exists; and the
prompt and topic list in the markup match i18n.js.
/privacy/, /terms/ and /support/ are the three addresses the app stores and the
plugin directories ask for, and every footer on the site links them. They are English
only and say so: legal text in a language nobody reviewed is worse than English.
The policy and the terms are not written here. PRIVACY.md and TERMS.md in the
app repository are the source, tools/legal/privacy.md and terms.md are copies, and
each page links back to its file and carries its "Last updated" date. After either
changes there:
node tools/sync-legal.mjs # or: node tools/sync-legal.mjs ../loopky
node tools/build-guides.mjs
pages.yml runs node tools/sync-legal.mjs --check and warns when the copies have
drifted, so drift never blocks a deploy. That only runs when this repository changes,
so legal-drift.yml asks again every Monday and fails, which emails the owner. Never edit the two copies by hand, since the next sync
overwrites them. tools/legal/support.md has no upstream and is edited here.
tools/legal.mjs reads a small subset of Markdown (headings, paragraphs, flat lists,
tables, links, bold, code), which is everything those files use. Anything else, a
### heading or a numbered list, fails the build instead of rendering as text. The privacy page is
exempt from the em dash rule because it is PRIVACY.md word for word.
The app shares a deck as https://loopky.app/deck/?author={pubky}&id={deckId} and a
person as https://loopky.app/profile/?pubky={pubky}, rather than the bare
pubky:// address it used to. Nothing on the web linkifies pubky://, pubky.app's
markdown included, so the old link was text a reader had to copy. An https link is
clickable everywhere, and it does the right thing on both sides of the install:
.well-known/assetlinks.json before it agrees. With
that in place the tap opens the deck in the app and this site is never loaded.On an Android browser the page adds an Open in Loopky button. It is an
intent:// link wrapping the pubky:// address with the package named and Google
Play as the fallback, so one tap opens the app if it is there and the store if it
is not. That button matters where App Links do not fire, which is mostly in-app
browsers.
On iOS the same button is the bare pubky:// address, which the app claims as a URL
scheme. There is no iOS link that falls back to a store, and a link to this same
domain never leaves Safari, so without the app the tap does nothing useful: the hint
under it names TestFlight, and the Google Play button is hidden. Whoever sees the
page on an iPhone is someone the universal link did not catch, which is an in-app
browser or a Loopky installed after the page loaded.
On a desktop there is no Loopky to open (only the CLI), so the button is not drawn and Google Play is the primary action.
Query parameters, not paths, because Pages is static. /deck/{pubky}/{id}
would exist only as a 404.html fallback, served with a 404 status that link
preview bots and crawlers take at its word. /deck/ is a real file and answers
200 whatever follows the ?.
The preview card is generic, and cannot be anything else here. A link preview
reads the page's Open Graph tags without running its script, and on a static host
every deck gets the same file. Per-deck titles and covers in a preview need
something that renders at request time. Both pages carry noindex and stay out of
the sitemap for the same reason: each is a template that answers for every deck.
It lists two certificates. The first, 09:81:13:…:80:48, is the Play App Signing
certificate and also the upload key: Play Console shows that SHA-256 under Test and
release › App integrity › App signing key certificate, and the APK attached to
each GitHub release is signed with it too. So a Play install and a release APK both
verify. The second, FB:07:11:…:4F:7C, is a debug key.
A build signed with any other key, a debug build from a machine whose debug key is
not listed for one, is not verified, and every link opens the browser. That failure
is silent: the browser shows this web page, which looks like it works. On a device,
adb shell pm get-app-links com.github.jvsena42.loopky shows whether loopky.app
reads verified. If Play's signing key is ever rotated, the new SHA-256 has to be
added here.
The deploy uses upload-pages-artifact@v3, which keeps dot-directories in the
artifact. v4 drops them unless the step sets include-hidden-files: true, and
tools/check.mjs fails on that combination, because the result would be every App
Link quietly turning back into a web page.
The iOS equivalent. It names one app, WFQWFBS5H7.com.github.jvsena42.loopky
(the Apple Team ID, then the bundle identifier), for /deck and /profile with
or without anything after them. It has to agree with the app's
applinks:loopky.app entitlement and with the team that signs the build: a
TestFlight or App Store build from any other team, or one under another bundle
identifier, is not verified and the link opens this site instead.
It also lists the same app under webcredentials, paired with the app's
webcredentials:loopky.app entitlement. That is what lets the iOS app save a
recovery phrase to the Passwords app as a loopky.app entry and read it back.
There is no login form on this site and the entry is not for one.
Pages serves the extensionless file as application/octet-stream, which Apple
accepts, and without a redirect, which it does not. iOS does not fetch the file
from here: Apple's CDN does, and caches it for up to a day. What the CDN holds is
at https://app-site-association.cdn-apple.com/a/v1/loopky.app; check that after
changing the file, before blaming the app. tools/check.mjs validates it.
Each December the app offers a "Loopky Recap" of the year, and its share buttons hand
out https://loopky.app/recap/?pubky={pubky}&year={year}. That link lands on
recap/index.html.
The link carries who and which year, and nothing else. The numbers are worked out
on the page from the account's own public records: its deck manifests, the decks it
follows, every review-state chunk under /pub/loopky/srs/, and its settings record
for where "mastered" sits. So a link cannot be edited into a better year than the
account had, and nothing new is published to make the page work.
The arithmetic exists twice and has to agree. assets/js/recap-core.js is
YearRecap.from from the app, ported. Three rules are there only to keep the two
equal: the recap counts review states rather than cards (the page will not fetch every
card to learn which were deleted), the year runs in UTC on both sides, and ties are
broken by code unit, never localeCompare. tools/recap-fixture.json is a
byte-for-byte copy of the file the app's YearRecapFixtureJvmTest reads, and
node tools/recap.test.mjs holds this side to it. Change a rule in both repositories,
with the fixture, or in neither.
It is not an App Link, unlike a deck or a profile. Someone holding the app already has their recap in it. This page is for whoever was sent one, and what it asks of them is the band underneath: get the app.
Like the deck and profile pages it is noindex, stays out of the sitemap, reads only
the one public homeserver, and shows every recap behind the same generic link preview
(assets/img/og-recap.png), since a static host cannot put one person's numbers in
the page a preview bot fetches. An account with nothing studied and nothing created
that year gets "Nothing to recap yet" rather than a row of zeroes, and a link without
a usable pubky and year goes to the home page.
Pages serves the page and its assets with the same max-age=600 and no
fingerprinting, and a browser expires each on its own clock. A deploy that adds a
string can therefore leave a returning visitor holding the new markup next to a
ten-minute-old i18n.js, which is how feat.cli.t once got painted into a card
where its title belonged.
Two things settle it, and both are wanted. The asset URLs carry a hash of their own contents, so new markup names a script the cache has never seen:
node tools/stamp-assets.mjs
It covers index.html, deck/index.html, profile/index.html, recap/index.html and the guide pages. Run it after touching any file under assets/; an unchanged file keeps its stamp, so
it is safe to run always, and tools/check.mjs fails when a stamp and its file
disagree. And a key that resolves nowhere now leaves the markup's own English
standing rather than painting its name, so the worst case is an untranslated card
rather than a broken one.
English is the default. The picker covers the same languages as the app: English, Português, Español, Français, Deutsch, Italiano, 日本語, 한국어, Tiếng Việt, 简体中文, 繁體中文.
Each language has its own home page: / in English, and /pt-br/, /es/, /ja/
and so on for the other ten. They are built from index.html by
node tools/build-guides.mjs, with that language's dictionary from i18n.js
applied at build time, so each page is real text in its language with its own
canonical and hreflang, not English that a script rewrites. Never edit a
translated copy: edit index.html or i18n.js and rebuild. tools/check.mjs fails
when a copy is stale.
/ sends a visitor to their language's page when ?lang=, an earlier choice in
localStorage, or the browser says so, which also keeps old ?lang= links working.
A translated page never redirects: someone who searched in Portuguese stays on the
Portuguese page. The picker moves between the copies. The deck and profile pages
have one copy each and still repaint in place.
The slogan is the exception and stays English everywhere. strings.xml declares
brand_tagline as translatable="false" and the app shows the English line in every
locale, so hero.title and cta.title are one SLOGAN constant rather than eleven
strings. tools/check.mjs fails if any locale translates it.
The top bar carries a switch with three states: system, light, dark. It cycles in
that order and the choice lives in localStorage under loopky.theme. It wears the
same pill as the language picker beside it and names the state it is in, because the
first version was an icon on its own and people walked past it. Under 420px the
label goes and the icon stands alone, which is where it was always easy to find: the
nav links are gone at that width and it sits next to the brand. No choice
stored means no data-theme attribute on <html>, and the page follows the
system, including when the system turns dark at sunset. A two-way switch has no way
back to that once it has been touched, which is why there are three.
An inline script in the <head> puts a stored choice on <html> before the first
paint. Without it the page draws the system theme and then flips in front of the
visitor.
The dark palette is therefore spelled twice in assets/css/style.css: once under
@media (prefers-color-scheme: dark) for the system, once under
:root[data-theme="dark"] for the choice. CSS cannot share one block between a
media query and an attribute, so tools/check.mjs compares the two and fails when
only one was edited. The media query is scoped to
:root:not([data-theme="light"]), so a visitor who picked light stays there on a
machine set to dark.
color-scheme follows along, which is what makes the native parts the browser
paints itself, the language dropdown and the scrollbars, match the page. The two
theme-color meta tags are keyed to the system in the markup; an explicit choice
flips one to all and the other to not all, so the address bar follows too.
python3 -m http.server 8000
Then open http://localhost:8000. Before pushing, run the same check CI runs. It needs nothing but Node:
node tools/check.mjs
It parses every script, holds the eleven dictionaries against each other and against
the page, confirms the AI prompt still names real CLI commands, checks that every
asset and hreflang the page references exists, holds the two dark palettes in the
stylesheet against each other, validates assetlinks.json and apple-app-site-association, and refuses an em dash in anything a visitor reads. It covers the deck and profile pages as well as the home page.
node tools/layout-check.mjs
That one loads the real pages (home, a real deck, its author's profile, every guide) in headless Chrome at four widths and fails if it scrolls sideways. It exists because a wrapper around the topic chips once stretched that row to 1679px at every viewport and took the page's width with it, and no screenshot showed it: a cropped screenshot of an overflowing page looks exactly like a cropped screenshot of a correct one. It skips with a note when no Chrome is installed. Adding a string means adding it to all eleven dictionaries, and forgetting one is the mistake this repo invites: the page falls back to English silently, so nothing looks broken. That is the check worth having.
The domain is a custom one, set three places that have to agree:
CNAME at the repository root, holding the bare domain and nothing else. It is
part of the published artifact, which is how GitHub learns the domain when the
deploy is a workflow rather than a branch.A and four AAAA records on the apex pointing at GitHub Pages, and
www as a CNAME to jvsena42.github.io. so GitHub can redirect it to the apex.
A CAA record narrows certificate issuance to Let's Encrypt, which is the CA Pages
uses.Only that last one lives in these files, and everything the browser loads is a
relative path, so the page itself works under any prefix untouched. What cares is the
part search engines read: the canonical link, the Open Graph and Twitter cards, the
eleven hreflang alternates, the JSON-LD, the sitemap and robots.txt. One command
moves them together:
node tools/set-site-url.mjs https://loopky.app/
tools/check.mjs fails if those addresses drift apart, which is what a half-applied
move looks like.
.app is on the HSTS preload list, so browsers refuse plain HTTP for it and there is
no insecure fallback while a certificate is being issued. A domain change means the
site is unreachable until Pages has one, usually minutes.
After each deploy, tools/indexnow.mjs tells the IndexNow engines (Bing, Yandex,
Seznam, Naver and others) which pages the push changed, so they recrawl within
hours. It announces only HTML files the push touched, or the whole sitemap when it
cannot tell. The engines check that the site owns the key by fetching
70747e102c8b7c4c31ffdd586a8ea820.txt at the root, so that file keeps its name and
contents; tools/check.mjs fails if they drift from the key in the script. The step
only warns when it fails.
.github/workflows/pages.yml runs tools/check.mjs on every push and pull request,
then deploys main to Pages. It also asks the indexer the two questions the Discover
section asks, and logs what came back. That one is a warning, never a failure: the
network being down is not a reason to block a deploy.
The deploy job turns Pages on itself the first time, via enablement: true, so the
repository needs no click in Settings. If you would rather serve the branch directly,
set Settings → Pages → Source to Deploy from a branch (main, /) and delete
the deploy job. The check job is worth keeping either way.
Taken from androidApp/.../ui/theme/LoopkyColors.kt in the app repository, light
palette and dark, as CSS custom properties. A change there wants the same change in
assets/css/style.css, and a change to a dark token wants it in both of the two dark
blocks. See Light and dark.