@rsktash/beads-ui 0.1.51 → 0.10.3
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/README.md +85 -144
- package/bin/bd-web +94 -0
- package/dist/assets/index-BBVIMXaM.js +73 -0
- package/dist/assets/index-SUXI6Mzt.css +1 -0
- package/dist/bd-favicon-16.svg +6 -0
- package/dist/bd-favicon-32.svg +6 -0
- package/dist/bd-logo-512.svg +6 -0
- package/dist/favicon.ico +0 -0
- package/dist/icon-180.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +8 -5
- package/package.json +39 -29
- package/server/auth.js +68 -87
- package/server/db.js +91 -134
- package/server/dsn.js +130 -0
- package/server/index.js +151 -87
- package/server/queries.js +247 -0
- package/server/routes/auth.js +27 -0
- package/server/routes/issues.js +120 -0
- package/server/routes/stream.js +111 -0
- package/server/types.js +83 -0
- package/app/protocol.js +0 -216
- package/bin/bd-grep +0 -121
- package/bin/bd-ui +0 -19
- package/dist/assets/abap-DsBKuouk.js +0 -1
- package/dist/assets/actionscript-3-D_z4Izcz.js +0 -1
- package/dist/assets/ada-727ZlQH0.js +0 -1
- package/dist/assets/andromeeda-C3khCPGq.js +0 -1
- package/dist/assets/angular-html-LfdN0zeE.js +0 -1
- package/dist/assets/angular-ts-CKsD7JZE.js +0 -1
- package/dist/assets/apache-Dn00JSTd.js +0 -1
- package/dist/assets/apex-COJ4H7py.js +0 -1
- package/dist/assets/apl-BBq3IX1j.js +0 -1
- package/dist/assets/applescript-Bu5BbsvL.js +0 -1
- package/dist/assets/ara-7O62HKoU.js +0 -1
- package/dist/assets/asciidoc-BPT9niGB.js +0 -1
- package/dist/assets/asm-Dhn9LcZ4.js +0 -1
- package/dist/assets/astro-CqkE3fuf.js +0 -1
- package/dist/assets/aurora-x-D-2ljcwZ.js +0 -1
- package/dist/assets/awk-eg146-Ew.js +0 -1
- package/dist/assets/ayu-dark-Cv9koXgw.js +0 -1
- package/dist/assets/ballerina-Du268qiB.js +0 -1
- package/dist/assets/bat-fje9CFhw.js +0 -1
- package/dist/assets/beancount-BwXTMy5W.js +0 -1
- package/dist/assets/berry-3xVqZejG.js +0 -1
- package/dist/assets/bibtex-xW4inM5L.js +0 -1
- package/dist/assets/bicep-DHo0CJ0O.js +0 -1
- package/dist/assets/blade-a8OxSdnT.js +0 -1
- package/dist/assets/bsl-Dgyn0ogV.js +0 -1
- package/dist/assets/c-C3t2pwGQ.js +0 -1
- package/dist/assets/cadence-DNquZEk8.js +0 -1
- package/dist/assets/cairo--RitsXJZ.js +0 -1
- package/dist/assets/catppuccin-frappe-CD_QflpE.js +0 -1
- package/dist/assets/catppuccin-latte-DRW-0cLl.js +0 -1
- package/dist/assets/catppuccin-macchiato-C-_shW-Y.js +0 -1
- package/dist/assets/catppuccin-mocha-LGGdnPYs.js +0 -1
- package/dist/assets/clarity-BHOwM8T6.js +0 -1
- package/dist/assets/clojure-DxSadP1t.js +0 -1
- package/dist/assets/cmake-DbXoA79R.js +0 -1
- package/dist/assets/cobol-PTqiYgYu.js +0 -1
- package/dist/assets/codeowners-Bp6g37R7.js +0 -1
- package/dist/assets/codeql-sacFqUAJ.js +0 -1
- package/dist/assets/coffee-dyiR41kL.js +0 -1
- package/dist/assets/common-lisp-C7gG9l05.js +0 -1
- package/dist/assets/coq-Dsg_Bt_b.js +0 -1
- package/dist/assets/cpp-BksuvNSY.js +0 -1
- package/dist/assets/crystal-DtDmRg-F.js +0 -1
- package/dist/assets/csharp-D9R-vmeu.js +0 -1
- package/dist/assets/css-BPhBrDlE.js +0 -1
- package/dist/assets/csv-B0qRVHPH.js +0 -1
- package/dist/assets/cue-DtFQj3wx.js +0 -1
- package/dist/assets/cypher-m2LEI-9-.js +0 -1
- package/dist/assets/d-BoXegm-a.js +0 -1
- package/dist/assets/dark-plus-C3mMm8J8.js +0 -1
- package/dist/assets/dart-B9wLZaAG.js +0 -1
- package/dist/assets/dax-ClGRhx96.js +0 -1
- package/dist/assets/desktop-DEIpsLCJ.js +0 -1
- package/dist/assets/diff-BgYniUM_.js +0 -1
- package/dist/assets/docker-COcR7UxN.js +0 -1
- package/dist/assets/dotenv-BjQB5zDj.js +0 -1
- package/dist/assets/dracula-BzJJZx-M.js +0 -1
- package/dist/assets/dracula-soft-BXkSAIEj.js +0 -1
- package/dist/assets/dream-maker-C-nORZOA.js +0 -1
- package/dist/assets/edge-D5gP-w-T.js +0 -1
- package/dist/assets/elixir-CLiX3zqd.js +0 -1
- package/dist/assets/elm-CmHSxxaM.js +0 -1
- package/dist/assets/emacs-lisp-BX77sIaO.js +0 -1
- package/dist/assets/erb-BYTLMnw6.js +0 -1
- package/dist/assets/erlang-B-DoSBHF.js +0 -1
- package/dist/assets/everforest-dark-BgDCqdQA.js +0 -1
- package/dist/assets/everforest-light-C8M2exoo.js +0 -1
- package/dist/assets/fennel-bCA53EVm.js +0 -1
- package/dist/assets/fish-w-ucz2PV.js +0 -1
- package/dist/assets/fluent-Dayu4EKP.js +0 -1
- package/dist/assets/fortran-fixed-form-TqA4NnZg.js +0 -1
- package/dist/assets/fortran-free-form-DKXYxT9g.js +0 -1
- package/dist/assets/fsharp-XplgxFYe.js +0 -1
- package/dist/assets/gdresource-BHYsBjWJ.js +0 -1
- package/dist/assets/gdscript-DfxzS6Rs.js +0 -1
- package/dist/assets/gdshader-SKMF96pI.js +0 -1
- package/dist/assets/genie-ajMbGru0.js +0 -1
- package/dist/assets/gherkin--30QC5Em.js +0 -1
- package/dist/assets/git-commit-i4q6IMui.js +0 -1
- package/dist/assets/git-rebase-B-v9cOL2.js +0 -1
- package/dist/assets/github-dark-DHJKELXO.js +0 -1
- package/dist/assets/github-dark-default-Cuk6v7N8.js +0 -1
- package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +0 -1
- package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +0 -1
- package/dist/assets/github-light-DAi9KRSo.js +0 -1
- package/dist/assets/github-light-default-D7oLnXFd.js +0 -1
- package/dist/assets/github-light-high-contrast-BfjtVDDH.js +0 -1
- package/dist/assets/gleam-B430Bg39.js +0 -1
- package/dist/assets/glimmer-js-D-cwc0-E.js +0 -1
- package/dist/assets/glimmer-ts-pgjy16dm.js +0 -1
- package/dist/assets/glsl-DBO2IWDn.js +0 -1
- package/dist/assets/gnuplot-CM8KxXT1.js +0 -1
- package/dist/assets/go-B1SYOhNW.js +0 -1
- package/dist/assets/graphql-cDcHW_If.js +0 -1
- package/dist/assets/groovy-DkBy-JyN.js +0 -1
- package/dist/assets/hack-D1yCygmZ.js +0 -1
- package/dist/assets/haml-B2EZWmdv.js +0 -1
- package/dist/assets/handlebars-BQGss363.js +0 -1
- package/dist/assets/haskell-BILxekzW.js +0 -1
- package/dist/assets/haxe-C5wWYbrZ.js +0 -1
- package/dist/assets/hcl-HzYwdGDm.js +0 -1
- package/dist/assets/hjson-T-Tgc4AT.js +0 -1
- package/dist/assets/hlsl-ifBTmRxC.js +0 -1
- package/dist/assets/houston-DnULxvSX.js +0 -1
- package/dist/assets/html-C2L_23MC.js +0 -1
- package/dist/assets/html-derivative-CSfWNPLT.js +0 -1
- package/dist/assets/http-FRrOvY1W.js +0 -1
- package/dist/assets/hxml-TIA70rKU.js +0 -1
- package/dist/assets/hy-BMj5Y0dO.js +0 -1
- package/dist/assets/imba-bv_oIlVt.js +0 -1
- package/dist/assets/index-CNDeKQGk.js +0 -125
- package/dist/assets/index-oO5WB2l2.css +0 -1
- package/dist/assets/ini-BjABl1g7.js +0 -1
- package/dist/assets/java-xI-RfyKK.js +0 -1
- package/dist/assets/javascript-ySlJ1b_l.js +0 -1
- package/dist/assets/jinja-DGy0s7-h.js +0 -1
- package/dist/assets/jison-BqZprYcd.js +0 -1
- package/dist/assets/json-BQoSv7ci.js +0 -1
- package/dist/assets/json5-w8dY5SsB.js +0 -1
- package/dist/assets/jsonc-TU54ms6u.js +0 -1
- package/dist/assets/jsonl-DREVFZK8.js +0 -1
- package/dist/assets/jsonnet-BfivnA6A.js +0 -1
- package/dist/assets/jssm-P4WzXJd0.js +0 -1
- package/dist/assets/jsx-BAng5TT0.js +0 -1
- package/dist/assets/julia-BBuGR-5E.js +0 -1
- package/dist/assets/kanagawa-dragon-CkXjmgJE.js +0 -1
- package/dist/assets/kanagawa-lotus-CfQXZHmo.js +0 -1
- package/dist/assets/kanagawa-wave-DWedfzmr.js +0 -1
- package/dist/assets/kotlin-B5lbUyaz.js +0 -1
- package/dist/assets/kusto-mebxcVVE.js +0 -1
- package/dist/assets/laserwave-DUszq2jm.js +0 -1
- package/dist/assets/latex-C-cWTeAZ.js +0 -1
- package/dist/assets/lean-XBlWyCtg.js +0 -1
- package/dist/assets/less-BfCpw3nA.js +0 -1
- package/dist/assets/light-plus-B7mTdjB0.js +0 -1
- package/dist/assets/liquid-D3W5UaiH.js +0 -1
- package/dist/assets/log-Cc5clBb7.js +0 -1
- package/dist/assets/logo-IuBKFhSY.js +0 -1
- package/dist/assets/lua-CvWAzNxB.js +0 -1
- package/dist/assets/luau-Du5NY7AG.js +0 -1
- package/dist/assets/make-Bvotw-X0.js +0 -1
- package/dist/assets/markdown-UIAJJxZW.js +0 -1
- package/dist/assets/marko-z0MBrx5-.js +0 -1
- package/dist/assets/material-theme-D5KoaKCx.js +0 -1
- package/dist/assets/material-theme-darker-BfHTSMKl.js +0 -1
- package/dist/assets/material-theme-lighter-B0m2ddpp.js +0 -1
- package/dist/assets/material-theme-ocean-CyktbL80.js +0 -1
- package/dist/assets/material-theme-palenight-Csfq5Kiy.js +0 -1
- package/dist/assets/matlab-D9-PGadD.js +0 -1
- package/dist/assets/mdc-DB_EDNY_.js +0 -1
- package/dist/assets/mdx-sdHcTMYB.js +0 -1
- package/dist/assets/mermaid-Ci6OQyBP.js +0 -1
- package/dist/assets/min-dark-CafNBF8u.js +0 -1
- package/dist/assets/min-light-CTRr51gU.js +0 -1
- package/dist/assets/mipsasm-BC5c_5Pe.js +0 -1
- package/dist/assets/mojo-Tz6hzZYG.js +0 -1
- package/dist/assets/monokai-D4h5O-jR.js +0 -1
- package/dist/assets/move-DB_GagMm.js +0 -1
- package/dist/assets/narrat-DLbgOhZU.js +0 -1
- package/dist/assets/nextflow-B0XVJmRM.js +0 -1
- package/dist/assets/nginx-D_VnBJ67.js +0 -1
- package/dist/assets/night-owl-C39BiMTA.js +0 -1
- package/dist/assets/nim-ZlGxZxc3.js +0 -1
- package/dist/assets/nix-shcSOmrb.js +0 -1
- package/dist/assets/nord-Ddv68eIx.js +0 -1
- package/dist/assets/nushell-D4Tzg5kh.js +0 -1
- package/dist/assets/objective-c-Deuh7S70.js +0 -1
- package/dist/assets/objective-cpp-BUEGK8hf.js +0 -1
- package/dist/assets/ocaml-BNioltXt.js +0 -1
- package/dist/assets/one-dark-pro-GBQ2dnAY.js +0 -1
- package/dist/assets/one-light-PoHY5YXO.js +0 -1
- package/dist/assets/pascal-JqZropPD.js +0 -1
- package/dist/assets/perl-CHQXSrWU.js +0 -1
- package/dist/assets/php-B5ebYQev.js +0 -1
- package/dist/assets/plastic-3e1v2bzS.js +0 -1
- package/dist/assets/plsql-LKU2TuZ1.js +0 -1
- package/dist/assets/po-BFLt1xDp.js +0 -1
- package/dist/assets/poimandres-CS3Unz2-.js +0 -1
- package/dist/assets/polar-DKykz6zU.js +0 -1
- package/dist/assets/postcss-B3ZDOciz.js +0 -1
- package/dist/assets/powerquery-CSHBycmS.js +0 -1
- package/dist/assets/powershell-BIEUsx6d.js +0 -1
- package/dist/assets/prisma-B48N-Iqd.js +0 -1
- package/dist/assets/prolog-BY-TUvya.js +0 -1
- package/dist/assets/proto-zocC4JxJ.js +0 -1
- package/dist/assets/pug-CM9l7STV.js +0 -1
- package/dist/assets/puppet-Cza_XSSt.js +0 -1
- package/dist/assets/purescript-Bg-kzb6g.js +0 -1
- package/dist/assets/python-DhUJRlN_.js +0 -1
- package/dist/assets/qml-D8XfuvdV.js +0 -1
- package/dist/assets/qmldir-C8lEn-DE.js +0 -1
- package/dist/assets/qss-DhMKtDLN.js +0 -1
- package/dist/assets/r-CwjWoCRV.js +0 -1
- package/dist/assets/racket-CzouJOBO.js +0 -1
- package/dist/assets/raku-B1bQXN8T.js +0 -1
- package/dist/assets/razor-CNLDkMZG.js +0 -1
- package/dist/assets/red-bN70gL4F.js +0 -1
- package/dist/assets/reg-5LuOXUq_.js +0 -1
- package/dist/assets/regexp-DWJ3fJO_.js +0 -1
- package/dist/assets/rel-DJlmqQ1C.js +0 -1
- package/dist/assets/riscv-QhoSD0DR.js +0 -1
- package/dist/assets/rose-pine-CmCqftbK.js +0 -1
- package/dist/assets/rose-pine-dawn-Ds-gbosJ.js +0 -1
- package/dist/assets/rose-pine-moon-CjDtw9vr.js +0 -1
- package/dist/assets/rst-4NLicBqY.js +0 -1
- package/dist/assets/ruby-DeZ3UC14.js +0 -1
- package/dist/assets/rust-Be6lgOlo.js +0 -1
- package/dist/assets/sas-BmTFh92c.js +0 -1
- package/dist/assets/sass-BJ4Li9vH.js +0 -1
- package/dist/assets/scala-DQVVAn-B.js +0 -1
- package/dist/assets/scheme-BJGe-b2p.js +0 -1
- package/dist/assets/scss-C31hgJw-.js +0 -1
- package/dist/assets/sdbl-BLhTXw86.js +0 -1
- package/dist/assets/shaderlab-B7qAK45m.js +0 -1
- package/dist/assets/shellscript-atvbtKCR.js +0 -1
- package/dist/assets/shellsession-C_rIy8kc.js +0 -1
- package/dist/assets/slack-dark-BthQWCQV.js +0 -1
- package/dist/assets/slack-ochin-DqwNpetd.js +0 -1
- package/dist/assets/smalltalk-DkLiglaE.js +0 -1
- package/dist/assets/snazzy-light-Bw305WKR.js +0 -1
- package/dist/assets/solarized-dark-DXbdFlpD.js +0 -1
- package/dist/assets/solarized-light-L9t79GZl.js +0 -1
- package/dist/assets/solidity-C1w2a3ep.js +0 -1
- package/dist/assets/soy-C-lX7w71.js +0 -1
- package/dist/assets/sparql-bYkjHRlG.js +0 -1
- package/dist/assets/splunk-Cf8iN4DR.js +0 -1
- package/dist/assets/sql-COK4E0Yg.js +0 -1
- package/dist/assets/ssh-config-BknIz3MU.js +0 -1
- package/dist/assets/stata-DorPZHa4.js +0 -1
- package/dist/assets/stylus-BeQkCIfX.js +0 -1
- package/dist/assets/svelte-MSaWC3Je.js +0 -1
- package/dist/assets/swift-BSxZ-RaX.js +0 -1
- package/dist/assets/synthwave-84-CbfX1IO0.js +0 -1
- package/dist/assets/system-verilog-C7L56vO4.js +0 -1
- package/dist/assets/systemd-CUnW07Te.js +0 -1
- package/dist/assets/talonscript-C1XDQQGZ.js +0 -1
- package/dist/assets/tasl-CQjiPCtT.js +0 -1
- package/dist/assets/tcl-DQ1-QYvQ.js +0 -1
- package/dist/assets/templ-dwX3ZSMB.js +0 -1
- package/dist/assets/terraform-BbSNqyBO.js +0 -1
- package/dist/assets/tex-rYs2v40G.js +0 -1
- package/dist/assets/tokyo-night-DBQeEorK.js +0 -1
- package/dist/assets/toml-CB2ApiWb.js +0 -1
- package/dist/assets/ts-tags-CipyTH0X.js +0 -1
- package/dist/assets/tsv-B_m7g4N7.js +0 -1
- package/dist/assets/tsx-B6W0miNI.js +0 -1
- package/dist/assets/turtle-BMR_PYu6.js +0 -1
- package/dist/assets/twig-NC5TFiHP.js +0 -1
- package/dist/assets/typescript-Dj6nwHGl.js +0 -1
- package/dist/assets/typespec-BpWG_bgh.js +0 -1
- package/dist/assets/typst-BVUVsWT6.js +0 -1
- package/dist/assets/v-CAQ2eGtk.js +0 -1
- package/dist/assets/vala-BFOHcciG.js +0 -1
- package/dist/assets/vb-CdO5JTpU.js +0 -1
- package/dist/assets/verilog-CJaU5se_.js +0 -1
- package/dist/assets/vesper-BEBZ7ncR.js +0 -1
- package/dist/assets/vhdl-DYoNaHQp.js +0 -1
- package/dist/assets/viml-m4uW47V2.js +0 -1
- package/dist/assets/vitesse-black-Bkuqu6BP.js +0 -1
- package/dist/assets/vitesse-dark-D0r3Knsf.js +0 -1
- package/dist/assets/vitesse-light-CVO1_9PV.js +0 -1
- package/dist/assets/vue-BuYVFjOK.js +0 -1
- package/dist/assets/vue-html-xdeiXROB.js +0 -1
- package/dist/assets/vyper-nyqBNV6O.js +0 -1
- package/dist/assets/wasm-C6j12Q_x.js +0 -1
- package/dist/assets/wasm-CG6Dc4jp.js +0 -1
- package/dist/assets/wenyan-7A4Fjokl.js +0 -1
- package/dist/assets/wgsl-CB0Krxn9.js +0 -1
- package/dist/assets/wikitext-DCE3LsBG.js +0 -1
- package/dist/assets/wolfram-C3FkfJm5.js +0 -1
- package/dist/assets/xml-e3z08dGr.js +0 -1
- package/dist/assets/xsl-Dd0NUgwM.js +0 -1
- package/dist/assets/yaml-CVw76BM1.js +0 -1
- package/dist/assets/zenscript-HnGAYVZD.js +0 -1
- package/dist/assets/zig-BVz_zdnA.js +0 -1
- package/server/app.js +0 -165
- package/server/app.test.js +0 -30
- package/server/bd.js +0 -227
- package/server/bd.test.js +0 -194
- package/server/cli/cli.test.js +0 -207
- package/server/cli/commands.integration.test.js +0 -148
- package/server/cli/commands.js +0 -285
- package/server/cli/commands.unit.test.js +0 -408
- package/server/cli/daemon.js +0 -340
- package/server/cli/daemon.test.js +0 -31
- package/server/cli/index.js +0 -135
- package/server/cli/open.js +0 -178
- package/server/cli/open.test.js +0 -26
- package/server/cli/usage.js +0 -49
- package/server/config.js +0 -36
- package/server/db.test.js +0 -169
- package/server/dolt-pool.js +0 -313
- package/server/dolt-queries.js +0 -781
- package/server/list-adapters.js +0 -421
- package/server/list-adapters.test.js +0 -208
- package/server/logging.js +0 -23
- package/server/registry-watcher.js +0 -200
- package/server/subscriptions.js +0 -299
- package/server/subscriptions.test.js +0 -128
- package/server/validators.js +0 -124
- package/server/watcher.js +0 -139
- package/server/watcher.test.js +0 -120
- package/server/ws.comments.test.js +0 -262
- package/server/ws.delete.test.js +0 -119
- package/server/ws.js +0 -1329
- package/server/ws.labels.test.js +0 -95
- package/server/ws.list-refresh.coalesce.test.js +0 -95
- package/server/ws.list-subscriptions.test.js +0 -403
- package/server/ws.mutation-window.test.js +0 -147
- package/server/ws.mutations.test.js +0 -389
- package/server/ws.test.js +0 -52
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// SQL used by the read-only Hono routes. Schema is owned by the Go CLI; we
|
|
2
|
+
// only SELECT + the occasional mutation. Both engines accept the same ANSI
|
|
3
|
+
// SQL we use here (LIKE patterns, IN clauses, parameterised values).
|
|
4
|
+
//
|
|
5
|
+
// Placeholders are `?` style. The pg adapter rewrites them to $N at runtime.
|
|
6
|
+
|
|
7
|
+
import { rowToComment, rowToDependency, rowToIssue } from './types.js';
|
|
8
|
+
|
|
9
|
+
export async function getConfigValue(db, key) {
|
|
10
|
+
const r = await db.one('SELECT value FROM config WHERE key = ?', [key]);
|
|
11
|
+
return r ? String(r.value) : '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Computed columns shared by list/ready/detail. Keeps the JSON shape stable
|
|
15
|
+
// across endpoints (parent_id, parent_title, total_children, closed_children,
|
|
16
|
+
// blocked_by_count, comment_count).
|
|
17
|
+
const ENRICHED_COMPUTED = `
|
|
18
|
+
(SELECT depends_on_id FROM dependencies d
|
|
19
|
+
WHERE d.issue_id = i.id AND d.type = 'parent-child' LIMIT 1) AS parent_id,
|
|
20
|
+
(SELECT p.title FROM dependencies d JOIN issues p ON p.id = d.depends_on_id
|
|
21
|
+
WHERE d.issue_id = i.id AND d.type = 'parent-child' LIMIT 1) AS parent_title,
|
|
22
|
+
(SELECT COUNT(*) FROM dependencies d
|
|
23
|
+
WHERE d.depends_on_id = i.id AND d.type = 'parent-child') AS total_children,
|
|
24
|
+
(SELECT COUNT(*) FROM dependencies d JOIN issues c ON c.id = d.issue_id
|
|
25
|
+
WHERE d.depends_on_id = i.id AND d.type = 'parent-child'
|
|
26
|
+
AND c.status = 'closed') AS closed_children,
|
|
27
|
+
(SELECT COUNT(*) FROM dependencies d JOIN issues b ON b.id = d.depends_on_id
|
|
28
|
+
WHERE d.issue_id = i.id AND d.type = 'blocks'
|
|
29
|
+
AND b.status NOT IN ('closed', 'pinned')) AS blocked_by_count,
|
|
30
|
+
(SELECT COUNT(*) FROM comments c WHERE c.issue_id = i.id) AS comment_count
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
// Full row + enrichment — used by getIssue (detail page wants everything).
|
|
34
|
+
const ENRICHED_FULL = `i.*, ${ENRICHED_COMPUTED}`;
|
|
35
|
+
|
|
36
|
+
// Slim row + enrichment — used by listIssues and readyIssues. Drops the heavy
|
|
37
|
+
// markdown bodies (description/design/acceptance_criteria/notes) and the JSON
|
|
38
|
+
// blobs (metadata/payload) which the board, list, and search dialog never
|
|
39
|
+
// render. On epic-heavy projects this cuts the polled response by 50–80%.
|
|
40
|
+
// rowToIssue (server/types.js) defaults missing fields to '' so the public
|
|
41
|
+
// JSON shape stays the same — clients still see those keys, just empty.
|
|
42
|
+
const ENRICHED_SLIM = `
|
|
43
|
+
i.id, i.title, i.status, i.priority, i.issue_type,
|
|
44
|
+
i.assignee, i.estimated_minutes, i.content_hash,
|
|
45
|
+
i.created_at, i.created_by, i.owner,
|
|
46
|
+
i.updated_at, i.started_at, i.closed_at, i.due_at, i.defer_until,
|
|
47
|
+
i.closed_by_session, i.close_reason,
|
|
48
|
+
i.external_ref, i.spec_id, i.source_repo, i.source_system,
|
|
49
|
+
i.sender, i.ephemeral, i.pinned, i.is_template,
|
|
50
|
+
i.wisp_type, i.mol_type, i.role_type, i.event_kind,
|
|
51
|
+
i.actor, i.target,
|
|
52
|
+
${ENRICHED_COMPUTED}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
export async function listIssues(db, filters = {}, limit = 0) {
|
|
56
|
+
const where = [];
|
|
57
|
+
const args = [];
|
|
58
|
+
if (filters.status) {
|
|
59
|
+
where.push('i.status = ?');
|
|
60
|
+
args.push(filters.status);
|
|
61
|
+
}
|
|
62
|
+
if (filters.status_in) {
|
|
63
|
+
const list = String(filters.status_in)
|
|
64
|
+
.split(',')
|
|
65
|
+
.map((s) => s.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
if (list.length) {
|
|
68
|
+
where.push(`i.status IN (${list.map(() => '?').join(',')})`);
|
|
69
|
+
args.push(...list);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (filters.type) {
|
|
73
|
+
where.push('i.issue_type = ?');
|
|
74
|
+
args.push(filters.type);
|
|
75
|
+
}
|
|
76
|
+
if (filters.assignee) {
|
|
77
|
+
where.push('i.assignee = ?');
|
|
78
|
+
args.push(filters.assignee);
|
|
79
|
+
}
|
|
80
|
+
if (filters.priority !== undefined && filters.priority !== null && filters.priority !== '') {
|
|
81
|
+
where.push('i.priority = ?');
|
|
82
|
+
args.push(Number(filters.priority));
|
|
83
|
+
}
|
|
84
|
+
let sql = `SELECT ${ENRICHED_SLIM} FROM issues i`;
|
|
85
|
+
if (where.length) sql += ' WHERE ' + where.join(' AND ');
|
|
86
|
+
// ORDER BY: priority is irrelevant once an issue is closed — caller can ask
|
|
87
|
+
// for closed_at_desc to get a "recently finished" feed. Default keeps
|
|
88
|
+
// priority primary with updated_at DESC as tiebreaker so stale work sinks.
|
|
89
|
+
// NULLS LAST handles postgres (defaults to NULLS FIRST on DESC); sqlite
|
|
90
|
+
// already places NULLs last by default but accepts the keyword.
|
|
91
|
+
if (filters.order === 'closed_at_desc') {
|
|
92
|
+
sql += ' ORDER BY i.closed_at DESC NULLS LAST, i.created_at DESC';
|
|
93
|
+
} else {
|
|
94
|
+
sql += ' ORDER BY i.priority ASC, i.updated_at DESC NULLS LAST';
|
|
95
|
+
}
|
|
96
|
+
if (limit > 0) sql += ` LIMIT ${Number(limit) | 0}`;
|
|
97
|
+
const rows = await db.all(sql, args);
|
|
98
|
+
return rows.map(rowToIssue);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function getIssue(db, id) {
|
|
102
|
+
const sql = `SELECT ${ENRICHED_FULL} FROM issues i WHERE i.id = ?`;
|
|
103
|
+
const r = await db.one(sql, [id]);
|
|
104
|
+
return r ? rowToIssue(r) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function listLabels(db, issueId) {
|
|
108
|
+
const rows = await db.all(
|
|
109
|
+
'SELECT label FROM labels WHERE issue_id = ? ORDER BY label',
|
|
110
|
+
[issueId],
|
|
111
|
+
);
|
|
112
|
+
return rows.map((r) => r.label);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// listBlockedBy returns up to `limit` issue ids/titles that currently block
|
|
116
|
+
// the given issue. Used by the IssueCard "blocked by" badges.
|
|
117
|
+
export async function listBlockedBy(db, issueId, limit = 5) {
|
|
118
|
+
const rows = await db.all(
|
|
119
|
+
`SELECT b.id, b.title FROM dependencies d
|
|
120
|
+
JOIN issues b ON b.id = d.depends_on_id
|
|
121
|
+
WHERE d.issue_id = ? AND d.type = 'blocks'
|
|
122
|
+
AND b.status NOT IN ('closed', 'pinned')
|
|
123
|
+
ORDER BY b.created_at
|
|
124
|
+
LIMIT ${Number(limit) | 0}`,
|
|
125
|
+
[issueId],
|
|
126
|
+
);
|
|
127
|
+
return rows;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// listBlocks returns up to `limit` issue ids/titles currently blocked BY the
|
|
131
|
+
// given issue (i.e., rows where this issue is depends_on_id, type='blocks').
|
|
132
|
+
// Mirror of listBlockedBy with reversed direction; powers the "Blocks" card
|
|
133
|
+
// in the issue-detail sidebar.
|
|
134
|
+
export async function listBlocks(db, issueId, limit = 10) {
|
|
135
|
+
const rows = await db.all(
|
|
136
|
+
`SELECT b.id, b.title FROM dependencies d
|
|
137
|
+
JOIN issues b ON b.id = d.issue_id
|
|
138
|
+
WHERE d.depends_on_id = ? AND d.type = 'blocks'
|
|
139
|
+
AND b.status NOT IN ('closed', 'pinned')
|
|
140
|
+
ORDER BY b.created_at
|
|
141
|
+
LIMIT ${Number(limit) | 0}`,
|
|
142
|
+
[issueId],
|
|
143
|
+
);
|
|
144
|
+
return rows;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// listChildren returns the parent-child children of an issue (where THIS
|
|
148
|
+
// issue is the depends_on_id, type='parent-child'). Used by the metadata
|
|
149
|
+
// sidebar's Children card.
|
|
150
|
+
export async function listChildren(db, parentId) {
|
|
151
|
+
const rows = await db.all(
|
|
152
|
+
`SELECT c.id, c.title, c.status, c.priority, c.issue_type
|
|
153
|
+
FROM dependencies d
|
|
154
|
+
JOIN issues c ON c.id = d.issue_id
|
|
155
|
+
WHERE d.depends_on_id = ? AND d.type = 'parent-child'
|
|
156
|
+
ORDER BY c.priority ASC, c.created_at ASC`,
|
|
157
|
+
[parentId],
|
|
158
|
+
);
|
|
159
|
+
return rows;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function listDependencies(db, issueId) {
|
|
163
|
+
const rows = await db.all(
|
|
164
|
+
'SELECT * FROM dependencies WHERE issue_id = ? OR depends_on_id = ? ORDER BY created_at',
|
|
165
|
+
[issueId, issueId],
|
|
166
|
+
);
|
|
167
|
+
return rows.map(rowToDependency);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function listComments(db, issueId) {
|
|
171
|
+
const rows = await db.all(
|
|
172
|
+
'SELECT * FROM comments WHERE issue_id = ? ORDER BY created_at',
|
|
173
|
+
[issueId],
|
|
174
|
+
);
|
|
175
|
+
return rows.map(rowToComment);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function readyIssues(db) {
|
|
179
|
+
const sql = `
|
|
180
|
+
SELECT ${ENRICHED_SLIM} FROM issues i
|
|
181
|
+
WHERE i.status = 'open'
|
|
182
|
+
AND i.ephemeral = 0
|
|
183
|
+
AND i.is_template = 0
|
|
184
|
+
AND (i.defer_until IS NULL OR i.defer_until <= ?)
|
|
185
|
+
AND NOT EXISTS (
|
|
186
|
+
SELECT 1 FROM dependencies d
|
|
187
|
+
JOIN issues blocker ON blocker.id = d.depends_on_id
|
|
188
|
+
WHERE d.issue_id = i.id
|
|
189
|
+
AND d.type = 'blocks'
|
|
190
|
+
AND blocker.status NOT IN ('closed', 'pinned')
|
|
191
|
+
)
|
|
192
|
+
ORDER BY i.priority ASC, i.updated_at DESC NULLS LAST`;
|
|
193
|
+
const now = new Date().toISOString();
|
|
194
|
+
const rows = await db.all(sql, [now]);
|
|
195
|
+
return rows.map(rowToIssue);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------- mutations (subset: comments + labels only) ----------
|
|
199
|
+
|
|
200
|
+
export async function addComment(db, { id, issueId, author, text }) {
|
|
201
|
+
const now = new Date().toISOString();
|
|
202
|
+
await db.exec(
|
|
203
|
+
`INSERT INTO comments (id, issue_id, author, text, created_at)
|
|
204
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
205
|
+
[id, issueId, author, text, now],
|
|
206
|
+
);
|
|
207
|
+
return { id, issue_id: issueId, author, text, created_at: now };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function deleteComment(db, commentId) {
|
|
211
|
+
await db.exec('DELETE FROM comments WHERE id = ?', [commentId]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function addLabel(db, issueId, label) {
|
|
215
|
+
// idempotent — re-adding the same (issue, label) is a no-op
|
|
216
|
+
try {
|
|
217
|
+
await db.exec(
|
|
218
|
+
'INSERT INTO labels (issue_id, label) VALUES (?, ?)',
|
|
219
|
+
[issueId, label],
|
|
220
|
+
);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (!/UNIQUE|duplicate/i.test(String(err?.message))) throw err;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function removeLabel(db, issueId, label) {
|
|
227
|
+
await db.exec(
|
|
228
|
+
'DELETE FROM labels WHERE issue_id = ? AND label = ?',
|
|
229
|
+
[issueId, label],
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function listProjects(db) {
|
|
234
|
+
if (db.driver !== 'postgres') return [];
|
|
235
|
+
const sql = `
|
|
236
|
+
SELECT s.schema_name AS prefix
|
|
237
|
+
FROM information_schema.schemata s
|
|
238
|
+
WHERE s.schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'public')
|
|
239
|
+
AND s.schema_name NOT LIKE 'pg_%'
|
|
240
|
+
AND EXISTS (
|
|
241
|
+
SELECT 1 FROM information_schema.tables t
|
|
242
|
+
WHERE t.table_schema = s.schema_name AND t.table_name = 'config'
|
|
243
|
+
)
|
|
244
|
+
ORDER BY s.schema_name`;
|
|
245
|
+
const rows = await db.all(sql);
|
|
246
|
+
return rows.map((r) => ({ prefix: r.prefix }));
|
|
247
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { login, logout } from '../auth.js';
|
|
3
|
+
|
|
4
|
+
export function authRouter(auth) {
|
|
5
|
+
const r = new Hono();
|
|
6
|
+
|
|
7
|
+
r.post('/login', async (c) => {
|
|
8
|
+
if (!auth.enabled) return c.json({ error: 'auth disabled' }, 400);
|
|
9
|
+
const body = await c.req.json().catch(() => ({}));
|
|
10
|
+
const { username, password } = body;
|
|
11
|
+
if (!username || !password) {
|
|
12
|
+
return c.json({ error: 'username and password are required' }, 400);
|
|
13
|
+
}
|
|
14
|
+
const result = login(auth, username, password);
|
|
15
|
+
if (!result) return c.json({ error: 'invalid credentials' }, 401);
|
|
16
|
+
return c.json(result);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
r.post('/logout', async (c) => {
|
|
20
|
+
if (!auth.enabled) return c.json({ ok: true });
|
|
21
|
+
const token = c.get('token');
|
|
22
|
+
if (token) logout(auth, token);
|
|
23
|
+
return c.json({ ok: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return r;
|
|
27
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { etag } from 'hono/etag';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
addComment,
|
|
6
|
+
addLabel,
|
|
7
|
+
deleteComment,
|
|
8
|
+
getIssue,
|
|
9
|
+
listBlockedBy,
|
|
10
|
+
listBlocks,
|
|
11
|
+
listChildren,
|
|
12
|
+
listComments,
|
|
13
|
+
listDependencies,
|
|
14
|
+
listIssues,
|
|
15
|
+
listLabels,
|
|
16
|
+
readyIssues,
|
|
17
|
+
removeLabel,
|
|
18
|
+
} from '../queries.js';
|
|
19
|
+
|
|
20
|
+
// Reads the per-request project db from c.get('db'). Set by the project-
|
|
21
|
+
// scope middleware in index.js. No db is captured at registration time so
|
|
22
|
+
// the same router serves every project.
|
|
23
|
+
export function issuesRouter() {
|
|
24
|
+
const r = new Hono();
|
|
25
|
+
|
|
26
|
+
// ETag for GET responses: the board and detail pages poll every 5s, so
|
|
27
|
+
// most refetches return identical bodies. The middleware hashes the
|
|
28
|
+
// serialized response and replies 304 with an empty body when the
|
|
29
|
+
// client's If-None-Match header matches — saves bandwidth and the
|
|
30
|
+
// client-side parse/render. Skipped for mutations.
|
|
31
|
+
//
|
|
32
|
+
// Cache-Control "private, no-cache" tells the browser to keep the body
|
|
33
|
+
// in the per-origin disk cache and always revalidate via If-None-Match.
|
|
34
|
+
// Without this header the browser may not cache JSON responses at all,
|
|
35
|
+
// which would mean ETag is set but never sent back — neutering the 304
|
|
36
|
+
// path. "private" prevents shared caches from storing the data.
|
|
37
|
+
const etagMiddleware = etag();
|
|
38
|
+
r.use('/*', async (c, next) => {
|
|
39
|
+
if (c.req.method !== 'GET') return next();
|
|
40
|
+
await etagMiddleware(c, next);
|
|
41
|
+
if (c.res.status === 200 || c.res.status === 304) {
|
|
42
|
+
c.res.headers.set('Cache-Control', 'private, no-cache');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
r.get('/', async (c) => {
|
|
47
|
+
const db = c.get('db');
|
|
48
|
+
const q = c.req.query();
|
|
49
|
+
const limit = q.limit ? Number(q.limit) : 0;
|
|
50
|
+
const issues = await listIssues(db, q, limit);
|
|
51
|
+
return c.json({ issues });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
r.get('/ready', async (c) => {
|
|
55
|
+
const db = c.get('db');
|
|
56
|
+
const issues = await readyIssues(db);
|
|
57
|
+
return c.json({ issues });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
r.get('/:id', async (c) => {
|
|
61
|
+
const db = c.get('db');
|
|
62
|
+
const id = c.req.param('id');
|
|
63
|
+
const issue = await getIssue(db, id);
|
|
64
|
+
if (!issue) return c.json({ error: 'not found' }, 404);
|
|
65
|
+
const [labels, deps, comments, blockedBy, blocks, children] = await Promise.all([
|
|
66
|
+
listLabels(db, id),
|
|
67
|
+
listDependencies(db, id),
|
|
68
|
+
listComments(db, id),
|
|
69
|
+
listBlockedBy(db, id, 5),
|
|
70
|
+
listBlocks(db, id, 10),
|
|
71
|
+
listChildren(db, id),
|
|
72
|
+
]);
|
|
73
|
+
return c.json({
|
|
74
|
+
issue, labels, dependencies: deps, comments,
|
|
75
|
+
blocked_by: blockedBy,
|
|
76
|
+
blocks,
|
|
77
|
+
children,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
r.post('/:id/comments', async (c) => {
|
|
82
|
+
const db = c.get('db');
|
|
83
|
+
const issueId = c.req.param('id');
|
|
84
|
+
const body = await c.req.json().catch(() => ({}));
|
|
85
|
+
const text = String(body.text || '').trim();
|
|
86
|
+
if (!text) return c.json({ error: 'text is required' }, 400);
|
|
87
|
+
const user = c.get('user') || { username: 'anon' };
|
|
88
|
+
const comment = await addComment(db, {
|
|
89
|
+
id: randomUUID(),
|
|
90
|
+
issueId,
|
|
91
|
+
author: user.username,
|
|
92
|
+
text,
|
|
93
|
+
});
|
|
94
|
+
return c.json({ comment });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
r.delete('/:id/comments/:commentId', async (c) => {
|
|
98
|
+
const db = c.get('db');
|
|
99
|
+
await deleteComment(db, c.req.param('commentId'));
|
|
100
|
+
return c.json({ ok: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
r.post('/:id/labels', async (c) => {
|
|
104
|
+
const db = c.get('db');
|
|
105
|
+
const issueId = c.req.param('id');
|
|
106
|
+
const body = await c.req.json().catch(() => ({}));
|
|
107
|
+
const label = String(body.label || '').trim();
|
|
108
|
+
if (!label) return c.json({ error: 'label is required' }, 400);
|
|
109
|
+
await addLabel(db, issueId, label);
|
|
110
|
+
return c.json({ ok: true, label });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
r.delete('/:id/labels/:label', async (c) => {
|
|
114
|
+
const db = c.get('db');
|
|
115
|
+
await removeLabel(db, c.req.param('id'), c.req.param('label'));
|
|
116
|
+
return c.json({ ok: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return r;
|
|
120
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SSE: per-project change stream. Server polls a cheap "fingerprint" query
|
|
2
|
+
// every WATCH_MS and broadcasts a `tick` event when it changes. Clients open
|
|
3
|
+
// an EventSource and invalidate their cached queries on tick.
|
|
4
|
+
//
|
|
5
|
+
// Why poll-and-broadcast: bd is mutated by the CLI, agents, and the web
|
|
6
|
+
// server. Postgres LISTEN/NOTIFY would catch all of them but only on pg, and
|
|
7
|
+
// requires triggers. A 1.5s SQL fingerprint (max(updated_at), count(*)) is
|
|
8
|
+
// cheap enough for the polling rate we want, works on both engines, and
|
|
9
|
+
// scales: N connected browsers per project share one watcher.
|
|
10
|
+
//
|
|
11
|
+
// Fingerprint covers issues, dependencies, comments, and labels — anything
|
|
12
|
+
// the board / list / detail page renders.
|
|
13
|
+
|
|
14
|
+
const WATCH_MS = 1500; // server poll cadence
|
|
15
|
+
const HEARTBEAT_MS = 25000; // SSE ping to keep proxies from killing the stream
|
|
16
|
+
|
|
17
|
+
// project prefix -> { db, hash, timer, subs:Set<{enqueue,close}> }
|
|
18
|
+
const watchers = new Map();
|
|
19
|
+
|
|
20
|
+
// stamp normalises whatever the driver returns for a timestamp column
|
|
21
|
+
// (string from sqlite, Date from pg) into a stable string for hashing.
|
|
22
|
+
const stamp = (v) => {
|
|
23
|
+
if (v == null) return '';
|
|
24
|
+
if (v instanceof Date) return v.toISOString();
|
|
25
|
+
return String(v);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function fingerprint(db) {
|
|
29
|
+
// Cheap "did anything change?" probe: max-of-mutable-timestamp + row count
|
|
30
|
+
// for each table the UI cares about. Portable across sqlite/postgres.
|
|
31
|
+
const i = await db.one(`SELECT MAX(updated_at) AS m, COUNT(*) AS c FROM issues`);
|
|
32
|
+
const d = await db.one(`SELECT MAX(created_at) AS m, COUNT(*) AS c FROM dependencies`);
|
|
33
|
+
const cm = await db.one(`SELECT MAX(created_at) AS m, COUNT(*) AS c FROM comments`);
|
|
34
|
+
const l = await db.one(`SELECT COUNT(*) AS c FROM labels`);
|
|
35
|
+
return [
|
|
36
|
+
`i:${stamp(i?.m)}/${i?.c ?? 0}`,
|
|
37
|
+
`d:${stamp(d?.m)}/${d?.c ?? 0}`,
|
|
38
|
+
`c:${stamp(cm?.m)}/${cm?.c ?? 0}`,
|
|
39
|
+
`l:${l?.c ?? 0}`,
|
|
40
|
+
].join('|');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function startWatcher(prefix, db) {
|
|
44
|
+
const w = { db, hash: '', subs: new Set(), timer: null };
|
|
45
|
+
watchers.set(prefix, w);
|
|
46
|
+
const tick = async () => {
|
|
47
|
+
if (w.subs.size === 0) {
|
|
48
|
+
// No subscribers — stop polling and let the watcher idle out.
|
|
49
|
+
clearInterval(w.timer);
|
|
50
|
+
watchers.delete(prefix);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let next;
|
|
54
|
+
try { next = await fingerprint(db); } catch (err) {
|
|
55
|
+
// Don't kill the watcher on transient db errors.
|
|
56
|
+
if (process.env.DEBUG) console.error('stream fingerprint failed:', err.message);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (next !== w.hash) {
|
|
60
|
+
w.hash = next;
|
|
61
|
+
const payload = `event: tick\ndata: ${next}\n\n`;
|
|
62
|
+
for (const sub of w.subs) {
|
|
63
|
+
try { sub.enqueue(payload); } catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
w.timer = setInterval(tick, WATCH_MS);
|
|
68
|
+
// prime hash so the first real change emits, but the initial open doesn't
|
|
69
|
+
fingerprint(db).then((h) => { w.hash = h; }).catch(() => {});
|
|
70
|
+
return w;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function streamHandler(c) {
|
|
74
|
+
const prefix = c.get('project');
|
|
75
|
+
const db = c.get('db');
|
|
76
|
+
if (!prefix || !db) return c.json({ error: 'no project' }, 400);
|
|
77
|
+
|
|
78
|
+
const w = watchers.get(prefix) || startWatcher(prefix, db);
|
|
79
|
+
|
|
80
|
+
const stream = new ReadableStream({
|
|
81
|
+
start(controller) {
|
|
82
|
+
const enc = new TextEncoder();
|
|
83
|
+
const sub = {
|
|
84
|
+
enqueue: (s) => controller.enqueue(enc.encode(s)),
|
|
85
|
+
close: () => { try { controller.close(); } catch {} },
|
|
86
|
+
};
|
|
87
|
+
w.subs.add(sub);
|
|
88
|
+
// Initial hello + heartbeat loop.
|
|
89
|
+
sub.enqueue(`event: hello\ndata: ${w.hash}\n\n`);
|
|
90
|
+
const hb = setInterval(() => {
|
|
91
|
+
try { sub.enqueue(`: ping\n\n`); } catch {}
|
|
92
|
+
}, HEARTBEAT_MS);
|
|
93
|
+
// Cleanup when the client disconnects.
|
|
94
|
+
const onClose = () => {
|
|
95
|
+
clearInterval(hb);
|
|
96
|
+
w.subs.delete(sub);
|
|
97
|
+
sub.close();
|
|
98
|
+
};
|
|
99
|
+
c.req.raw.signal?.addEventListener('abort', onClose);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return new Response(stream, {
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
106
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
107
|
+
'Connection': 'keep-alive',
|
|
108
|
+
'X-Accel-Buffering': 'no', // disable nginx buffering
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
package/server/types.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Row-shape helpers shared by the route handlers. Matches the public types
|
|
2
|
+
// in the Go side (beads.Issue, beads.Dependency, beads.Comment).
|
|
3
|
+
|
|
4
|
+
export function rowToIssue(r) {
|
|
5
|
+
if (!r) return null;
|
|
6
|
+
return {
|
|
7
|
+
id: r.id,
|
|
8
|
+
content_hash: r.content_hash || '',
|
|
9
|
+
title: r.title,
|
|
10
|
+
description: r.description || '',
|
|
11
|
+
design: r.design || '',
|
|
12
|
+
acceptance_criteria: r.acceptance_criteria || '',
|
|
13
|
+
notes: r.notes || '',
|
|
14
|
+
status: r.status,
|
|
15
|
+
priority: Number(r.priority),
|
|
16
|
+
issue_type: r.issue_type,
|
|
17
|
+
assignee: r.assignee || '',
|
|
18
|
+
estimated_minutes: Number(r.estimated_minutes || 0),
|
|
19
|
+
created_at: toISO(r.created_at),
|
|
20
|
+
created_by: r.created_by || '',
|
|
21
|
+
owner: r.owner || '',
|
|
22
|
+
updated_at: toISO(r.updated_at),
|
|
23
|
+
started_at: toISO(r.started_at),
|
|
24
|
+
closed_at: toISO(r.closed_at),
|
|
25
|
+
closed_by_session: r.closed_by_session || '',
|
|
26
|
+
external_ref: r.external_ref || '',
|
|
27
|
+
spec_id: r.spec_id || '',
|
|
28
|
+
metadata: r.metadata || '{}',
|
|
29
|
+
source_repo: r.source_repo || '',
|
|
30
|
+
source_system: r.source_system || '',
|
|
31
|
+
close_reason: r.close_reason || '',
|
|
32
|
+
sender: r.sender || '',
|
|
33
|
+
ephemeral: !!Number(r.ephemeral),
|
|
34
|
+
pinned: !!Number(r.pinned),
|
|
35
|
+
is_template: !!Number(r.is_template),
|
|
36
|
+
wisp_type: r.wisp_type || '',
|
|
37
|
+
mol_type: r.mol_type || '',
|
|
38
|
+
role_type: r.role_type || '',
|
|
39
|
+
event_kind: r.event_kind || '',
|
|
40
|
+
actor: r.actor || '',
|
|
41
|
+
target: r.target || '',
|
|
42
|
+
payload: r.payload || '',
|
|
43
|
+
due_at: toISO(r.due_at),
|
|
44
|
+
defer_until: toISO(r.defer_until),
|
|
45
|
+
// Enriched fields populated by ENRICHED SELECT (queries.js). Optional —
|
|
46
|
+
// detail/list endpoints set them; raw inserts won't have them.
|
|
47
|
+
parent_id: r.parent_id || '',
|
|
48
|
+
parent_title: r.parent_title || '',
|
|
49
|
+
total_children: r.total_children != null ? Number(r.total_children) : 0,
|
|
50
|
+
closed_children: r.closed_children != null ? Number(r.closed_children) : 0,
|
|
51
|
+
blocked_by_count: r.blocked_by_count != null ? Number(r.blocked_by_count) : 0,
|
|
52
|
+
comment_count: r.comment_count != null ? Number(r.comment_count) : 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function rowToDependency(r) {
|
|
57
|
+
return {
|
|
58
|
+
issue_id: r.issue_id,
|
|
59
|
+
depends_on_id: r.depends_on_id,
|
|
60
|
+
type: r.type,
|
|
61
|
+
created_at: toISO(r.created_at),
|
|
62
|
+
created_by: r.created_by || '',
|
|
63
|
+
metadata: r.metadata || '{}',
|
|
64
|
+
thread_id: r.thread_id || '',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function rowToComment(r) {
|
|
69
|
+
return {
|
|
70
|
+
id: r.id,
|
|
71
|
+
issue_id: r.issue_id,
|
|
72
|
+
author: r.author,
|
|
73
|
+
text: r.text,
|
|
74
|
+
created_at: toISO(r.created_at),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toISO(v) {
|
|
79
|
+
if (!v) return null;
|
|
80
|
+
if (v instanceof Date) return v.toISOString();
|
|
81
|
+
// sqlite returns 'YYYY-MM-DD HH:MM:SS' or ISO already; postgres returns Date
|
|
82
|
+
return new Date(v).toISOString();
|
|
83
|
+
}
|