Plan: Transitioning to Forward Deployed Engineer / AI Engineer
백엔드 엔지니어가 FDE·AI 엔지니어로 전환하기 위한 단계별 로드맵
AP 물리 C·화학·예비미적분·미적분·생물학을 위한 직관 중심 인터랙티브 학습 사이트, 무료이며 회원가입 없이 오프라인에서도 사용 가능
Interactive, intuition-first study sites for AP Physics C, Chemistry, Precalculus, Calculus and Biology. Free, no sign-up, works offline.
교육HTML · JavaScriptGitHub @jiacode2030온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Intulab: the landing page for free, interactive, intuition-first AP study sites (Physics C, Chemistry, Calculus, Biology). No sign-up, works offline as an app. Unofficial; not affiliated with the College Board.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Interactive, intuition-first study sites for AP science and math. This repo (jiacode2030/intulab) is the landing page at https://jiacode2030.github.io/intulab/, which links to each course site.
It's plain static files with no build step:
index.html is the whole page (styles, script and the GoatCounter config line).courses.js is the course list.privacy.html and terms.html hold the privacy policy and terms of use for every Intulab site, styled by legal.css. Each course site links to them at /intulab/privacy.html and /intulab/terms.html. If you change what a site stores or which services it uses, update the privacy policy and its "Last updated" date.tests/check.js holds the Playwright checks.Unofficial study aids. Not affiliated with the College Board. Use these to study, not on graded work.
Intulab can be installed as an app (a Progressive Web App), with no app store involved. On iPhone it's Share → Add to Home Screen; on Android it's ⋮ → Install app. The Get the app button on the landing page shows these steps, or opens Chrome's install prompt directly.
manifest.webmanifest holds the app's name, colors and icons. Its scope is /, so every course site (Mechanics, E&M, Chemistry, Calculus, Precalculus and Biology) opens inside the app. Every AP page links to this manifest, so "Add to Home Screen" from any of them installs the same app.icons/ holds the app icons: any, maskable (Android) and apple-touch-icon.png (iPhone).sw.js (a service worker). It loads pages from the network first, so updates show up right away, and falls back to the saved copy when offline. Google Fonts are saved after the first download. Analytics and other sites are never touched.sw.js, so all courses work offline from the first launch. Browser visitors only save the landing page.A new AP course needs the same head tags (manifest, apple-touch-icon, theme-color), a sw.js (copy one, change the aplabs-<site> cache name) and the one-line registration. Then add it to courses.js.
Test it with node tests/app.js. Run it from this repo, with the course repos cloned next to it; it serves them at their github.io paths. It checks that Chrome considers the app installable, that the Get the app dialog works, that the installed app saves every course, and that every page opens offline at phone width.
Add one entry to courses.js. Nothing else needs to change.
{
id: 'bio', // short and unique; used in events (open:bio)
title: 'Biology Lab',
course: 'AP Biology',
subject: 'Biology', // Physics | Chemistry | Biology | Math
description: 'One line about the site.',
modules: 24, // the Y in "X of Y modules done"; null if unknown
topics: ['Cells', 'Genetics', 'Evolution'],
url: 'https://jiacode2030.github.io/biology-lab/',
status: 'soon', // 'live' when the site is up
accent: '#F59E0B', // the page darkens it in light mode
prefix: 'bio-', // the site's localStorage prefix
progressKey: 'bio-done', // where the site stores "marked as done"
label: 'course:bio' // label in intulab-feedback
}
ICONS in index.html.status: 'live' once the site is deployed. Until then the card is muted, shows "Coming soon" and isn't a link.All the sites share one origin (jiacode2030.github.io), so the landing page can read each course's localStorage. It only reads and never writes. The ring shows when progressKey holds a JSON object of finished modules, for example {"projectile": 1, "forces": 1}, which is what Mechanics stores under kin-done. Missing or unreadable data hides the ring. When modules is null, the card shows "X modules done" with no ring.
New course sites should store progress the same way: localStorage.setItem('<prefix>done', JSON.stringify({moduleId: 1, ...})).
The landing page has no "Continue where you left off" strip, because the course sites don't store a last-visited module. To add one later, have each site save <prefix>last = "<moduleId>".
Every course site should link back to the landing page. Paste this right after the site's brand link (in Mechanics, after <a class="brand" ...>…</a> in the side rail). It uses the site's own --accent, --line and --ink colors when they exist:
<a class="morelabs" href="/intulab/">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>
More labs
</a>
<style>
.morelabs{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:0 14px;border-radius:999px;
border:1px solid var(--line,#C3D3E7);color:var(--ink,#0E1B2E);font:500 14px/1 inherit;text-decoration:none}
.morelabs:hover{border-color:var(--accent,#1F4FD6)}
.morelabs:focus-visible{outline:3px solid var(--accent,#1F4FD6);outline-offset:3px}
</style>
The link is relative (/intulab/), so it works on every site under jiacode2030.github.io. GoatCounter already counts the landing page view, so the link needs no event of its own.
Each course site has its own color, so they're easy to tell apart. Pick a new one for each new course:
| Course | Color |
|---|---|
| Mechanics Lab | sky blue #5CB8FF |
| E&M Lab | violet #A78BFA |
| Chemistry Lab | sage green #3DD6A3 |
| Calculus Lab | ocean aqua #5FD0DA (site theme: aqua) |
| Biology Lab | shell and amber (accent #F5B35C) |
| Precalculus Lab | rose-magenta pink #F27BAF (site theme: rose, accent #B3245C in light mode) |
Push to main. GitHub Pages serves this repo from the root of main (Settings → Pages → Deploy from a branch → main / root) at https://jiacode2030.github.io/intulab/. .nojekyll keeps Pages from running Jekyll.
The landing page sits beside the course sites under the same origin (jiacode2030.github.io), so it can still read their progress. The bare jiacode2030.github.io/ stays free for a personal page. If you'd like it to forward here instead, create a jiacode2030.github.io repo whose index.html is <!doctype html><meta http-equiv="refresh" content="0; url=/intulab/">.
These need Node.js and Playwright (Chromium):
node tests/check.js
The script opens the page at 360, 768 and 1280 px in light and dark, then checks for:
It also seeds fake progress and confirms the ring reads "12 of 32 modules done", that bad or missing data hides the ring, and that the course keys are never written. Screenshots go to tests/shots/, or to the folder in SHOTS=....
Every site reports to jiacode2030/intulab-feedback. That repo has issue forms for Content error, Bug and Suggestion, each with a Course dropdown. A small workflow adds the course:* label from the dropdown, or from the **Course:** line the sites pre-fill.
To see issues across courses, filter by label:
is:issue is:open label:course:mechanicsis:issue is:open label:type:bugis:issue is:open, then sort by newest or by most commentedCreate the labels once (Issues → Labels): course:mechanics, course:em, course:chem, course:calc, course:precalc, course:bio, course:landing, type:content, type:bug and type:suggestion. The workflow creates any course:* label that's missing the first time it's used.
Snippet for each course site. The Mechanics site builds its issue link from REPO in its Report code. Replace that constant and the issue.href=... line with this, and change the three constants per site:
const REPO='https://github.com/jiacode2030/intulab-feedback';
const COURSE='Mechanics Lab', COURSE_LABEL='course:mechanics', SITE_VERSION='2026-10-23';
// in fallback(r):
issue.href=REPO+'/issues/new?labels='+encodeURIComponent(COURSE_LABEL)
+'&title='+encodeURIComponent(clip(`[${COURSE}] [${r.type}] ${viewName(r.where)}: ${r.details.replace(/\s+/g,' ')}`,100))
+'&body='+encodeURIComponent(`**Course:** ${COURSE}\n**Module:** ${viewName(r.where)} (#${r.where})\n**Type:** ${r.type}\n**Site version:** ${SITE_VERSION}\n\n${r.details}\n\n**Context:** ${r.context}`);
The values per site are:
| Site | COURSE | COURSE_LABEL |
|---|---|---|
| Mechanics | Mechanics Lab | course:mechanics |
| E&M | E&M Lab | course:em |
| Chemistry | Chemistry Lab | course:chem |
| Calculus | Calculus Lab | course:calc |
| Precalculus | Precalculus Lab | course:precalc |
| Biology | Biology Lab | course:bio |
(GitHub applies the labels= URL parameter only for people with triage access. For everyone else, the workflow labels the issue from the **Course:** line.)
GoatCounter is free and uses no cookies, so no consent banner is needed. It sends anonymous counts only, with no personal data. Every site reports to one GoatCounter property, so you see views per path (/intulab/, /physics-mechanics/projectile, /physics-em/gauss, ...) in one dashboard.
The landing page has one config line in index.html:
const GOATCOUNTER = 'intulab'; // the part before .goatcounter.com
No site loads the counter when the browser sends Do Not Track or Global Privacy Control, as the privacy policy promises. Keep that check in any new course site. The page loads the counter only on *.github.io, so local testing isn't counted. Your dashboard is at https://intulab.goatcounter.com.
Snippet for each course site. The Mechanics site already has this pattern. Point it at the shared code:
(function analytics(){if(!/\.github\.io$/.test(location.hostname))return;window.goatcounter={no_onload:true};
const s=document.createElement('script');s.async=true;s.src='https://gc.zgo.at/count.js';
s.dataset.goatcounter='https://intulab.goatcounter.com/count';
const view=()=>{const id=(location.hash||'').slice(1)||'overview';if(window.goatcounter&&window.goatcounter.count)
window.goatcounter.count({path:location.pathname.replace(/\/$/,'')+'/'+id,title:id});};
s.onload=()=>{view();addEventListener('hashchange',view);};document.head.appendChild(s);})();
Events. The landing page sends open:<course-id> when someone opens a course card. A course site can send a module event in one line, for example when a module opens or is marked done:
window.goatcounter?.count?.({path:'module:'+id, title:'module:'+id, event:true});
Use module:<id> for opens and done:<id> for "marked as done" (for example, call it in setDone when v is true).
Public totals on the landing page. The usage cards ("visits", or "visitors" and "page views" when GoatCounter reports different numbers) read https://intulab.goatcounter.com/counter/TOTAL.json. Turn on Settings → Allow adding visitor counts on your website in GoatCounter or the cards stay hidden. They also stay hidden under Do Not Track / Global Privacy Control, off the live site, and until a total passes USAGE_MIN in index.html (50).
Reading the counts. In the GoatCounter dashboard:
/physics-mechanics/ to see one course.open:*, module:*, done:*) appear in the same list, marked as events. Type open: in the filter box to compare which cards get clicked.