tracker
All repositories: gitoria
4.7 KB
// components/main.hl — THE SHELL: the application header (brand; top right the login: ident's "Log in with// ident" button or Log out) and the slot every page renders into. All CSS is in styles.hl. Copied from// calendar.worldapi.org's components/main.hl unchanged in shape (tracker.worldapi.org#1, rejected once for// centering the sign-in on the page instead of using the header selector).// THE SELECTOR: `<ident-selector key=IDENT_API_KEY>` from <ident>/selector.js, as in calendar/gitoria. Choosing an// identity fires `ident-login` (one-time code); /login.js hands it to the hidden input #identcode, whose `change`// goes to the face trackerLogin (server-side exchange, no reload). The element's class is `in` / `out`; /login.js// mirrors it into the selector's `loggedIn`.// LOGIN: a plain link to <ident>/login?key=&return=<public url>/login/callback (project.hl loginCallback).// After a login or logout in this tab the page is fetched again (navigate('/')).import { navigate } from 'hl:web'import { appTitle } from '../project.hl'import { identKey, selectorScript, loginHref, userIdOfSession, exchangeCode, ensureUser } from '../users.hl'import { randomBytes } from 'hl:crypto'slot = nullsession = nullloggedIn = userIdOfSession(session) != nullloggedOut = userIdOfSession(session) == nullbuttonHref = loginHrefselectorClass = loggedIn ? 'in' : 'out'selectorKey = identKeyidentScript = selectorScriptloginError = ''hasLoginError = false// OFFLINE (tracker.worldapi.org#10): the service worker opens `/` from its cache without a network; the shell then// says so. hl:web gives a page no connection state and no "mounted" hook, so a tick asks the browser: `netProbe` runs// an endless 1 s CSS animation (styles.hl), and each `animationiteration` reads navigator.onLine — a write only when// it changed. (An `img` error or a socket event would race the hydration and be missed.)offline = falseView {body {applicationHeader {a { class = "brand" href = "/" appTitle }userBox { id = "userbox"// tracker.worldapi.org#14: the search (components/search.hl) — a magnifier, drawn in SVG like the checksa { id = "searchlink" class = "searchlink" href = "/search" title = "Search" "aria-label" = "Search"svg { viewBox = "0 0 24 24" width = "22" height = "22" "aria-hidden" = "true"circle { cx = "10.5" cy = "10.5" r = "6.5" }path { d = "M15.5 15.5 L20.5 20.5" }}}identSelector { id = "selector" key = selectorKey class = selectorClass }if (loggedIn) {button { id = "logout" type = "button" class = "quiet" "Log out" on click(e) { emit doLogout(e) } }}if (loggedOut) {a { id = "loginbutton" class = "button" href = buttonHref "Log in with ident" }}input { id = "identcode" type = "hidden" on change(e) { emit gotCode(e) } }}}if (hasLoginError) { p { id = "loginerror" class = "message banner" loginError } }if (offline) { offlineNote { id = "offline" "You are offline. Your shows and lists need the network." } }netProbe { "aria-hidden" = "true" on animationiteration(e) { emit netTick(e) } }main { slot }script { src = identScript }script { src = "/login.js" }}}showIn = () => {loggedIn = trueloggedOut = falseselectorClass = 'in'loginError = ''hasLoginError = false}showOut = () => {loggedIn = falseloggedOut = trueselectorClass = 'out'}// the selector's code (via /login.js and the hidden input) → the server exchanges iton gotCode(e) {let code = e.target.valuee.target.value = ''if (code == null || code == '') { return null }let r = emit server trackerLogin(code)if (r == null || r.error != null) {loginError = r != null ? r.error : 'the login failed'hasLoginError = truereturn null}showIn()navigate('/')}on client trackerSignedIn(tag) {showIn()navigate('/')}on client trackerSignedOut(tag) {showOut()navigate('/')}on netTick(e) {let down = window.navigator.onLine == falseif (down != offline) { offline = down }}on doLogout(e) {emit server trackerLogOut()showOut()navigate('/')}// ---- the faces (the trailing `session` is always the server's: hybriel #16) ----on server trackerLogin(code, session) {if (session == null) { return { error = 'no session — reload the page' } }let x = exchangeCode(code)if (x.error != null) { return { error = x.error } }let u = ensureUser(x.identity)if (u == null) { return { error = 'could not store the user' } }let tag = randomBytes(16)session.user = { id = u.id }session.data.tag = tagemit client trackerSignedIn(tag)return { ok = true }}on server trackerLogOut(session) {if (session == null) { return { error = 'no session' } }emit client trackerSignedOut(session.data.tag)session.user = nullreturn { ok = true }}
Branches
- mainmain branch
Latest commits
- 93be9476tracker#16: person pages /person/<slug> with the filmography fetched from TMDB on the first visit (step by step), gatemre
- 47a3cae6STATUS: mission 053 merge commit idsmre
- dcc5eecaMerge branch 't14-search'mre
- 03edc783Merge branch 't15-tvmaze'mre
- 71b46345tracker#15: numbering check by date or title, placeholder titles in other languages, docs + real-data proofmre
- 6bb2daf1tracker#13: homepage (tiles, intro, latest movies/shows), /shows, /movies/page/N, /my/movies; lists cached in memorymre
- b8bd1157tracker#14: README + STATUS (search, real-data numbers, gate, merge notes)mre
- 65c694a8tracker#14: search — header magnifier, /search/<text> (in-memory word-prefix index over titles + people), Fetch from web (TMDB search/multi, ours left out), Add = import via syncShow; gate +25 checks, real-data scriptmre
- 34f2c15btracker#15: TVmaze merge in the sync (gaps only: new episodes/seasons, empty titles/air dates; numbering check), fake TVmaze episodes + gatemre
- cbdc4ea7tracker#12: link icons TMDB/IMDb/TVDB/TVmaze; sync fills missing ids (TVmaze lookup); movies fetched via /movie/mre
- b105bcd8tracker#11: Hybriel master ff51cf46 (checks no longer vanish), mobile-first styles, carets, follow button, sign-in modal, inverted check, orange castmre
- 31b758aatracker#10: installable app (manifest, service worker, offline shell), own icon + faviconmre
- 2fa9d997tracker#9: TMDB sync (followed shows: seasons, episodes, posters), tools/sync-tmdb.hl + daily run 04:00 UTC, fake TMDB in gatemre
- 49e1f61edeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre
- 3251488atracker#7: /my/shows (followed shows, newest follow first, poster, title, last watched SxxEyy); gate can take screenshots (TRACKER_GATE_SHOTS)mre
- 44b7d9f9tracker#6: /schedule — upcoming episodes of followed shows, soonest firstmre
- 91c9fc8ctracker#5: /unwatched — unwatched released episodes of followed shows, newest firstmre
- a97c0295tracker#4: show page /shows/:slug (header, seasons, episodes, watch checks) + tools/relink-episode-seasons.hlmre
- 05f407c5tracker: no border on any button except inverted ones (Log out, ident status and identities too); header brand weight 100mre