tickets
All repositories: gitoria
11.2 KB
// project.hl — tickets.worldapi.org: THE MAP. Config, routes and WHO HEARS WHAT; the logic lives in lib/.//// WHERE THINGS ARE (one line per feature; README.md "Files" has the full list):// tickets, their states and relations (parent / blocked-by) lib/tickets.hl (state names, page shapes: lib/tickets-helpers.hl)// the history of a ticket (events) lib/events.hl// projects, members, roles, slugs lib/projects.hl// users, login through ident, API tokens, sessions lib/users.hl// invites to a project (ident#22) lib/invites.hl// connecting an app to a project (ticket #21) lib/connections.hl// Markdown in text fields / the Markdown read view lib/markdown.hl / lib/mdview.hl// the JSON API + /login/callback, /connect, /tickets/:ref lib/api.hl (plumbing: lib/api-helpers.hl, lib/jsoncheck.hl)// the ticket #20 data migration (every start) lib/migrate.hl// the ticket-file format of the import lib/import.hl (the import tool: tools/import.hl)// shared small helpers (env, storage dir, URLs, sorting, Vienna time) lib/util.hl// pages: shell, list, ticket, inbox, you, new project, settings, connect components/*.hl; CSS components/styles.hlimport WebFramework from 'hl:web'import { env } from 'hl:proc'import Styles from './components/styles.hl'import { dark, darker } from './shared/tokens.hl'import { migrateAll } from './lib/migrate.hl'import { tagOf } from './lib/users.hl'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'import TicketList from './components/ticket_list.hl'import Ticket from './components/ticket.hl'import Inbox from './components/inbox.hl'import You from './components/you.hl'import NewProject from './components/new_project.hl'import Settings from './components/settings.hl'import Connect from './components/connect.hl'static siteName = "tickets"appTitle = siteNamestyles = Styles// ---- THE INSTALLABLE APP (mission 046), the same way calendar.worldapi.org does it: hl:web's own web app manifest// (/__hl/manifest.webmanifest, linked from every head with the apple-touch-icon and theme-color) and service worker// (/__hl/sw.js) from these settings — no JavaScript of ours. Icons in icons/ (icon.svg, a ticket stub, is the source;// the PNGs and favicon.ico are rendered from it with rsvg-convert / magick, README "PWA"). The theme colour is the// header's background (darker, as tracker), the splash background the page's (dark) — both off the tokens, not written out again.appThemeColor = darker.valueappBackgroundColor = dark.valueappIcons = [{ src = '/icons/icon-192.png' sizes = '192x192' purpose = 'any' }{ src = '/icons/icon-512.png' sizes = '512x512' purpose = 'any' }{ src = '/icons/icon-192.png' sizes = '192x192' purpose = 'maskable' }{ src = '/icons/icon-512.png' sizes = '512x512' purpose = 'maskable' }]appTouchIcon = '/icons/apple-touch-icon.png'appFavicon = '/icons/icon.svg'// OFFLINE: the shell and `/` — `/` IS the ticket list, so offline it shows the list as this browser last loaded it,// and the shell (components/main.hl) says "You are offline". Its value-form emits are never queued. Other pages are// not kept, except a project list (/<slug>, same component) this browser visited; the rest get hl:web's// "Unavailable offline" page.offline = [ TicketList ]// ---- THE ROUTES: files, the function routes (lib/api.hl), the pages (components/) ----------------routes = [{ pattern = "/favicon.ico" file = "./icons/favicon.ico" headers = { 'Cache-Control' = 'no-cache' } }// the installable app (mission 046): the icons (manifest and service worker are hl:web's own, from appIcons / offline){ pattern = "/icons/icon-192.png" file = "./icons/icon-192.png" headers = { 'Cache-Control' = 'no-cache' } }{ pattern = "/icons/icon-512.png" file = "./icons/icon-512.png" headers = { 'Cache-Control' = 'no-cache' } }{ pattern = "/icons/apple-touch-icon.png" file = "./icons/apple-touch-icon.png" headers = { 'Cache-Control' = 'no-cache' } }{ pattern = "/icons/icon.svg" file = "./icons/icon.svg" headers = { 'Cache-Control' = 'no-cache' } }// the login button's return (ticket #7): it needs the request's session and the framework's session store, BY REFERENCE{ pattern = "/login/callback" function = (route, req) => { return loginCallback(route, &req, &server.sessions) } }{ pattern = "/login.js" file = "./login.js" headers = { 'Cache-Control' = 'no-cache' } }// ticket #12 (mission 024): the Markdown editor <md-editor>, vendored from worldapi-components{ pattern = "/md-editor.js" file = "./shared/md-editor.js" headers = { 'Cache-Control' = 'no-cache' } }{ pattern = "/api/tickets" function = postTicket }{ pattern = "/api/tickets/:ref" function = getTicket }{ pattern = "/api/tickets/:ref/comments" function = postComment }{ pattern = "/api/tickets/:ref/state" function = postState }{ pattern = "/api/tickets/:ref/edit" function = postEdit }{ pattern = "/api/tickets/:ref/assign" function = postAssign }{ pattern = "/api/tickets/:ref/parent" function = postParent }{ pattern = "/api/tickets/:ref/blocked-by" function = postBlockers }{ pattern = "/api/projects" function = getProjects }{ pattern = "/api/inbox" function = getInbox }{ pattern = "/api/projects/:slug" function = getProject }{ pattern = "/api/projects/:slug/members" function = postMembers }{ pattern = "/api/projects/:slug/members/remove" function = postMemberRemove }{ pattern = "/api/projects/:slug/invites" function = postInvites }{ pattern = "/api/connect/exchange" function = connectExchange }{ pattern = "/api/projects/:slug/connections" function = projectConnections }{ pattern = "/api/projects/:slug/connections/remove" function = projectDisconnect }{ pattern = "/connect" function = connectStart }{ pattern = "/api/projects/:slug/tickets" function = projectTickets }{ pattern = "/api/projects/:slug/tickets/:number" function = getProjectTicket }{ pattern = "/api/projects/:slug/tickets/:number/comments" function = postProjectComment }{ pattern = "/api/projects/:slug/tickets/:number/state" function = postProjectState }{ pattern = "/api/projects/:slug/tickets/:number/edit" function = postProjectEdit }{ pattern = "/api/projects/:slug/tickets/:number/assign" function = postProjectAssign }{ pattern = "/api/projects/:slug/tickets/:number/parent" function = postProjectParent }{ pattern = "/api/projects/:slug/tickets/:number/blocked-by" function = postProjectBlockers }{ pattern = "/" component = TicketList }{ pattern = "/project/:projectName" component = TicketList }{ pattern = "/state/:stateSlug" component = TicketList }{ pattern = "/project/:projectName/state/:stateSlug" component = TicketList }{ pattern = "/inbox" component = Inbox }{ pattern = "/new-project" component = NewProject }{ pattern = "/connect/:nonce" component = Connect }{ pattern = "/projects/:projectSlug/settings" component = Settings }{ pattern = "/you" component = You }// OLD PAGE URLS → 301 to the short ones (ticket #25): `/tickets/<old global number or uuid>`, `/projects/<slug>`,// `/projects/<slug>/<number>`, `/projects/<slug>/tickets/<number>`{ pattern = "/tickets/:ref" function = oldTicketPage }{ pattern = "/projects/:slug" function = oldProjectPage }{ pattern = "/projects/:slug/:number" function = oldProjectTicketPage }{ pattern = "/projects/:slug/tickets/:number" function = oldProjectTicketPage }// THE SHORT PAGE URLS (ticket #25): the project `/<slug>`, a ticket `/<slug>/<number>`. They match any first// segment, so they come last — and below, after the construction, behind hl:web's own urls too (`lastPage`){ pattern = "/:projectName" component = TicketList lastPage = true }{ pattern = "/:projectSlug/:ticketNumber" component = Ticket lastPage = true }]// WHO GETS THE PUSH: tickets and events are public (reading needs no login). A frame// reaches a connection only if a mounted component listens for it.// `signedIn` / `signedOut` (ticket #7) go to the tabs of ONE session: the one whose login// carries that random tag (session.data.tag, set at login) — every page of it switches to// logged in / out without a reload.audience = {ticketCreated = (row, session) => { return true }ticketEvent = (ticketId, ev, row, session) => { return true }// ticket #4: the relation views (tickets-helpers.hl relationViewOf) of a ticket and its related onesticketsRelated = (views, session) => { return true }// ticket #21: an app was connected / disconnected — the project page refreshes its lineconnectionsChanged = (slug, session) => { return true }signedIn = (tag, info, session) => { return tag != null && tagOf(session) == tag }signedOut = (tag, session) => { return tag != null && tagOf(session) == tag }}sessionDir = env('TICKETS_SESSIONS') != null ? env('TICKETS_SESSIONS') : nullport = env('TICKETS_PORT') != null ? toNumber(env('TICKETS_PORT')) : 8350// THE LISTENER's interface (mission 010, deploy to Byrodin): hl:web reads HL_HOST (or HOST)// itself (hybriel#24): 127.0.0.1 on Byrodin behind nginx; unset = 0.0.0.0 (dev on Loreana).// TICKETS_WATCH=0 turns the dev watcher off (the container: a deploy is an rsync + restart,// half-copied .hl files must not be re-analysed); unset = on, as before.watching = env('TICKETS_WATCH') != '0'// THE MIGRATION of ticket #20 (lib/migrate.hl): idempotent, runs at every startfor (line of migrateAll()) { console.log('migrated: ' + line) }server = new WebFramework(routes = routes, styles = styles, minify = true, port = port, watchMode = watching)// THE SHORT PAGE URLS GO BEHIND hl:web's OWN (ticket #25). The first match answers, and hl:web APPENDS its urls// (/__hl/app.css, /__hl/emit, /__hl/sw.js …, the component modules /components/<file>.hl) to the table while it is// constructed: left where they are, `/:projectSlug/:ticketNumber` answered /__hl/app.css and /components/ticket.hl// with a ticket page. They stay in the table above (hl:web serves the modules of the components routes mount, and// the service worker knows them as pages) and are moved to the end of the router's table here — the one that// answers requests and that the browser gets for its links. A first segment any route answers is a reserved slug// (lib/projects.hl reservedSlugs; the gate checks both).pagesFirst = []pagesLast = []for (r of server.router.routes) { if (r.lastPage == true) { pagesLast[] = r } else { pagesFirst[] = r } }for (r of pagesLast) { pagesFirst[] = r }server.router.routes = pagesFirst// THE GLOBAL ERROR HANDLER (ticket #7): a plugin's failure — above all hl:fetch's refused /// timed-out exchange with ident (users.hl exchangeCode, invites.hl) — arrives here instead of aborting the// request with a 500 that shows source paths; the failed call yields null and the caller answers// it ("ident did not answer"). Every absorbed error is logged.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