gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit65c694a865c694a8tracker#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 scriptmre65c694a8/components/main.hl

4.7 KB

  1. // components/main.hl — THE SHELL: the application header (brand; top right the login: ident's "Log in with
  2. // ident" button or Log out) and the slot every page renders into. All CSS is in styles.hl. Copied from
  3. // calendar.worldapi.org's components/main.hl unchanged in shape (tracker.worldapi.org#1, rejected once for
  4. // centering the sign-in on the page instead of using the header selector).
  5. // THE SELECTOR: `<ident-selector key=IDENT_API_KEY>` from <ident>/selector.js, as in calendar/gitoria. Choosing an
  6. // identity fires `ident-login` (one-time code); /login.js hands it to the hidden input #identcode, whose `change`
  7. // goes to the face trackerLogin (server-side exchange, no reload). The element's class is `in` / `out`; /login.js
  8. // mirrors it into the selector's `loggedIn`.
  9. // LOGIN: a plain link to <ident>/login?key=&return=<public url>/login/callback (project.hl loginCallback).
  10. // After a login or logout in this tab the page is fetched again (navigate('/')).
  11. import { navigate } from 'hl:web'
  12. import { appTitle } from '../project.hl'
  13. import { identKey, selectorScript, loginHref, userIdOfSession, exchangeCode, ensureUser } from '../users.hl'
  14. import { randomBytes } from 'hl:crypto'
  15. slot = null
  16. session = null
  17. loggedIn = userIdOfSession(session) != null
  18. loggedOut = userIdOfSession(session) == null
  19. buttonHref = loginHref
  20. selectorClass = loggedIn ? 'in' : 'out'
  21. selectorKey = identKey
  22. identScript = selectorScript
  23. loginError = ''
  24. hasLoginError = false
  25. // OFFLINE (tracker.worldapi.org#10): the service worker opens `/` from its cache without a network; the shell then
  26. // says so. hl:web gives a page no connection state and no "mounted" hook, so a tick asks the browser: `netProbe` runs
  27. // an endless 1 s CSS animation (styles.hl), and each `animationiteration` reads navigator.onLine — a write only when
  28. // it changed. (An `img` error or a socket event would race the hydration and be missed.)
  29. offline = false
  30. View {
  31. body {
  32. applicationHeader {
  33. a { class = "brand" href = "/" appTitle }
  34. userBox { id = "userbox"
  35. // tracker.worldapi.org#14: the search (components/search.hl) — a magnifier, drawn in SVG like the checks
  36. a { id = "searchlink" class = "searchlink" href = "/search" title = "Search" "aria-label" = "Search"
  37. svg { viewBox = "0 0 24 24" width = "22" height = "22" "aria-hidden" = "true"
  38. circle { cx = "10.5" cy = "10.5" r = "6.5" }
  39. path { d = "M15.5 15.5 L20.5 20.5" }
  40. }
  41. }
  42. identSelector { id = "selector" key = selectorKey class = selectorClass }
  43. if (loggedIn) {
  44. button { id = "logout" type = "button" class = "quiet" "Log out" on click(e) { emit doLogout(e) } }
  45. }
  46. if (loggedOut) {
  47. a { id = "loginbutton" class = "button" href = buttonHref "Log in with ident" }
  48. }
  49. input { id = "identcode" type = "hidden" on change(e) { emit gotCode(e) } }
  50. }
  51. }
  52. if (hasLoginError) { p { id = "loginerror" class = "message banner" loginError } }
  53. if (offline) { offlineNote { id = "offline" "You are offline. Your shows and lists need the network." } }
  54. netProbe { "aria-hidden" = "true" on animationiteration(e) { emit netTick(e) } }
  55. main { slot }
  56. script { src = identScript }
  57. script { src = "/login.js" }
  58. }
  59. }
  60. showIn = () => {
  61. loggedIn = true
  62. loggedOut = false
  63. selectorClass = 'in'
  64. loginError = ''
  65. hasLoginError = false
  66. }
  67. showOut = () => {
  68. loggedIn = false
  69. loggedOut = true
  70. selectorClass = 'out'
  71. }
  72. // the selector's code (via /login.js and the hidden input) → the server exchanges it
  73. on gotCode(e) {
  74. let code = e.target.value
  75. e.target.value = ''
  76. if (code == null || code == '') { return null }
  77. let r = emit server trackerLogin(code)
  78. if (r == null || r.error != null) {
  79. loginError = r != null ? r.error : 'the login failed'
  80. hasLoginError = true
  81. return null
  82. }
  83. showIn()
  84. navigate('/')
  85. }
  86. on client trackerSignedIn(tag) {
  87. showIn()
  88. navigate('/')
  89. }
  90. on client trackerSignedOut(tag) {
  91. showOut()
  92. navigate('/')
  93. }
  94. on netTick(e) {
  95. let down = window.navigator.onLine == false
  96. if (down != offline) { offline = down }
  97. }
  98. on doLogout(e) {
  99. emit server trackerLogOut()
  100. showOut()
  101. navigate('/')
  102. }
  103. // ---- the faces (the trailing `session` is always the server's: hybriel #16) ----
  104. on server trackerLogin(code, session) {
  105. if (session == null) { return { error = 'no session — reload the page' } }
  106. let x = exchangeCode(code)
  107. if (x.error != null) { return { error = x.error } }
  108. let u = ensureUser(x.identity)
  109. if (u == null) { return { error = 'could not store the user' } }
  110. let tag = randomBytes(16)
  111. session.user = { id = u.id }
  112. session.data.tag = tag
  113. emit client trackerSignedIn(tag)
  114. return { ok = true }
  115. }
  116. on server trackerLogOut(session) {
  117. if (session == null) { return { error = 'no session' } }
  118. emit client trackerSignedOut(session.data.tag)
  119. session.user = null
  120. return { ok = true }
  121. }

Branches

Latest commits

  • 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
  • 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
  • 17375427tracker#2: tools/migrate.hl + tools/verify.hl — old MongoDB data into mpackdb with new idsmre
  • 31aac936tracker#3: no border on the header and on filled buttons; inverted buttons keep theirsmre
  • 2ad9d29ctracker#1: login exactly like calendar (identity selector in the header, empty homepage)mre
  • 3691e176tracker#1: empty tracker with the ident login (state of 2026-09-27)mre