@starlove/orbit 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +10 -8
- package/README.md +146 -164
- package/dist/full/csp.txt +1 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
- package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
- package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
- package/dist/full/fonts/Mona_Sans/README.txt +10 -0
- package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
- package/dist/full/fonts/Newsreader/README.txt +8 -0
- package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
- package/dist/full/index.html +36 -653
- package/dist/full/js/app.BmuponPJ.js +32 -0
- package/dist/full/js/app.BmuponPJ.js.map +1 -0
- package/dist/full/js/chunk.1fJGQGbW.js +2 -0
- package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
- package/dist/full/js/chunk.5D7JO2zp.js +2 -0
- package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
- package/dist/full/js/chunk.BrNn_D79.js +2 -0
- package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
- package/dist/full/js/chunk.CokJXIqM.js +2 -0
- package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
- package/dist/full/js/chunk.D_Haqy6R.js +2 -0
- package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
- package/dist/full/js/chunk.RO0jsH6I.js +15 -0
- package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
- package/dist/full/js/chunk.dqWOEtBM.js +2 -0
- package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
- package/dist/full/js/warm.f260bbbc.js +15 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg-wide.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.2b154ffe.css +1 -0
- package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_GUIDE.md +114 -0
- package/docs/UPDATING.md +249 -64
- package/lib/adapters/indexeddb/blobs.d.ts +3 -0
- package/lib/adapters/indexeddb/blobs.js +54 -0
- package/lib/adapters/indexeddb/database.d.ts +60 -0
- package/lib/adapters/indexeddb/database.js +126 -0
- package/lib/adapters/indexeddb/index.d.ts +5 -0
- package/lib/adapters/indexeddb/index.js +3 -0
- package/lib/adapters/indexeddb/locks.d.ts +14 -0
- package/lib/adapters/indexeddb/locks.js +40 -0
- package/lib/adapters/indexeddb/store.d.ts +40 -0
- package/lib/adapters/indexeddb/store.js +424 -0
- package/lib/adapters/indexeddb/tables.d.ts +43 -0
- package/lib/adapters/indexeddb/tables.js +127 -0
- package/lib/adapters/memory/blobs.d.ts +3 -0
- package/lib/adapters/memory/blobs.js +44 -0
- package/lib/adapters/memory/board.d.ts +41 -0
- package/lib/adapters/memory/board.js +521 -0
- package/lib/adapters/memory/index.d.ts +2 -0
- package/lib/adapters/memory/index.js +2 -0
- package/lib/adapters/memory/search.d.ts +6 -0
- package/lib/adapters/memory/search.js +30 -0
- package/lib/adapters/memory/store.d.ts +34 -0
- package/lib/adapters/memory/store.js +266 -0
- package/lib/adapters/sqlite/backups.d.ts +53 -0
- package/lib/adapters/sqlite/backups.js +232 -0
- package/lib/adapters/sqlite/blobs.d.ts +14 -0
- package/lib/adapters/sqlite/blobs.js +109 -0
- package/lib/adapters/sqlite/board.d.ts +77 -0
- package/lib/adapters/sqlite/board.js +328 -0
- package/lib/adapters/sqlite/database.d.ts +67 -0
- package/lib/adapters/sqlite/database.js +285 -0
- package/lib/adapters/sqlite/index.d.ts +3 -0
- package/lib/adapters/sqlite/index.js +2 -0
- package/lib/adapters/sqlite/lock.d.ts +11 -0
- package/lib/adapters/sqlite/lock.js +34 -0
- package/lib/adapters/sqlite/records.d.ts +54 -0
- package/lib/adapters/sqlite/records.js +546 -0
- package/lib/adapters/sqlite/schema.d.ts +54 -0
- package/lib/adapters/sqlite/schema.js +276 -0
- package/lib/adapters/sqlite/store.d.ts +39 -0
- package/lib/adapters/sqlite/store.js +385 -0
- package/lib/browser/in-page.d.ts +31 -0
- package/lib/browser/in-page.js +46 -0
- package/lib/browser/old-preview.d.ts +13 -0
- package/lib/browser/old-preview.js +80 -0
- package/lib/browser/preview-board.d.ts +8 -0
- package/lib/browser/preview-board.js +69 -0
- package/lib/cli/main.d.ts +7 -0
- package/lib/cli/main.js +343 -0
- package/lib/cli/orbit.d.ts +8 -0
- package/lib/cli/orbit.js +25 -0
- package/lib/client/http.d.ts +38 -0
- package/lib/client/http.js +334 -0
- package/lib/client/local.d.ts +26 -0
- package/lib/client/local.js +190 -0
- package/lib/client/object-url.d.ts +8 -0
- package/lib/client/object-url.js +21 -0
- package/lib/client/types.d.ts +268 -0
- package/lib/client/types.js +8 -0
- package/lib/commands/agent-tools.d.ts +31 -0
- package/lib/commands/agent-tools.js +100 -0
- package/lib/commands/index.d.ts +56 -0
- package/lib/commands/index.js +298 -0
- package/lib/commands/parse.d.ts +73 -0
- package/lib/commands/parse.js +231 -0
- package/lib/commands/present.d.ts +134 -0
- package/lib/commands/present.js +198 -0
- package/lib/commands/session.d.ts +85 -0
- package/lib/commands/session.js +138 -0
- package/lib/commands/verbs.d.ts +35 -0
- package/lib/commands/verbs.js +842 -0
- package/lib/core/actors.d.ts +18 -0
- package/lib/core/actors.js +45 -0
- package/lib/core/agent.d.ts +128 -0
- package/lib/core/agent.js +323 -0
- package/lib/core/boards.d.ts +113 -0
- package/lib/core/boards.js +288 -0
- package/lib/core/bootstrap.d.ts +47 -0
- package/lib/core/bootstrap.js +100 -0
- package/lib/core/bytes.d.ts +10 -0
- package/lib/core/bytes.js +52 -0
- package/lib/core/card-paths.d.ts +25 -0
- package/lib/core/card-paths.js +44 -0
- package/lib/core/context.d.ts +60 -0
- package/lib/core/context.js +42 -0
- package/lib/core/cross-board.d.ts +81 -0
- package/lib/core/cross-board.js +228 -0
- package/lib/core/describe.d.ts +21 -0
- package/lib/core/describe.js +81 -0
- package/lib/core/errors.d.ts +159 -0
- package/lib/core/errors.js +160 -0
- package/lib/core/files.d.ts +79 -0
- package/lib/core/files.js +262 -0
- package/lib/core/history.d.ts +20 -0
- package/lib/core/history.js +53 -0
- package/lib/core/image-types.d.ts +16 -0
- package/lib/core/image-types.js +36 -0
- package/lib/core/model.d.ts +263 -0
- package/lib/core/model.js +34 -0
- package/lib/core/nebula-link.d.ts +14 -0
- package/lib/core/nebula-link.js +39 -0
- package/lib/core/relations.d.ts +58 -0
- package/lib/core/relations.js +112 -0
- package/lib/core/search.d.ts +38 -0
- package/lib/core/search.js +92 -0
- package/lib/core/sessions.d.ts +44 -0
- package/lib/core/sessions.js +158 -0
- package/lib/core/snapshot-schema.d.ts +295 -0
- package/lib/core/snapshot-schema.js +82 -0
- package/lib/core/snapshots.d.ts +75 -0
- package/lib/core/snapshots.js +409 -0
- package/lib/core/states.d.ts +25 -0
- package/lib/core/states.js +126 -0
- package/lib/core/text.d.ts +40 -0
- package/lib/core/text.js +113 -0
- package/lib/core/tickets.d.ts +69 -0
- package/lib/core/tickets.js +311 -0
- package/lib/core/trello.d.ts +17 -0
- package/lib/core/trello.js +269 -0
- package/lib/core/verdicts.d.ts +28 -0
- package/lib/core/verdicts.js +92 -0
- package/lib/core/views.d.ts +95 -0
- package/lib/core/views.js +186 -0
- package/lib/http/app.d.ts +46 -0
- package/lib/http/app.js +491 -0
- package/lib/mcp-server.d.ts +10 -0
- package/lib/mcp-server.js +120 -0
- package/lib/node/assets.d.ts +7 -0
- package/lib/node/assets.js +68 -0
- package/lib/node/data-folder.d.ts +21 -0
- package/lib/node/data-folder.js +34 -0
- package/lib/node/env.d.ts +38 -0
- package/lib/node/env.js +49 -0
- package/lib/node/host-check.d.ts +24 -0
- package/lib/node/host-check.js +53 -0
- package/lib/node/host-services.d.ts +9 -0
- package/lib/node/host-services.js +150 -0
- package/lib/node/local.d.ts +22 -0
- package/lib/node/local.js +61 -0
- package/lib/node/migrate.d.ts +53 -0
- package/lib/node/migrate.js +635 -0
- package/lib/node/package.d.ts +7 -0
- package/lib/node/package.js +14 -0
- package/lib/node/repo-board.d.ts +49 -0
- package/lib/node/repo-board.js +245 -0
- package/lib/node/serve.d.ts +8 -0
- package/lib/node/serve.js +106 -0
- package/lib/orbit.d.ts +47 -0
- package/lib/orbit.js +44 -0
- package/lib/quiet-sqlite-warning.d.ts +1 -0
- package/lib/quiet-sqlite-warning.js +18 -0
- package/lib/store/blob-path.d.ts +6 -0
- package/lib/store/blob-path.js +12 -0
- package/lib/store/conformance.d.ts +52 -0
- package/lib/store/conformance.js +1449 -0
- package/lib/store/search-words.d.ts +10 -0
- package/lib/store/search-words.js +19 -0
- package/lib/store/types.d.ts +452 -0
- package/lib/store/types.js +34 -0
- package/llms.txt +63 -0
- package/package.json +109 -25
- package/src/cli/orbit.js +11 -658
- package/src/mcp-server.js +16 -876
- package/SKILL-ORBIT.md +0 -151
- package/build.mjs +0 -210
- package/dist/full/js/app.4e6fbe20.js +0 -952
- package/dist/full/js/app.4e6fbe20.js.map +0 -7
- package/dist/full/styles.4e60d824.css +0 -1
- package/docs/AGENT_PROTOCOL.md +0 -424
- package/docs/MCP_SETUP.md +0 -54
- package/docs/ORBIT_DISPATCH.md +0 -280
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -13
- package/docs/setup/orbit-init-wizard.md +0 -174
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +0 -93
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +0 -93
- package/public/fonts/Inter/README.txt +0 -118
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/README.txt +0 -79
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +0 -100
- package/public/fonts/Orbitron/OFL.txt +0 -93
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +0 -68
- package/public/fonts/Rajdhani/OFL.txt +0 -93
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +0 -769
- package/public/js/actor-labels.js +0 -65
- package/public/js/api.js +0 -39
- package/public/js/app.js +0 -45
- package/public/js/artifact-drawer.js +0 -79
- package/public/js/board-export.js +0 -161
- package/public/js/board-menu.js +0 -374
- package/public/js/card-actions.js +0 -545
- package/public/js/config.js +0 -20
- package/public/js/create-card.js +0 -190
- package/public/js/dom.js +0 -19
- package/public/js/drawer.js +0 -273
- package/public/js/font-preference.js +0 -230
- package/public/js/format.js +0 -433
- package/public/js/help-menu.js +0 -31
- package/public/js/icons.js +0 -16
- package/public/js/kanban.js +0 -694
- package/public/js/local-backend.js +0 -1015
- package/public/js/main.js +0 -161
- package/public/js/modal.js +0 -33
- package/public/js/motion-preference.js +0 -64
- package/public/js/panel.js +0 -58
- package/public/js/router.js +0 -134
- package/public/js/search.js +0 -56
- package/public/js/settings-tabs.js +0 -7
- package/public/js/settings.js +0 -1634
- package/public/js/sse.js +0 -159
- package/public/js/starscape.js +0 -332
- package/public/js/state.js +0 -129
- package/public/js/text-editing.js +0 -128
- package/public/js/theme-preference.js +0 -95
- package/public/js/ticket-detail.js +0 -1516
- package/public/js/ticket-typeahead.js +0 -49
- package/public/js/toast.js +0 -89
- package/public/js/topbar-notes.js +0 -155
- package/public/js/transport.js +0 -22
- package/public/js/typeahead.js +0 -206
- package/public/js/unread.js +0 -121
- package/public/js/update-check.js +0 -58
- package/public/js/url-routes.js +0 -94
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg-wide.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +0 -7709
- package/src/core/agent.js +0 -387
- package/src/core/agents-md.js +0 -109
- package/src/core/artifacts.js +0 -149
- package/src/core/attachments.js +0 -238
- package/src/core/auth.js +0 -85
- package/src/core/backups.js +0 -142
- package/src/core/board.js +0 -148
- package/src/core/boards.js +0 -409
- package/src/core/bootstrap.js +0 -170
- package/src/core/css-bundle.js +0 -30
- package/src/core/db.js +0 -315
- package/src/core/delete-board.js +0 -163
- package/src/core/dispatch.js +0 -1007
- package/src/core/events.js +0 -314
- package/src/core/http.js +0 -115
- package/src/core/migrate.js +0 -198
- package/src/core/paths.js +0 -55
- package/src/core/provision-repo-board.js +0 -119
- package/src/core/queries.js +0 -246
- package/src/core/registry.js +0 -211
- package/src/core/relations.js +0 -163
- package/src/core/review-verdicts.js +0 -138
- package/src/core/router.js +0 -556
- package/src/core/search.js +0 -183
- package/src/core/seed.js +0 -234
- package/src/core/snapshots.js +0 -692
- package/src/core/states.js +0 -167
- package/src/core/system-picker.js +0 -109
- package/src/core/tickets.js +0 -400
- package/src/core/util.js +0 -106
- package/src/core/version-check.js +0 -154
- package/src/mcp/orbit-client.js +0 -287
- package/src/server.js +0 -115
|
@@ -1,1015 +0,0 @@
|
|
|
1
|
-
// Preview-edition local backend. Replaces transport.js at build time so the
|
|
2
|
-
// app's existing `api()` calls land in IndexedDB instead of an HTTP server.
|
|
3
|
-
// Single-board, no auth, no agent routes — just enough surface to drive the
|
|
4
|
-
// kanban UI for a "try it in the browser" demo.
|
|
5
|
-
//
|
|
6
|
-
// Shape parity matters more than internal cleanliness here: every response
|
|
7
|
-
// has to look enough like the server's JSON that the UI doesn't notice.
|
|
8
|
-
|
|
9
|
-
const DB_NAME = "starscape-orbit-preview";
|
|
10
|
-
const DB_VERSION = 1;
|
|
11
|
-
const BOARD_ID = "preview-board";
|
|
12
|
-
|
|
13
|
-
// Object stores (each is a SQL table, roughly).
|
|
14
|
-
const STORES = [
|
|
15
|
-
"boards", // single record id=BOARD_ID
|
|
16
|
-
"states", // {id, board_id, name, position, is_default, role, created_at}
|
|
17
|
-
"labels", // {id, board_id, name, color, created_at}
|
|
18
|
-
"tickets", // full ticket row (no joins)
|
|
19
|
-
"ticket_labels", // {ticket_id, label_id} composite key as `${ticket_id}|${label_id}`
|
|
20
|
-
"comments", // {id, ticket_id, author, kind, body, omitted_for_ai, created_at}
|
|
21
|
-
"relations", // {id, source_ticket_id, target_ticket_id, kind, created_at, ...}
|
|
22
|
-
"board_entries", // {id, board_id, type, title, body, ticket_id, struck_at, created_at, updated_at}
|
|
23
|
-
"events", // {id, board_id, ticket_id, type, actor, body_json, created_at}
|
|
24
|
-
"meta" // {key, value} — schema_seeded, etc.
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
let dbPromise = null;
|
|
28
|
-
|
|
29
|
-
function openDb() {
|
|
30
|
-
if (dbPromise) return dbPromise;
|
|
31
|
-
dbPromise = new Promise((resolve, reject) => {
|
|
32
|
-
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
33
|
-
req.onupgradeneeded = () => {
|
|
34
|
-
const db = req.result;
|
|
35
|
-
for (const name of STORES) {
|
|
36
|
-
if (db.objectStoreNames.contains(name)) continue;
|
|
37
|
-
if (name === "ticket_labels") {
|
|
38
|
-
const store = db.createObjectStore(name, { keyPath: "key" });
|
|
39
|
-
store.createIndex("ticket_id", "ticket_id");
|
|
40
|
-
store.createIndex("label_id", "label_id");
|
|
41
|
-
} else if (name === "meta") {
|
|
42
|
-
db.createObjectStore(name, { keyPath: "key" });
|
|
43
|
-
} else {
|
|
44
|
-
const store = db.createObjectStore(name, { keyPath: "id" });
|
|
45
|
-
if (name === "tickets") {
|
|
46
|
-
store.createIndex("board_id", "board_id");
|
|
47
|
-
store.createIndex("number", "number");
|
|
48
|
-
store.createIndex("parent_ticket_id", "parent_ticket_id");
|
|
49
|
-
}
|
|
50
|
-
if (name === "comments" || name === "events") {
|
|
51
|
-
store.createIndex("ticket_id", "ticket_id");
|
|
52
|
-
}
|
|
53
|
-
if (name === "relations") {
|
|
54
|
-
store.createIndex("source_ticket_id", "source_ticket_id");
|
|
55
|
-
store.createIndex("target_ticket_id", "target_ticket_id");
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
req.onsuccess = () => resolve(req.result);
|
|
61
|
-
req.onerror = () => reject(req.error);
|
|
62
|
-
});
|
|
63
|
-
return dbPromise;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function tx(db, storeNames, mode = "readonly") {
|
|
67
|
-
const t = db.transaction(storeNames, mode);
|
|
68
|
-
const stores = {};
|
|
69
|
-
for (const name of storeNames) stores[name] = t.objectStore(name);
|
|
70
|
-
const done = new Promise((resolve, reject) => {
|
|
71
|
-
t.oncomplete = () => resolve();
|
|
72
|
-
t.onerror = () => reject(t.error);
|
|
73
|
-
t.onabort = () => reject(t.error);
|
|
74
|
-
});
|
|
75
|
-
return { stores, done };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function reqPromise(req) {
|
|
79
|
-
return new Promise((resolve, reject) => {
|
|
80
|
-
req.onsuccess = () => resolve(req.result);
|
|
81
|
-
req.onerror = () => reject(req.error);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function getAll(store, query, indexName) {
|
|
86
|
-
const target = indexName ? store.index(indexName) : store;
|
|
87
|
-
return reqPromise(target.getAll(query));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
91
|
-
// Helpers
|
|
92
|
-
|
|
93
|
-
function uid() {
|
|
94
|
-
return crypto.randomUUID();
|
|
95
|
-
}
|
|
96
|
-
function nowIso() {
|
|
97
|
-
return new Date().toISOString();
|
|
98
|
-
}
|
|
99
|
-
function tlKey(ticketId, labelId) {
|
|
100
|
-
return `${ticketId}|${labelId}`;
|
|
101
|
-
}
|
|
102
|
-
function slugify(input) {
|
|
103
|
-
return String(input || "")
|
|
104
|
-
.toLowerCase()
|
|
105
|
-
.trim()
|
|
106
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
107
|
-
.replace(/^-+|-+$/g, "")
|
|
108
|
-
.slice(0, 60) || "label";
|
|
109
|
-
}
|
|
110
|
-
function err(status, code) {
|
|
111
|
-
const e = new Error(code);
|
|
112
|
-
e.status = status;
|
|
113
|
-
e.payload = { error: code };
|
|
114
|
-
return e;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
118
|
-
// Seed (one-shot, mirrors src/core/seed.js minus the parent-cycle business)
|
|
119
|
-
|
|
120
|
-
async function seedIfEmpty(db) {
|
|
121
|
-
const { stores: ro } = tx(db, ["meta"], "readonly");
|
|
122
|
-
const seeded = await reqPromise(ro.meta.get("schema_seeded"));
|
|
123
|
-
if (seeded?.value) return;
|
|
124
|
-
|
|
125
|
-
const t = nowIso();
|
|
126
|
-
const stateRows = [
|
|
127
|
-
["Backlog", null],
|
|
128
|
-
["Todo", null],
|
|
129
|
-
["In Progress", "in_progress"],
|
|
130
|
-
["Review", "review"],
|
|
131
|
-
["Done", "done"],
|
|
132
|
-
["Cancelled", null]
|
|
133
|
-
].map(([name, role], position) => ({
|
|
134
|
-
id: uid(),
|
|
135
|
-
board_id: BOARD_ID,
|
|
136
|
-
name,
|
|
137
|
-
position,
|
|
138
|
-
is_default: name === "Todo" ? 1 : 0,
|
|
139
|
-
role,
|
|
140
|
-
created_at: t
|
|
141
|
-
}));
|
|
142
|
-
const stateByName = Object.fromEntries(stateRows.map((s) => [s.name, s]));
|
|
143
|
-
|
|
144
|
-
const labelRows = [
|
|
145
|
-
["human-only", "#7c2d12"],
|
|
146
|
-
["needs-human-input", "#dc2626"],
|
|
147
|
-
["needs-decomposition", "#9333ea"],
|
|
148
|
-
["needs-followup", "#b45309"],
|
|
149
|
-
["tech-debt", "#64748b"],
|
|
150
|
-
["security", "#991b1b"],
|
|
151
|
-
["onboarding", "#0f766e"]
|
|
152
|
-
].map(([name, color]) => ({
|
|
153
|
-
id: uid(),
|
|
154
|
-
board_id: BOARD_ID,
|
|
155
|
-
name,
|
|
156
|
-
color,
|
|
157
|
-
created_at: t
|
|
158
|
-
}));
|
|
159
|
-
const labelByName = Object.fromEntries(labelRows.map((l) => [l.name, l]));
|
|
160
|
-
|
|
161
|
-
const board = {
|
|
162
|
-
id: BOARD_ID,
|
|
163
|
-
slug: "preview",
|
|
164
|
-
name: "Preview Board",
|
|
165
|
-
repo_url: "",
|
|
166
|
-
system_path: "",
|
|
167
|
-
default_branch: "main",
|
|
168
|
-
project_notes:
|
|
169
|
-
"This preview lives entirely in your browser (IndexedDB). Use it to try the flow; export the snapshot from Settings → Data when you want to move to a real Orbit install.",
|
|
170
|
-
agent_instructions:
|
|
171
|
-
"Preview edition has no agent or MCP integration. The full Orbit install (npm i -g starscape-orbit && orbit init) provides those.",
|
|
172
|
-
ai_enabled: 0,
|
|
173
|
-
repo_path: "",
|
|
174
|
-
created_at: t,
|
|
175
|
-
updated_at: t
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const tickets = [];
|
|
179
|
-
const ticketLabels = [];
|
|
180
|
-
const comments = [];
|
|
181
|
-
|
|
182
|
-
function addTicket(number, fields) {
|
|
183
|
-
const id = uid();
|
|
184
|
-
tickets.push({
|
|
185
|
-
id,
|
|
186
|
-
board_id: BOARD_ID,
|
|
187
|
-
number,
|
|
188
|
-
title: fields.title,
|
|
189
|
-
description: fields.description,
|
|
190
|
-
type: fields.type,
|
|
191
|
-
parent_ticket_id: fields.parentTicketId || null,
|
|
192
|
-
ai_plan: fields.aiPlan || "",
|
|
193
|
-
implementation_summary: "",
|
|
194
|
-
implementation_updates: "",
|
|
195
|
-
state_id: fields.stateId,
|
|
196
|
-
priority: fields.priority,
|
|
197
|
-
created_by: "system",
|
|
198
|
-
archived_at: null,
|
|
199
|
-
created_at: t,
|
|
200
|
-
updated_at: t
|
|
201
|
-
});
|
|
202
|
-
for (const labelId of fields.labels || []) {
|
|
203
|
-
ticketLabels.push({ key: tlKey(id, labelId), ticket_id: id, label_id: labelId });
|
|
204
|
-
}
|
|
205
|
-
return id;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const epicId = addTicket(1, {
|
|
209
|
-
title: "Try the preview kanban",
|
|
210
|
-
description:
|
|
211
|
-
"This onboarding card is for the in-browser preview. Drag it across lanes, edit fields, add comments — it all stays in this browser only.",
|
|
212
|
-
type: "epic",
|
|
213
|
-
aiPlan: "1. Drag this card to In Progress.\n2. Add a comment.\n3. Export the snapshot from Settings → Data.",
|
|
214
|
-
stateId: stateByName.Todo.id,
|
|
215
|
-
priority: 4,
|
|
216
|
-
labels: [labelByName["onboarding"].id, labelByName["needs-decomposition"].id]
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
addTicket(2, {
|
|
220
|
-
title: "Edit a card and watch it persist",
|
|
221
|
-
description: "Click into a card, change its title, refresh — your edit survives because it lives in IndexedDB.",
|
|
222
|
-
type: "feature",
|
|
223
|
-
parentTicketId: epicId,
|
|
224
|
-
stateId: stateByName.Todo.id,
|
|
225
|
-
priority: 3,
|
|
226
|
-
labels: [labelByName["onboarding"].id]
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
addTicket(3, {
|
|
230
|
-
title: "Move me to In Progress",
|
|
231
|
-
description: "Drag this card to the In Progress lane, then to Review.",
|
|
232
|
-
type: "task",
|
|
233
|
-
parentTicketId: epicId,
|
|
234
|
-
stateId: stateByName.Backlog.id,
|
|
235
|
-
priority: 2,
|
|
236
|
-
labels: [labelByName["onboarding"].id]
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
addTicket(4, {
|
|
240
|
-
title: "Export your snapshot when ready",
|
|
241
|
-
description: "Settings → Data → Export downloads a JSON snapshot you can import into a real Orbit install.",
|
|
242
|
-
type: "task",
|
|
243
|
-
stateId: stateByName.Backlog.id,
|
|
244
|
-
priority: 1,
|
|
245
|
-
labels: [labelByName["onboarding"].id]
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
comments.push({
|
|
249
|
-
id: uid(),
|
|
250
|
-
ticket_id: epicId,
|
|
251
|
-
author: "system",
|
|
252
|
-
kind: "note",
|
|
253
|
-
body: "Welcome to the Orbit preview. Everything lives in your browser.",
|
|
254
|
-
omitted_for_ai: 0,
|
|
255
|
-
created_at: t
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
const { stores, done } = tx(
|
|
259
|
-
db,
|
|
260
|
-
["boards", "states", "labels", "tickets", "ticket_labels", "comments", "meta"],
|
|
261
|
-
"readwrite"
|
|
262
|
-
);
|
|
263
|
-
stores.boards.put(board);
|
|
264
|
-
stateRows.forEach((r) => stores.states.put(r));
|
|
265
|
-
labelRows.forEach((r) => stores.labels.put(r));
|
|
266
|
-
tickets.forEach((r) => stores.tickets.put(r));
|
|
267
|
-
ticketLabels.forEach((r) => stores.ticket_labels.put(r));
|
|
268
|
-
comments.forEach((r) => stores.comments.put(r));
|
|
269
|
-
stores.meta.put({ key: "schema_seeded", value: true });
|
|
270
|
-
await done;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
274
|
-
// Snapshot helpers (full DB read, used by bootstrap / context / export)
|
|
275
|
-
|
|
276
|
-
async function readBoardSnapshot(db) {
|
|
277
|
-
const { stores } = tx(
|
|
278
|
-
db,
|
|
279
|
-
[
|
|
280
|
-
"boards",
|
|
281
|
-
"states",
|
|
282
|
-
"labels",
|
|
283
|
-
"tickets",
|
|
284
|
-
"ticket_labels",
|
|
285
|
-
"comments",
|
|
286
|
-
"relations",
|
|
287
|
-
"board_entries",
|
|
288
|
-
"events"
|
|
289
|
-
],
|
|
290
|
-
"readonly"
|
|
291
|
-
);
|
|
292
|
-
const [board, states, labels, tickets, ticketLabels, comments, relations, boardEntries, events] = await Promise.all([
|
|
293
|
-
reqPromise(stores.boards.get(BOARD_ID)),
|
|
294
|
-
getAll(stores.states),
|
|
295
|
-
getAll(stores.labels),
|
|
296
|
-
getAll(stores.tickets),
|
|
297
|
-
getAll(stores.ticket_labels),
|
|
298
|
-
getAll(stores.comments),
|
|
299
|
-
getAll(stores.relations),
|
|
300
|
-
getAll(stores.board_entries),
|
|
301
|
-
getAll(stores.events)
|
|
302
|
-
]);
|
|
303
|
-
return { board, states, labels, tickets, ticketLabels, comments, relations, boardEntries, events };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function decorateTickets(snapshot, includeArchived = false) {
|
|
307
|
-
const { board, states, labels, tickets, ticketLabels, comments } = snapshot;
|
|
308
|
-
const labelById = Object.fromEntries(labels.map((l) => [l.id, l]));
|
|
309
|
-
const stateById = Object.fromEntries(states.map((s) => [s.id, s]));
|
|
310
|
-
const visible = tickets.filter((t) => includeArchived || !t.archived_at);
|
|
311
|
-
const sorted = [...visible].sort((a, b) => {
|
|
312
|
-
const byTime = String(b.updated_at).localeCompare(String(a.updated_at));
|
|
313
|
-
if (byTime !== 0) return byTime;
|
|
314
|
-
return String(a.id).localeCompare(String(b.id));
|
|
315
|
-
});
|
|
316
|
-
return sorted.map((ticket) => {
|
|
317
|
-
const stateRow = stateById[ticket.state_id];
|
|
318
|
-
const tLabels = ticketLabels
|
|
319
|
-
.filter((tl) => tl.ticket_id === ticket.id)
|
|
320
|
-
.map((tl) => labelById[tl.label_id])
|
|
321
|
-
.filter(Boolean)
|
|
322
|
-
.map((l) => ({ ticket_id: ticket.id, id: l.id, name: l.name, color: l.color }));
|
|
323
|
-
const commentCount = comments.filter((c) => c.ticket_id === ticket.id).length;
|
|
324
|
-
const childCount = tickets.filter((t) => t.parent_ticket_id === ticket.id && !t.archived_at).length;
|
|
325
|
-
const parent = ticket.parent_ticket_id ? tickets.find((t) => t.id === ticket.parent_ticket_id) : null;
|
|
326
|
-
const parentState = parent ? stateById[parent.state_id] : null;
|
|
327
|
-
return {
|
|
328
|
-
...ticket,
|
|
329
|
-
state_name: stateRow?.name || "",
|
|
330
|
-
state_role: stateRow?.role || null,
|
|
331
|
-
board_slug: board.slug,
|
|
332
|
-
board_name: board.name,
|
|
333
|
-
labels: tLabels,
|
|
334
|
-
comment_count: commentCount,
|
|
335
|
-
child_count: childCount,
|
|
336
|
-
parent_ticket: parent
|
|
337
|
-
? {
|
|
338
|
-
id: parent.id,
|
|
339
|
-
board_id: parent.board_id,
|
|
340
|
-
board_slug: board.slug,
|
|
341
|
-
number: parent.number,
|
|
342
|
-
title: parent.title,
|
|
343
|
-
type: parent.type,
|
|
344
|
-
state_name: parentState?.name || "",
|
|
345
|
-
state_role: parentState?.role || null,
|
|
346
|
-
priority: parent.priority
|
|
347
|
-
}
|
|
348
|
-
: null
|
|
349
|
-
};
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
354
|
-
// Mutation helpers
|
|
355
|
-
|
|
356
|
-
async function putAndEvent(db, mutate) {
|
|
357
|
-
const { stores, done } = tx(
|
|
358
|
-
db,
|
|
359
|
-
["boards", "states", "labels", "tickets", "ticket_labels", "comments", "relations", "board_entries", "events"],
|
|
360
|
-
"readwrite"
|
|
361
|
-
);
|
|
362
|
-
const result = await mutate(stores);
|
|
363
|
-
await done;
|
|
364
|
-
return result;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function recordEvent(stores, type, ticketId, body) {
|
|
368
|
-
const ev = {
|
|
369
|
-
id: uid(),
|
|
370
|
-
board_id: BOARD_ID,
|
|
371
|
-
ticket_id: ticketId || null,
|
|
372
|
-
type,
|
|
373
|
-
actor: "you",
|
|
374
|
-
body_json: JSON.stringify(body || {}),
|
|
375
|
-
created_at: nowIso()
|
|
376
|
-
};
|
|
377
|
-
stores.events.put(ev);
|
|
378
|
-
return ev;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async function ensureLabel(stores, name) {
|
|
382
|
-
const all = await getAll(stores.labels);
|
|
383
|
-
const existing = all.find((l) => l.name.toLowerCase() === String(name).toLowerCase());
|
|
384
|
-
if (existing) return existing;
|
|
385
|
-
const created = {
|
|
386
|
-
id: uid(),
|
|
387
|
-
board_id: BOARD_ID,
|
|
388
|
-
name: slugify(name),
|
|
389
|
-
color: "#64748b",
|
|
390
|
-
created_at: nowIso()
|
|
391
|
-
};
|
|
392
|
-
stores.labels.put(created);
|
|
393
|
-
return created;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
397
|
-
// Handlers
|
|
398
|
-
|
|
399
|
-
async function handleBootstrap() {
|
|
400
|
-
const db = await openDb();
|
|
401
|
-
await seedIfEmpty(db);
|
|
402
|
-
const snap = await readBoardSnapshot(db);
|
|
403
|
-
const tickets = decorateTickets(snap);
|
|
404
|
-
return {
|
|
405
|
-
actor: { name: "you", role: "owner" },
|
|
406
|
-
meta: { db_path: null, export_dir: null },
|
|
407
|
-
boards: [snap.board],
|
|
408
|
-
board_entries: [...snap.boardEntries].sort((a, b) =>
|
|
409
|
-
String(b.created_at).localeCompare(String(a.created_at))
|
|
410
|
-
),
|
|
411
|
-
states: [...snap.states].sort((a, b) => a.position - b.position),
|
|
412
|
-
labels: [...snap.labels].sort((a, b) => a.name.localeCompare(b.name)),
|
|
413
|
-
tickets
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function handleBoardsList() {
|
|
418
|
-
const db = await openDb();
|
|
419
|
-
await seedIfEmpty(db);
|
|
420
|
-
const { stores } = tx(db, ["boards"], "readonly");
|
|
421
|
-
const board = await reqPromise(stores.boards.get(BOARD_ID));
|
|
422
|
-
return {
|
|
423
|
-
boards: [
|
|
424
|
-
{
|
|
425
|
-
id: board.id,
|
|
426
|
-
slug: board.slug,
|
|
427
|
-
name: board.name,
|
|
428
|
-
repo_path: board.repo_path || "",
|
|
429
|
-
repo_url: board.repo_url || "",
|
|
430
|
-
default_branch: board.default_branch,
|
|
431
|
-
db_path: null,
|
|
432
|
-
last_active_at: board.updated_at
|
|
433
|
-
}
|
|
434
|
-
]
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async function handleBoardContext(boardId, includeStruck) {
|
|
439
|
-
if (boardId !== BOARD_ID) throw err(404, "board_not_found");
|
|
440
|
-
const db = await openDb();
|
|
441
|
-
await seedIfEmpty(db);
|
|
442
|
-
const snap = await readBoardSnapshot(db);
|
|
443
|
-
const entries = (includeStruck ? snap.boardEntries : snap.boardEntries.filter((e) => !e.struck_at)).sort(
|
|
444
|
-
(a, b) => String(b.created_at).localeCompare(String(a.created_at))
|
|
445
|
-
);
|
|
446
|
-
return {
|
|
447
|
-
board: snap.board,
|
|
448
|
-
entries,
|
|
449
|
-
deployment: { app_root: "browser", system_path: "", data_dir: "", mcp_server_path: "", db_path: null }
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function handleBoardArchive(boardId) {
|
|
454
|
-
if (boardId !== BOARD_ID) throw err(404, "board_not_found");
|
|
455
|
-
const db = await openDb();
|
|
456
|
-
const snap = await readBoardSnapshot(db);
|
|
457
|
-
const tickets = decorateTickets(snap, true).filter((t) => t.archived_at);
|
|
458
|
-
return { tickets };
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
async function handleBoardPatch(boardId, body) {
|
|
462
|
-
if (boardId !== BOARD_ID) throw err(404, "board_not_found");
|
|
463
|
-
const db = await openDb();
|
|
464
|
-
return putAndEvent(db, async (stores) => {
|
|
465
|
-
const board = await reqPromise(stores.boards.get(BOARD_ID));
|
|
466
|
-
const ALLOWED = [
|
|
467
|
-
"name",
|
|
468
|
-
"repo_url",
|
|
469
|
-
"system_path",
|
|
470
|
-
"default_branch",
|
|
471
|
-
"project_notes",
|
|
472
|
-
"agent_instructions",
|
|
473
|
-
"ai_enabled"
|
|
474
|
-
];
|
|
475
|
-
for (const k of ALLOWED) {
|
|
476
|
-
if (!(k in body)) continue;
|
|
477
|
-
if (k === "name") {
|
|
478
|
-
const name = String(body[k] || "").trim();
|
|
479
|
-
if (!name) throw err(400, "missing_name");
|
|
480
|
-
board[k] = name;
|
|
481
|
-
} else {
|
|
482
|
-
board[k] = body[k];
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
board.updated_at = nowIso();
|
|
486
|
-
stores.boards.put(board);
|
|
487
|
-
recordEvent(stores, "board_updated", null, { fields: Object.keys(body) });
|
|
488
|
-
return board;
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async function handleBoardEntries(boardId, body) {
|
|
493
|
-
if (boardId !== BOARD_ID) throw err(404, "board_not_found");
|
|
494
|
-
const db = await openDb();
|
|
495
|
-
return putAndEvent(db, async (stores) => {
|
|
496
|
-
const entry = {
|
|
497
|
-
id: uid(),
|
|
498
|
-
board_id: BOARD_ID,
|
|
499
|
-
type: body.type || "note",
|
|
500
|
-
title: body.title || "",
|
|
501
|
-
body: body.body || "",
|
|
502
|
-
ticket_id: body.ticket_id || null,
|
|
503
|
-
struck_at: null,
|
|
504
|
-
created_by: "you",
|
|
505
|
-
created_at: nowIso(),
|
|
506
|
-
updated_at: nowIso()
|
|
507
|
-
};
|
|
508
|
-
stores.board_entries.put(entry);
|
|
509
|
-
recordEvent(stores, "board_entry_created", entry.ticket_id, { entry_id: entry.id, type: entry.type });
|
|
510
|
-
return entry;
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
async function handleBoardEntryPatch(entryId, body) {
|
|
515
|
-
const db = await openDb();
|
|
516
|
-
return putAndEvent(db, async (stores) => {
|
|
517
|
-
const entry = await reqPromise(stores.board_entries.get(entryId));
|
|
518
|
-
if (!entry) throw err(404, "board_entry_not_found");
|
|
519
|
-
const shouldStrike = Boolean(body.struck);
|
|
520
|
-
entry.struck_at = shouldStrike ? entry.struck_at || nowIso() : null;
|
|
521
|
-
entry.updated_at = nowIso();
|
|
522
|
-
stores.board_entries.put(entry);
|
|
523
|
-
return entry;
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async function handleStateCreate(boardId, body) {
|
|
528
|
-
if (boardId !== BOARD_ID) throw err(404, "board_not_found");
|
|
529
|
-
const db = await openDb();
|
|
530
|
-
return putAndEvent(db, async (stores) => {
|
|
531
|
-
const states = await getAll(stores.states);
|
|
532
|
-
const maxPos = states.reduce((a, s) => Math.max(a, s.position), -1);
|
|
533
|
-
const state = {
|
|
534
|
-
id: uid(),
|
|
535
|
-
board_id: BOARD_ID,
|
|
536
|
-
name: body.name || "New",
|
|
537
|
-
position: maxPos + 1,
|
|
538
|
-
is_default: 0,
|
|
539
|
-
role: body.role || null,
|
|
540
|
-
created_at: nowIso()
|
|
541
|
-
};
|
|
542
|
-
stores.states.put(state);
|
|
543
|
-
recordEvent(stores, "state_created", null, { state_id: state.id });
|
|
544
|
-
return state;
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async function handleStatePatch(stateId, body) {
|
|
549
|
-
const db = await openDb();
|
|
550
|
-
return putAndEvent(db, async (stores) => {
|
|
551
|
-
const state = await reqPromise(stores.states.get(stateId));
|
|
552
|
-
if (!state) throw err(404, "state_not_found");
|
|
553
|
-
for (const k of ["name", "role"]) if (k in body) state[k] = body[k];
|
|
554
|
-
stores.states.put(state);
|
|
555
|
-
return state;
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async function handleStateDelete(stateId) {
|
|
560
|
-
const db = await openDb();
|
|
561
|
-
return putAndEvent(db, async (stores) => {
|
|
562
|
-
const state = await reqPromise(stores.states.get(stateId));
|
|
563
|
-
if (!state) throw err(404, "state_not_found");
|
|
564
|
-
const tickets = await getAll(stores.tickets);
|
|
565
|
-
if (tickets.some((t) => t.state_id === stateId && !t.archived_at)) {
|
|
566
|
-
throw err(409, "state_has_tickets");
|
|
567
|
-
}
|
|
568
|
-
stores.states.delete(stateId);
|
|
569
|
-
recordEvent(stores, "state_deleted", null, { state_id: stateId });
|
|
570
|
-
return { ok: true };
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
async function handleStateReorder(boardId, body) {
|
|
575
|
-
if (boardId !== BOARD_ID) throw err(404, "board_not_found");
|
|
576
|
-
const db = await openDb();
|
|
577
|
-
return putAndEvent(db, async (stores) => {
|
|
578
|
-
const order = body.order || body.state_ids || [];
|
|
579
|
-
for (let i = 0; i < order.length; i++) {
|
|
580
|
-
const state = await reqPromise(stores.states.get(order[i]));
|
|
581
|
-
if (!state) continue;
|
|
582
|
-
state.position = i;
|
|
583
|
-
stores.states.put(state);
|
|
584
|
-
}
|
|
585
|
-
recordEvent(stores, "states_reordered", null, { order });
|
|
586
|
-
const states = await getAll(stores.states);
|
|
587
|
-
return { states: states.sort((a, b) => a.position - b.position) };
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Mirrors src/core/tickets.js#bumpTicketUpdatedAt — rolls child activity up to
|
|
592
|
-
// the parent epic so the column sort (updated_at DESC) keeps the epic group
|
|
593
|
-
// near recent child activity even when only its children changed.
|
|
594
|
-
async function bumpTicketUpdatedAt(stores, ticketId, time) {
|
|
595
|
-
if (!ticketId) return;
|
|
596
|
-
const parent = await reqPromise(stores.tickets.get(ticketId));
|
|
597
|
-
if (!parent) return;
|
|
598
|
-
parent.updated_at = time;
|
|
599
|
-
stores.tickets.put(parent);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async function handleTicketCreate(body) {
|
|
603
|
-
const db = await openDb();
|
|
604
|
-
return putAndEvent(db, async (stores) => {
|
|
605
|
-
const states = await getAll(stores.states);
|
|
606
|
-
const stateId =
|
|
607
|
-
body.state_id ||
|
|
608
|
-
states.find((s) => s.is_default)?.id ||
|
|
609
|
-
states.sort((a, b) => a.position - b.position)[0]?.id;
|
|
610
|
-
if (!stateId) throw err(400, "no_states");
|
|
611
|
-
const tickets = await getAll(stores.tickets);
|
|
612
|
-
const nextNumber = tickets.reduce((a, t) => Math.max(a, t.number || 0), 0) + 1;
|
|
613
|
-
const ticket = {
|
|
614
|
-
id: uid(),
|
|
615
|
-
board_id: BOARD_ID,
|
|
616
|
-
number: nextNumber,
|
|
617
|
-
title: body.title || "Untitled",
|
|
618
|
-
description: body.description || "",
|
|
619
|
-
type: body.type || (body.parent_ticket_id ? "feature" : "task"),
|
|
620
|
-
parent_ticket_id: body.parent_ticket_id || null,
|
|
621
|
-
ai_plan: body.ai_plan || "",
|
|
622
|
-
plan_artifact_path: body.plan_artifact_path || "",
|
|
623
|
-
implementation_summary: body.implementation_summary || "",
|
|
624
|
-
implementation_artifact_path: body.implementation_artifact_path || "",
|
|
625
|
-
implementation_updates: body.implementation_updates || "",
|
|
626
|
-
state_id: stateId,
|
|
627
|
-
priority: body.priority ?? 0,
|
|
628
|
-
created_by: "you",
|
|
629
|
-
archived_at: null,
|
|
630
|
-
created_at: nowIso(),
|
|
631
|
-
updated_at: nowIso()
|
|
632
|
-
};
|
|
633
|
-
stores.tickets.put(ticket);
|
|
634
|
-
for (const name of body.labels || []) {
|
|
635
|
-
const label = await ensureLabel(stores, name);
|
|
636
|
-
stores.ticket_labels.put({ key: tlKey(ticket.id, label.id), ticket_id: ticket.id, label_id: label.id });
|
|
637
|
-
}
|
|
638
|
-
recordEvent(stores, "ticket_created", ticket.id, { title: ticket.title, type: ticket.type });
|
|
639
|
-
await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, ticket.updated_at);
|
|
640
|
-
return ticket;
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
async function handleTicketPatch(ticketId, body) {
|
|
645
|
-
const db = await openDb();
|
|
646
|
-
return putAndEvent(db, async (stores) => {
|
|
647
|
-
const ticket = await reqPromise(stores.tickets.get(ticketId));
|
|
648
|
-
if (!ticket) throw err(404, "ticket_not_found");
|
|
649
|
-
const oldStateId = ticket.state_id;
|
|
650
|
-
const oldParentId = ticket.parent_ticket_id;
|
|
651
|
-
const SIMPLE = [
|
|
652
|
-
"title",
|
|
653
|
-
"description",
|
|
654
|
-
"type",
|
|
655
|
-
"parent_ticket_id",
|
|
656
|
-
"ai_plan",
|
|
657
|
-
"plan_artifact_path",
|
|
658
|
-
"implementation_summary",
|
|
659
|
-
"implementation_artifact_path",
|
|
660
|
-
"implementation_updates",
|
|
661
|
-
"priority"
|
|
662
|
-
];
|
|
663
|
-
for (const k of SIMPLE) if (k in body) ticket[k] = body[k];
|
|
664
|
-
if ("state_id" in body) ticket.state_id = body.state_id;
|
|
665
|
-
ticket.updated_at = nowIso();
|
|
666
|
-
stores.tickets.put(ticket);
|
|
667
|
-
|
|
668
|
-
if ("labels" in body) {
|
|
669
|
-
const existing = await getAll(stores.ticket_labels, ticketId, "ticket_id");
|
|
670
|
-
for (const row of existing) stores.ticket_labels.delete(row.key);
|
|
671
|
-
for (const name of body.labels || []) {
|
|
672
|
-
const label = await ensureLabel(stores, name);
|
|
673
|
-
stores.ticket_labels.put({ key: tlKey(ticketId, label.id), ticket_id: ticketId, label_id: label.id });
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (oldStateId !== ticket.state_id) {
|
|
678
|
-
const states = await getAll(stores.states);
|
|
679
|
-
const fromState = states.find((s) => s.id === oldStateId);
|
|
680
|
-
const toState = states.find((s) => s.id === ticket.state_id);
|
|
681
|
-
// Mirror tickets.js: when a ticket lands in role='done', auto-delete
|
|
682
|
-
// every relation row that names it as a blocker.
|
|
683
|
-
if (toState?.role === "done" && fromState?.role !== "done") {
|
|
684
|
-
const relations = await getAll(stores.relations);
|
|
685
|
-
for (const r of relations) {
|
|
686
|
-
if ((r.source_ticket_id === ticketId && r.type === "blocks") ||
|
|
687
|
-
(r.target_ticket_id === ticketId && r.type === "blocked_by")) {
|
|
688
|
-
stores.relations.delete(r.id);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
recordEvent(stores, "state_changed", ticketId, { from: fromState?.name || "", to: toState?.name || "" });
|
|
693
|
-
} else {
|
|
694
|
-
recordEvent(stores, "ticket_updated", ticketId, { fields: Object.keys(body) });
|
|
695
|
-
}
|
|
696
|
-
await bumpTicketUpdatedAt(stores, oldParentId, ticket.updated_at);
|
|
697
|
-
if (ticket.parent_ticket_id && ticket.parent_ticket_id !== oldParentId) {
|
|
698
|
-
await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, ticket.updated_at);
|
|
699
|
-
}
|
|
700
|
-
return ticket;
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
async function handleTicketDelete(ticketId) {
|
|
705
|
-
const db = await openDb();
|
|
706
|
-
return putAndEvent(db, async (stores) => {
|
|
707
|
-
const ticket = await reqPromise(stores.tickets.get(ticketId));
|
|
708
|
-
if (!ticket) throw err(404, "ticket_not_found");
|
|
709
|
-
stores.tickets.delete(ticketId);
|
|
710
|
-
const tlinks = await getAll(stores.ticket_labels, ticketId, "ticket_id");
|
|
711
|
-
for (const row of tlinks) stores.ticket_labels.delete(row.key);
|
|
712
|
-
const cmts = await getAll(stores.comments, ticketId, "ticket_id");
|
|
713
|
-
for (const c of cmts) stores.comments.delete(c.id);
|
|
714
|
-
recordEvent(stores, "ticket_deleted", ticketId, {});
|
|
715
|
-
await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, nowIso());
|
|
716
|
-
return { ok: true };
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
async function handleTicketArchive(ticketId, archive) {
|
|
721
|
-
const db = await openDb();
|
|
722
|
-
return putAndEvent(db, async (stores) => {
|
|
723
|
-
const ticket = await reqPromise(stores.tickets.get(ticketId));
|
|
724
|
-
if (!ticket) throw err(404, "ticket_not_found");
|
|
725
|
-
ticket.archived_at = archive ? nowIso() : null;
|
|
726
|
-
ticket.updated_at = nowIso();
|
|
727
|
-
stores.tickets.put(ticket);
|
|
728
|
-
recordEvent(stores, archive ? "ticket_archived" : "ticket_restored", ticketId, {});
|
|
729
|
-
await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, ticket.updated_at);
|
|
730
|
-
return ticket;
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async function handleTicketContext(ticketId, { includeProjectContext = false } = {}) {
|
|
735
|
-
const db = await openDb();
|
|
736
|
-
const snap = await readBoardSnapshot(db);
|
|
737
|
-
const ticket = snap.tickets.find((t) => t.id === ticketId);
|
|
738
|
-
if (!ticket) throw err(404, "ticket_not_found");
|
|
739
|
-
const labels = snap.ticketLabels
|
|
740
|
-
.filter((tl) => tl.ticket_id === ticketId)
|
|
741
|
-
.map((tl) => snap.labels.find((l) => l.id === tl.label_id))
|
|
742
|
-
.filter(Boolean)
|
|
743
|
-
.map((l) => ({ ticket_id: ticketId, id: l.id, name: l.name, color: l.color }));
|
|
744
|
-
const stateById = Object.fromEntries(snap.states.map((s) => [s.id, s]));
|
|
745
|
-
const compact = (t) => {
|
|
746
|
-
if (!t) return null;
|
|
747
|
-
const s = stateById[t.state_id];
|
|
748
|
-
return {
|
|
749
|
-
id: t.id,
|
|
750
|
-
board_id: t.board_id,
|
|
751
|
-
board_slug: snap.board.slug,
|
|
752
|
-
number: t.number,
|
|
753
|
-
title: t.title,
|
|
754
|
-
type: t.type,
|
|
755
|
-
state_name: s?.name || "",
|
|
756
|
-
state_role: s?.role || null,
|
|
757
|
-
priority: t.priority
|
|
758
|
-
};
|
|
759
|
-
};
|
|
760
|
-
const parent = ticket.parent_ticket_id ? snap.tickets.find((t) => t.id === ticket.parent_ticket_id) : null;
|
|
761
|
-
const childTickets = snap.tickets
|
|
762
|
-
.filter((t) => t.parent_ticket_id === ticketId && !t.archived_at)
|
|
763
|
-
.map((child) => ({
|
|
764
|
-
...child,
|
|
765
|
-
labels: snap.ticketLabels
|
|
766
|
-
.filter((tl) => tl.ticket_id === child.id)
|
|
767
|
-
.map((tl) => snap.labels.find((l) => l.id === tl.label_id))
|
|
768
|
-
.filter(Boolean)
|
|
769
|
-
.map((l) => ({ ticket_id: child.id, id: l.id, name: l.name, color: l.color })),
|
|
770
|
-
blockers: [],
|
|
771
|
-
child_count: snap.tickets.filter((t) => t.parent_ticket_id === child.id && !t.archived_at).length
|
|
772
|
-
}));
|
|
773
|
-
const relations = snap.relations
|
|
774
|
-
.filter((r) => r.source_ticket_id === ticketId || r.target_ticket_id === ticketId)
|
|
775
|
-
.map((r) => {
|
|
776
|
-
const otherId = r.source_ticket_id === ticketId ? r.target_ticket_id : r.source_ticket_id;
|
|
777
|
-
const other = snap.tickets.find((t) => t.id === otherId);
|
|
778
|
-
return {
|
|
779
|
-
...r,
|
|
780
|
-
direction: r.source_ticket_id === ticketId ? "outgoing" : "incoming",
|
|
781
|
-
other_ticket: compact(other)
|
|
782
|
-
};
|
|
783
|
-
});
|
|
784
|
-
const context = {
|
|
785
|
-
ticket: { ...ticket, labels },
|
|
786
|
-
board: includeProjectContext
|
|
787
|
-
? snap.board
|
|
788
|
-
: {
|
|
789
|
-
id: snap.board.id,
|
|
790
|
-
slug: snap.board.slug,
|
|
791
|
-
name: snap.board.name,
|
|
792
|
-
repo_path: snap.board.system_path || "",
|
|
793
|
-
system_path: snap.board.system_path || "",
|
|
794
|
-
default_branch: snap.board.default_branch || "main"
|
|
795
|
-
},
|
|
796
|
-
parent_ticket: compact(parent),
|
|
797
|
-
child_tickets: childTickets,
|
|
798
|
-
relations,
|
|
799
|
-
blockers: [],
|
|
800
|
-
related_tickets: [...childTickets, ...(parent ? [parent] : [])]
|
|
801
|
-
};
|
|
802
|
-
if (includeProjectContext) context.board_manual = { entries: snap.boardEntries.filter((e) => !e.struck_at) };
|
|
803
|
-
return context;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
async function handleTicketComments(ticketId) {
|
|
807
|
-
const snap = await snapshotAll();
|
|
808
|
-
const ticket = snap.tickets.find((t) => t.id === ticketId && !t.archived_at);
|
|
809
|
-
if (!ticket) throw err(404, "ticket_not_found");
|
|
810
|
-
return {
|
|
811
|
-
ticket_id: ticketId,
|
|
812
|
-
comments: snap.comments
|
|
813
|
-
.filter((c) => c.ticket_id === ticketId)
|
|
814
|
-
.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)))
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
async function handleTicketHistory(ticketId) {
|
|
819
|
-
const db = await openDb();
|
|
820
|
-
const { stores } = tx(db, ["events"], "readonly");
|
|
821
|
-
const events = await getAll(stores.events, ticketId, "ticket_id");
|
|
822
|
-
return events
|
|
823
|
-
.filter((e) => ["state_changed", "checkpoint_requested", "agent_completed"].includes(e.type))
|
|
824
|
-
.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)))
|
|
825
|
-
.map((e) => ({ ...e, body: safeJson(e.body_json) }));
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function safeJson(text) {
|
|
829
|
-
try {
|
|
830
|
-
return JSON.parse(text || "{}");
|
|
831
|
-
} catch {
|
|
832
|
-
return {};
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
async function handleCommentCreate(ticketId, body) {
|
|
837
|
-
const db = await openDb();
|
|
838
|
-
return putAndEvent(db, async (stores) => {
|
|
839
|
-
const ticket = await reqPromise(stores.tickets.get(ticketId));
|
|
840
|
-
if (!ticket) throw err(404, "ticket_not_found");
|
|
841
|
-
const time = nowIso();
|
|
842
|
-
const comment = {
|
|
843
|
-
id: uid(),
|
|
844
|
-
ticket_id: ticketId,
|
|
845
|
-
author: "you",
|
|
846
|
-
kind: body.kind || "note",
|
|
847
|
-
body: body.body || "",
|
|
848
|
-
omitted_for_ai: 0,
|
|
849
|
-
created_at: time
|
|
850
|
-
};
|
|
851
|
-
stores.comments.put(comment);
|
|
852
|
-
ticket.updated_at = time;
|
|
853
|
-
stores.tickets.put(ticket);
|
|
854
|
-
recordEvent(stores, "comment_created", ticketId, { kind: comment.kind });
|
|
855
|
-
await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, time);
|
|
856
|
-
return comment;
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
async function handleCommentPatch(ticketId, commentId, body) {
|
|
861
|
-
const db = await openDb();
|
|
862
|
-
return putAndEvent(db, async (stores) => {
|
|
863
|
-
const ticket = await reqPromise(stores.tickets.get(ticketId));
|
|
864
|
-
if (!ticket) throw err(404, "ticket_not_found");
|
|
865
|
-
const comment = await reqPromise(stores.comments.get(commentId));
|
|
866
|
-
if (!comment || comment.ticket_id !== ticketId) throw err(404, "comment_not_found");
|
|
867
|
-
const omittedForAi = body.omitted_for_ai ?? body.omittedForAi;
|
|
868
|
-
if (typeof omittedForAi !== "boolean") throw err(400, "omitted_for_ai_required");
|
|
869
|
-
const time = nowIso();
|
|
870
|
-
comment.omitted_for_ai = omittedForAi ? 1 : 0;
|
|
871
|
-
ticket.updated_at = time;
|
|
872
|
-
stores.comments.put(comment);
|
|
873
|
-
stores.tickets.put(ticket);
|
|
874
|
-
recordEvent(stores, "comment_ai_omission_changed", ticketId, { comment_id: commentId, omitted_for_ai: omittedForAi });
|
|
875
|
-
await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, time);
|
|
876
|
-
return comment;
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
async function handleRelationCreate(body) {
|
|
881
|
-
const db = await openDb();
|
|
882
|
-
return putAndEvent(db, async (stores) => {
|
|
883
|
-
const relation = {
|
|
884
|
-
id: uid(),
|
|
885
|
-
board_id: BOARD_ID,
|
|
886
|
-
source_ticket_id: body.source_ticket_id,
|
|
887
|
-
target_ticket_id: body.target_ticket_id,
|
|
888
|
-
kind: body.kind || "related",
|
|
889
|
-
created_by: "you",
|
|
890
|
-
created_at: nowIso()
|
|
891
|
-
};
|
|
892
|
-
stores.relations.put(relation);
|
|
893
|
-
return relation;
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
async function handleRelationDelete(relationId) {
|
|
898
|
-
const db = await openDb();
|
|
899
|
-
return putAndEvent(db, async (stores) => {
|
|
900
|
-
stores.relations.delete(relationId);
|
|
901
|
-
return { ok: true };
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
async function handleSearch(query) {
|
|
906
|
-
const db = await openDb();
|
|
907
|
-
const snap = await readBoardSnapshot(db);
|
|
908
|
-
const q = String(query || "").toLowerCase().trim();
|
|
909
|
-
if (!q) return { tickets: [] };
|
|
910
|
-
const tickets = decorateTickets(snap).filter((t) => {
|
|
911
|
-
const haystack = [t.title, t.description, t.ai_plan, t.implementation_summary, `#${t.number}`]
|
|
912
|
-
.join(" ")
|
|
913
|
-
.toLowerCase();
|
|
914
|
-
return haystack.includes(q);
|
|
915
|
-
});
|
|
916
|
-
return { tickets: tickets.slice(0, 50) };
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
async function handleExport(boardId) {
|
|
920
|
-
if (boardId !== BOARD_ID) throw err(404, "board_not_found");
|
|
921
|
-
const db = await openDb();
|
|
922
|
-
const snap = await readBoardSnapshot(db);
|
|
923
|
-
return {
|
|
924
|
-
schema: "orbit.board.snapshot/v1",
|
|
925
|
-
exported_at: nowIso(),
|
|
926
|
-
board: snap.board,
|
|
927
|
-
states: snap.states,
|
|
928
|
-
labels: snap.labels,
|
|
929
|
-
tickets: snap.tickets,
|
|
930
|
-
ticket_labels: snap.ticketLabels,
|
|
931
|
-
comments: snap.comments,
|
|
932
|
-
relations: snap.relations,
|
|
933
|
-
board_entries: snap.boardEntries
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
async function handleImport(body) {
|
|
938
|
-
const snapshot = body.snapshot || body;
|
|
939
|
-
if (!snapshot || !snapshot.board) throw err(400, "invalid_snapshot");
|
|
940
|
-
const db = await openDb();
|
|
941
|
-
return putAndEvent(db, async (stores) => {
|
|
942
|
-
// Replace within this single browser-board: clear stores then load.
|
|
943
|
-
for (const name of [
|
|
944
|
-
"states",
|
|
945
|
-
"labels",
|
|
946
|
-
"tickets",
|
|
947
|
-
"ticket_labels",
|
|
948
|
-
"comments",
|
|
949
|
-
"relations",
|
|
950
|
-
"board_entries"
|
|
951
|
-
]) {
|
|
952
|
-
const all = await getAll(stores[name]);
|
|
953
|
-
for (const row of all) stores[name].delete(row.key ?? row.id);
|
|
954
|
-
}
|
|
955
|
-
const incomingBoard = { ...snapshot.board, id: BOARD_ID };
|
|
956
|
-
stores.boards.put(incomingBoard);
|
|
957
|
-
(snapshot.states || []).forEach((s) => stores.states.put({ ...s, board_id: BOARD_ID }));
|
|
958
|
-
(snapshot.labels || []).forEach((l) => stores.labels.put({ ...l, board_id: BOARD_ID }));
|
|
959
|
-
(snapshot.tickets || []).forEach((t) => stores.tickets.put({ ...t, board_id: BOARD_ID }));
|
|
960
|
-
(snapshot.ticket_labels || []).forEach((tl) =>
|
|
961
|
-
stores.ticket_labels.put({ ...tl, key: tl.key || tlKey(tl.ticket_id, tl.label_id) })
|
|
962
|
-
);
|
|
963
|
-
(snapshot.comments || []).forEach((c) => stores.comments.put(c));
|
|
964
|
-
(snapshot.relations || []).forEach((r) => stores.relations.put({ ...r, board_id: BOARD_ID }));
|
|
965
|
-
(snapshot.board_entries || []).forEach((e) => stores.board_entries.put({ ...e, board_id: BOARD_ID }));
|
|
966
|
-
recordEvent(stores, "board_imported", null, { source: snapshot.board?.slug });
|
|
967
|
-
return { imported: true };
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
972
|
-
// Dispatch — match transport.js's contract: request({method, path, body})
|
|
973
|
-
|
|
974
|
-
const ROUTES = [
|
|
975
|
-
{ m: "GET", re: /^\/api\/bootstrap$/, fn: () => handleBootstrap() },
|
|
976
|
-
{ m: "GET", re: /^\/api\/boards$/, fn: () => handleBoardsList() },
|
|
977
|
-
{ m: "POST", re: /^\/api\/boards$/, fn: () => { throw err(403, "preview_is_single_board"); } },
|
|
978
|
-
{ m: "GET", re: /^\/api\/boards\/([^/]+)\/context$/, fn: (m, u) => handleBoardContext(decodeURIComponent(m[1]), u.searchParams.get("include_struck") === "true") },
|
|
979
|
-
{ m: "GET", re: /^\/api\/boards\/([^/]+)\/archive$/, fn: (m) => handleBoardArchive(decodeURIComponent(m[1])) },
|
|
980
|
-
{ m: "GET", re: /^\/api\/boards\/([^/]+)\/export$/, fn: (m) => handleExport(decodeURIComponent(m[1])) },
|
|
981
|
-
{ m: "PATCH", re: /^\/api\/boards\/([^/]+)$/, fn: (m, u, body) => handleBoardPatch(decodeURIComponent(m[1]), body) },
|
|
982
|
-
{ m: "POST", re: /^\/api\/boards\/([^/]+)\/entries$/, fn: (m, u, body) => handleBoardEntries(decodeURIComponent(m[1]), body) },
|
|
983
|
-
{ m: "POST", re: /^\/api\/boards\/([^/]+)\/states$/, fn: (m, u, body) => handleStateCreate(decodeURIComponent(m[1]), body) },
|
|
984
|
-
{ m: "PATCH", re: /^\/api\/boards\/([^/]+)\/states$/, fn: (m, u, body) => handleStateReorder(decodeURIComponent(m[1]), body) },
|
|
985
|
-
{ m: "PATCH", re: /^\/api\/board-entries\/([^/]+)$/, fn: (m, u, body) => handleBoardEntryPatch(decodeURIComponent(m[1]), body) },
|
|
986
|
-
{ m: "POST", re: /^\/api\/states$/, fn: (m, u, body) => handleStateCreate(BOARD_ID, body) },
|
|
987
|
-
{ m: "PATCH", re: /^\/api\/states\/([^/]+)$/, fn: (m, u, body) => handleStatePatch(decodeURIComponent(m[1]), body) },
|
|
988
|
-
{ m: "DELETE", re: /^\/api\/states\/([^/]+)$/, fn: (m) => handleStateDelete(decodeURIComponent(m[1])) },
|
|
989
|
-
{ m: "POST", re: /^\/api\/tickets$/, fn: (m, u, body) => handleTicketCreate(body) },
|
|
990
|
-
{ m: "PATCH", re: /^\/api\/tickets\/([^/]+)$/, fn: (m, u, body) => handleTicketPatch(decodeURIComponent(m[1]), body) },
|
|
991
|
-
{ m: "DELETE", re: /^\/api\/tickets\/([^/]+)$/, fn: (m) => handleTicketDelete(decodeURIComponent(m[1])) },
|
|
992
|
-
{ m: "POST", re: /^\/api\/tickets\/([^/]+)\/archive$/, fn: (m) => handleTicketArchive(decodeURIComponent(m[1]), true) },
|
|
993
|
-
{ m: "POST", re: /^\/api\/tickets\/([^/]+)\/restore$/, fn: (m) => handleTicketArchive(decodeURIComponent(m[1]), false) },
|
|
994
|
-
{ m: "GET", re: /^\/api\/tickets\/([^/]+)\/context\/full$/, fn: (m) => handleTicketContext(decodeURIComponent(m[1]), { includeProjectContext: true }) },
|
|
995
|
-
{ m: "GET", re: /^\/api\/tickets\/([^/]+)\/context$/, fn: (m) => handleTicketContext(decodeURIComponent(m[1])) },
|
|
996
|
-
{ m: "GET", re: /^\/api\/tickets\/([^/]+)\/comments$/, fn: (m) => handleTicketComments(decodeURIComponent(m[1])) },
|
|
997
|
-
{ m: "GET", re: /^\/api\/tickets\/([^/]+)\/history$/, fn: (m) => handleTicketHistory(decodeURIComponent(m[1])) },
|
|
998
|
-
{ m: "POST", re: /^\/api\/tickets\/([^/]+)\/comments$/, fn: (m, u, body) => handleCommentCreate(decodeURIComponent(m[1]), body) },
|
|
999
|
-
{ m: "PATCH", re: /^\/api\/tickets\/([^/]+)\/comments\/([^/]+)$/, fn: (m, u, body) => handleCommentPatch(decodeURIComponent(m[1]), decodeURIComponent(m[2]), body) },
|
|
1000
|
-
{ m: "POST", re: /^\/api\/relations$/, fn: (m, u, body) => handleRelationCreate(body) },
|
|
1001
|
-
{ m: "DELETE", re: /^\/api\/relations\/([^/]+)$/, fn: (m) => handleRelationDelete(decodeURIComponent(m[1])) },
|
|
1002
|
-
{ m: "GET", re: /^\/api\/search$/, fn: (m, u) => handleSearch(u.searchParams.get("q")) },
|
|
1003
|
-
{ m: "POST", re: /^\/api\/admin\/import$/, fn: (m, u, body) => handleImport(body) }
|
|
1004
|
-
];
|
|
1005
|
-
|
|
1006
|
-
export async function request({ method = "GET", path, body }) {
|
|
1007
|
-
const url = new URL(path, "http://local");
|
|
1008
|
-
for (const route of ROUTES) {
|
|
1009
|
-
if (route.m !== method) continue;
|
|
1010
|
-
const m = url.pathname.match(route.re);
|
|
1011
|
-
if (!m) continue;
|
|
1012
|
-
return route.fn(m, url, body || {});
|
|
1013
|
-
}
|
|
1014
|
-
throw err(404, "not_found");
|
|
1015
|
-
}
|