gitoriaLog in with ident

tickets

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmainbb64d57cmission 012 (ticket #25 API part): every /api/projects/<x>/… route takes the project's id (12-char record id) as well as its slug, same answers byte for byte; tickets stay under their project, no global /api/tickets/<x>. gate 315/0 (old code: the 13 id checks fail), connect 60/0, live-data copy old vs new 1,484 identical, slug vs id 772 identicalmremain/project.hl

11.2 KB

  1. // project.hl — tickets.worldapi.org: THE MAP. Config, routes and WHO HEARS WHAT; the logic lives in lib/.
  2. //
  3. // WHERE THINGS ARE (one line per feature; README.md "Files" has the full list):
  4. // tickets, their states and relations (parent / blocked-by) lib/tickets.hl (state names, page shapes: lib/tickets-helpers.hl)
  5. // the history of a ticket (events) lib/events.hl
  6. // projects, members, roles, slugs lib/projects.hl
  7. // users, login through ident, API tokens, sessions lib/users.hl
  8. // invites to a project (ident#22) lib/invites.hl
  9. // connecting an app to a project (ticket #21) lib/connections.hl
  10. // Markdown in text fields / the Markdown read view lib/markdown.hl / lib/mdview.hl
  11. // the JSON API + /login/callback, /connect, /tickets/:ref lib/api.hl (plumbing: lib/api-helpers.hl, lib/jsoncheck.hl)
  12. // the ticket #20 data migration (every start) lib/migrate.hl
  13. // the ticket-file format of the import lib/import.hl (the import tool: tools/import.hl)
  14. // shared small helpers (env, storage dir, URLs, sorting, Vienna time) lib/util.hl
  15. // pages: shell, list, ticket, inbox, you, new project, settings, connect components/*.hl; CSS components/styles.hl
  16. import WebFramework from 'hl:web'
  17. import { env } from 'hl:proc'
  18. import Styles from './components/styles.hl'
  19. import { dark, darker } from './shared/tokens.hl'
  20. import { migrateAll } from './lib/migrate.hl'
  21. import { tagOf } from './lib/users.hl'
  22. import { postTicket, getTicket, postComment, postState, postEdit, postAssign, postParent, postBlockers, getProjects, getInbox, getProject, postMembers, postMemberRemove, postInvites, connectExchange, projectConnections, projectDisconnect, connectStart, projectTickets, getProjectTicket, postProjectComment, postProjectState, postProjectEdit, postProjectAssign, postProjectParent, postProjectBlockers, oldTicketPage, oldProjectPage, oldProjectTicketPage, loginCallback } from './lib/api.hl'
  23. import TicketList from './components/ticket_list.hl'
  24. import Ticket from './components/ticket.hl'
  25. import Inbox from './components/inbox.hl'
  26. import You from './components/you.hl'
  27. import NewProject from './components/new_project.hl'
  28. import Settings from './components/settings.hl'
  29. import Connect from './components/connect.hl'
  30. static siteName = "tickets"
  31. appTitle = siteName
  32. styles = Styles
  33. // ---- THE INSTALLABLE APP (mission 046), the same way calendar.worldapi.org does it: hl:web's own web app manifest
  34. // (/__hl/manifest.webmanifest, linked from every head with the apple-touch-icon and theme-color) and service worker
  35. // (/__hl/sw.js) from these settings — no JavaScript of ours. Icons in icons/ (icon.svg, a ticket stub, is the source;
  36. // the PNGs and favicon.ico are rendered from it with rsvg-convert / magick, README "PWA"). The theme colour is the
  37. // header's background (darker, as tracker), the splash background the page's (dark) — both off the tokens, not written out again.
  38. appThemeColor = darker.value
  39. appBackgroundColor = dark.value
  40. appIcons = [
  41. { src = '/icons/icon-192.png' sizes = '192x192' purpose = 'any' }
  42. { src = '/icons/icon-512.png' sizes = '512x512' purpose = 'any' }
  43. { src = '/icons/icon-192.png' sizes = '192x192' purpose = 'maskable' }
  44. { src = '/icons/icon-512.png' sizes = '512x512' purpose = 'maskable' }
  45. ]
  46. appTouchIcon = '/icons/apple-touch-icon.png'
  47. appFavicon = '/icons/icon.svg'
  48. // OFFLINE: the shell and `/` — `/` IS the ticket list, so offline it shows the list as this browser last loaded it,
  49. // and the shell (components/main.hl) says "You are offline". Its value-form emits are never queued. Other pages are
  50. // not kept, except a project list (/<slug>, same component) this browser visited; the rest get hl:web's
  51. // "Unavailable offline" page.
  52. offline = [ TicketList ]
  53. // ---- THE ROUTES: files, the function routes (lib/api.hl), the pages (components/) ----------------
  54. routes = [
  55. { pattern = "/favicon.ico" file = "./icons/favicon.ico" headers = { 'Cache-Control' = 'no-cache' } }
  56. // the installable app (mission 046): the icons (manifest and service worker are hl:web's own, from appIcons / offline)
  57. { pattern = "/icons/icon-192.png" file = "./icons/icon-192.png" headers = { 'Cache-Control' = 'no-cache' } }
  58. { pattern = "/icons/icon-512.png" file = "./icons/icon-512.png" headers = { 'Cache-Control' = 'no-cache' } }
  59. { pattern = "/icons/apple-touch-icon.png" file = "./icons/apple-touch-icon.png" headers = { 'Cache-Control' = 'no-cache' } }
  60. { pattern = "/icons/icon.svg" file = "./icons/icon.svg" headers = { 'Cache-Control' = 'no-cache' } }
  61. // the login button's return (ticket #7): it needs the request's session and the framework's session store, BY REFERENCE
  62. { pattern = "/login/callback" function = (route, req) => { return loginCallback(route, &req, &server.sessions) } }
  63. { pattern = "/login.js" file = "./login.js" headers = { 'Cache-Control' = 'no-cache' } }
  64. // ticket #12 (mission 024): the Markdown editor <md-editor>, vendored from worldapi-components
  65. { pattern = "/md-editor.js" file = "./shared/md-editor.js" headers = { 'Cache-Control' = 'no-cache' } }
  66. { pattern = "/api/tickets" function = postTicket }
  67. { pattern = "/api/tickets/:ref" function = getTicket }
  68. { pattern = "/api/tickets/:ref/comments" function = postComment }
  69. { pattern = "/api/tickets/:ref/state" function = postState }
  70. { pattern = "/api/tickets/:ref/edit" function = postEdit }
  71. { pattern = "/api/tickets/:ref/assign" function = postAssign }
  72. { pattern = "/api/tickets/:ref/parent" function = postParent }
  73. { pattern = "/api/tickets/:ref/blocked-by" function = postBlockers }
  74. { pattern = "/api/projects" function = getProjects }
  75. { pattern = "/api/inbox" function = getInbox }
  76. { pattern = "/api/projects/:slug" function = getProject }
  77. { pattern = "/api/projects/:slug/members" function = postMembers }
  78. { pattern = "/api/projects/:slug/members/remove" function = postMemberRemove }
  79. { pattern = "/api/projects/:slug/invites" function = postInvites }
  80. { pattern = "/api/connect/exchange" function = connectExchange }
  81. { pattern = "/api/projects/:slug/connections" function = projectConnections }
  82. { pattern = "/api/projects/:slug/connections/remove" function = projectDisconnect }
  83. { pattern = "/connect" function = connectStart }
  84. { pattern = "/api/projects/:slug/tickets" function = projectTickets }
  85. { pattern = "/api/projects/:slug/tickets/:number" function = getProjectTicket }
  86. { pattern = "/api/projects/:slug/tickets/:number/comments" function = postProjectComment }
  87. { pattern = "/api/projects/:slug/tickets/:number/state" function = postProjectState }
  88. { pattern = "/api/projects/:slug/tickets/:number/edit" function = postProjectEdit }
  89. { pattern = "/api/projects/:slug/tickets/:number/assign" function = postProjectAssign }
  90. { pattern = "/api/projects/:slug/tickets/:number/parent" function = postProjectParent }
  91. { pattern = "/api/projects/:slug/tickets/:number/blocked-by" function = postProjectBlockers }
  92. { pattern = "/" component = TicketList }
  93. { pattern = "/project/:projectName" component = TicketList }
  94. { pattern = "/state/:stateSlug" component = TicketList }
  95. { pattern = "/project/:projectName/state/:stateSlug" component = TicketList }
  96. { pattern = "/inbox" component = Inbox }
  97. { pattern = "/new-project" component = NewProject }
  98. { pattern = "/connect/:nonce" component = Connect }
  99. { pattern = "/projects/:projectSlug/settings" component = Settings }
  100. { pattern = "/you" component = You }
  101. // OLD PAGE URLS → 301 to the short ones (ticket #25): `/tickets/<old global number or uuid>`, `/projects/<slug>`,
  102. // `/projects/<slug>/<number>`, `/projects/<slug>/tickets/<number>`
  103. { pattern = "/tickets/:ref" function = oldTicketPage }
  104. { pattern = "/projects/:slug" function = oldProjectPage }
  105. { pattern = "/projects/:slug/:number" function = oldProjectTicketPage }
  106. { pattern = "/projects/:slug/tickets/:number" function = oldProjectTicketPage }
  107. // THE SHORT PAGE URLS (ticket #25): the project `/<slug>`, a ticket `/<slug>/<number>`. They match any first
  108. // segment, so they come last — and below, after the construction, behind hl:web's own urls too (`lastPage`)
  109. { pattern = "/:projectName" component = TicketList lastPage = true }
  110. { pattern = "/:projectSlug/:ticketNumber" component = Ticket lastPage = true }
  111. ]
  112. // WHO GETS THE PUSH: tickets and events are public (reading needs no login). A frame
  113. // reaches a connection only if a mounted component listens for it.
  114. // `signedIn` / `signedOut` (ticket #7) go to the tabs of ONE session: the one whose login
  115. // carries that random tag (session.data.tag, set at login) — every page of it switches to
  116. // logged in / out without a reload.
  117. audience = {
  118. ticketCreated = (row, session) => { return true }
  119. ticketEvent = (ticketId, ev, row, session) => { return true }
  120. // ticket #4: the relation views (tickets-helpers.hl relationViewOf) of a ticket and its related ones
  121. ticketsRelated = (views, session) => { return true }
  122. // ticket #21: an app was connected / disconnected — the project page refreshes its line
  123. connectionsChanged = (slug, session) => { return true }
  124. signedIn = (tag, info, session) => { return tag != null && tagOf(session) == tag }
  125. signedOut = (tag, session) => { return tag != null && tagOf(session) == tag }
  126. }
  127. sessionDir = env('TICKETS_SESSIONS') != null ? env('TICKETS_SESSIONS') : null
  128. port = env('TICKETS_PORT') != null ? toNumber(env('TICKETS_PORT')) : 8350
  129. // THE LISTENER's interface (mission 010, deploy to Byrodin): hl:web reads HL_HOST (or HOST)
  130. // itself (hybriel#24): 127.0.0.1 on Byrodin behind nginx; unset = 0.0.0.0 (dev on Loreana).
  131. // TICKETS_WATCH=0 turns the dev watcher off (the container: a deploy is an rsync + restart,
  132. // half-copied .hl files must not be re-analysed); unset = on, as before.
  133. watching = env('TICKETS_WATCH') != '0'
  134. // THE MIGRATION of ticket #20 (lib/migrate.hl): idempotent, runs at every start
  135. for (line of migrateAll()) { console.log('migrated: ' + line) }
  136. server = new WebFramework(routes = routes, styles = styles, minify = true, port = port, watchMode = watching)
  137. // THE SHORT PAGE URLS GO BEHIND hl:web's OWN (ticket #25). The first match answers, and hl:web APPENDS its urls
  138. // (/__hl/app.css, /__hl/emit, /__hl/sw.js …, the component modules /components/<file>.hl) to the table while it is
  139. // constructed: left where they are, `/:projectSlug/:ticketNumber` answered /__hl/app.css and /components/ticket.hl
  140. // with a ticket page. They stay in the table above (hl:web serves the modules of the components routes mount, and
  141. // the service worker knows them as pages) and are moved to the end of the router's table here — the one that
  142. // answers requests and that the browser gets for its links. A first segment any route answers is a reserved slug
  143. // (lib/projects.hl reservedSlugs; the gate checks both).
  144. pagesFirst = []
  145. pagesLast = []
  146. for (r of server.router.routes) { if (r.lastPage == true) { pagesLast[] = r } else { pagesFirst[] = r } }
  147. for (r of pagesLast) { pagesFirst[] = r }
  148. server.router.routes = pagesFirst
  149. // THE GLOBAL ERROR HANDLER (ticket #7): a plugin's failure — above all hl:fetch's refused /
  150. // timed-out exchange with ident (users.hl exchangeCode, invites.hl) — arrives here instead of aborting the
  151. // request with a 500 that shows source paths; the failed call yields null and the caller answers
  152. // it ("ident did not answer"). Every absorbed error is logged.
  153. on Error(e) { console.log('error absorbed: ' + e.message) }

Branches

  • mainmain branch

Latest commits

  • bb64d57cmission 012 (ticket #25 API part): every /api/projects/<x>/… route takes the project's id (12-char record id) as well as its slug, same answers byte for byte; tickets stay under their project, no global /api/tickets/<x>. gate 315/0 (old code: the 13 id checks fail), connect 60/0, live-data copy old vs new 1,484 identical, slug vs id 772 identicalmre
  • 7538b034mission 011 (ticket #25 web part): short page URLs /<slug> and /<slug>/<number>; /projects/<slug>, /projects/<slug>/<n>, /projects/<slug>/tickets/<n> and /tickets/<ref> answer 301 (current slug); slug pages moved behind hl:web's own routes after construction (they answered /__hl/* and /components/*.hl); reserved slugs refused; API paths unchanged. gate 293/0, connect 60/0, live-data copy 187/0, API old vs new: only page links differmre
  • 0369106emission 010 (code order) 4/4: README file map + import order + 'Same output' test, STATUS (entry, lessons, how to verify), LOG, report; tests/realdata-baseline.mjs + realdata-compare*.py (a cleanup answers the same on live data), tests/letcount.pymre
  • a75e0279mission 010 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (456 lets → plain declarations; Hybriel refuses a plain declaration inside a loop on its 2nd pass). gate 249/0, connect 60/0, real-data reads identical, a 50-step write sequence (API + faces) identical to the old codemre
  • e9d5c618mission 010 (code order) 2/4: one lib/ file per topic — store.hl split into projects / tickets (+ relations) / events / tickets-helpers, util.hl shared helpers (env, storage dir, URLs, sorts, Vienna time), the function routes out of project.hl into lib/api.hl (thin; auth/filters/Accept in api-helpers.hl), invite + member-removal logic out of the faces/routes into invites.hl / tickets.hl; project.hl is the map. /login/callback gets req + the session store by reference. gate 249/0, connect 60/0, real-data reads identicalmre
  • 97e269b5mission 010 (code order) 1/4: .hl files out of the root — lib/ (store, users, connections, invites, migrate, markdown, mdview, import = ticketfile, util = localtime, jsoncheck, api-helpers = api), tools/import.hl, components/styles.hl; import paths only. gate 249/0, connect 60/0, real-data reads identicalmre
  • 38f9d10ftickets: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gate 249/0, connect 60/0mre
  • d3db6139tickets: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 249/0, connect 60/0mre
  • bce182e3tickets: Hybriel master 7eea0d32 (#126 memory, #48 lambda copies its argument); migrate.hl lambdas take &logmre
  • 4137be0fantcolony#40: mission references point to the moved missionsmre
  • 9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mre
  • 2ab91ee9tickets: gate checks rows appear once (session sync); re-vendor to ff51cf46 stopped on hybriel#122, stays 837fe120mre
  • e01c2b1dtickets#24: installable app (manifest, service worker, offline list), own icon; gate waits for the hello's pongmre
  • 752fbb7fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 38bdd5e4deploy.sh: never send .git or .gitignore to Byrodinmre
  • f12fa1bcState of 2026-09-27, before the move to gitoriamre