ocean-brain 0.11.1 → 0.12.1

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.
Files changed (152) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -2
  3. package/server/client/dist/assets/ArrowRight.es-DZTjsBaI.js +1 -0
  4. package/server/client/dist/assets/AuxiliaryPanelHeader-Bp3iRjm6.js +1 -0
  5. package/server/client/dist/assets/Calendar-CBYfyLwm.js +25 -0
  6. package/server/client/dist/assets/Callout-Cye6_6BM.js +1 -0
  7. package/server/client/dist/assets/CheckCircle.es-BTloiuti.js +1 -0
  8. package/server/client/dist/assets/Copy.es-CkLUgwY-.js +1 -0
  9. package/server/client/dist/assets/File.es-c83QExqr.js +1 -0
  10. package/server/client/dist/assets/FlowArrow.es-DWiIXWQG.js +1 -0
  11. package/server/client/dist/assets/Graph-jbPLcne9.js +46 -0
  12. package/server/client/dist/assets/Image.es-vY-nTyOe.js +1 -0
  13. package/server/client/dist/assets/Note-BTQLB80Y.js +25 -0
  14. package/server/client/dist/assets/{NotePropertiesPanel-CsaNhRnE.js → NotePropertiesPanel-uO-JKy19.js} +2 -2
  15. package/server/client/dist/assets/Plus.es-CaRgmXc_.js +1 -0
  16. package/server/client/dist/assets/Reminders-BAgZEUwV.js +1 -0
  17. package/server/client/dist/assets/Search-D0yR4DTH.js +31 -0
  18. package/server/client/dist/assets/SurfaceCard-BAlcul6z.js +1 -0
  19. package/server/client/dist/assets/Tag-B8UYGbt9.js +1 -0
  20. package/server/client/dist/assets/TagNotes-CDVHz74G.js +1 -0
  21. package/server/client/dist/assets/Trash.es-prxIxo9c.js +1 -0
  22. package/server/client/dist/assets/ViewChip-Cq_YzRvB.js +1 -0
  23. package/server/client/dist/assets/Views-Cjdbbut-.js +1 -0
  24. package/server/client/dist/assets/{abnfDiagram-VRR7QNED-CWlV5dXw.js → abnfDiagram-VRR7QNED-2cuaaDg0.js} +1 -1
  25. package/server/client/dist/assets/app-Clwvoiwx.js +139 -0
  26. package/server/client/dist/assets/app-DGFNtdpS.css +1 -0
  27. package/server/client/dist/assets/{arc-BV-n6RUX.js → arc-Cr-oYJoQ.js} +1 -1
  28. package/server/client/dist/assets/{architectureDiagram-ZJ3FMSHR-CekzMXdu.js → architectureDiagram-ZJ3FMSHR-HF8-bEM0.js} +1 -1
  29. package/server/client/dist/assets/{blockDiagram-677ZJIJ3-B49yHoxq.js → blockDiagram-677ZJIJ3-evR8rlpQ.js} +1 -1
  30. package/server/client/dist/assets/{c4Diagram-LMCZKHZV-0AfB9NBC.js → c4Diagram-LMCZKHZV-zCjuWUmC.js} +1 -1
  31. package/server/client/dist/assets/channel-Cq5ZBtfo.js +1 -0
  32. package/server/client/dist/assets/{chunk-2Q5K7J3B-DaPQAU1q.js → chunk-2Q5K7J3B-P0v1gRui.js} +1 -1
  33. package/server/client/dist/assets/{chunk-32BRIVSS-CfL3kZAg.js → chunk-32BRIVSS-102-EyWU.js} +1 -1
  34. package/server/client/dist/assets/{chunk-5VM5RSS4-DeOwWPE_.js → chunk-5VM5RSS4-uqI_1K1p.js} +1 -1
  35. package/server/client/dist/assets/{chunk-EX3LRPZG-C3W1oc9y.js → chunk-EX3LRPZG-k5hlNinR.js} +1 -1
  36. package/server/client/dist/assets/{chunk-JWPE2WC7-_Oqh79OY.js → chunk-JWPE2WC7-PLvgEy_B.js} +1 -1
  37. package/server/client/dist/assets/{chunk-MOJQB5TN-DDKNNzI6.js → chunk-MOJQB5TN-B-LmIwhJ.js} +1 -1
  38. package/server/client/dist/assets/{chunk-RYQCIY6F-DCHah7CC.js → chunk-RYQCIY6F-CaNSMki0.js} +1 -1
  39. package/server/client/dist/assets/{chunk-V7JOEXUC-uUOIzgxj.js → chunk-V7JOEXUC-CmSzAkUx.js} +1 -1
  40. package/server/client/dist/assets/{chunk-VR4S4FIN-DDVw2WPs.js → chunk-VR4S4FIN-IiOfWaBG.js} +1 -1
  41. package/server/client/dist/assets/{chunk-XXDRQBXY-Coo4UFm2.js → chunk-XXDRQBXY-DRXrI6SM.js} +1 -1
  42. package/server/client/dist/assets/classDiagram-OUVF2IWQ-BqUaa8a6.js +1 -0
  43. package/server/client/dist/assets/classDiagram-v2-EOCWNBFH-BqUaa8a6.js +1 -0
  44. package/server/client/dist/assets/{cose-bilkent-JH36ORCC-C3pHmHpD.js → cose-bilkent-JH36ORCC-COcEaZRS.js} +1 -1
  45. package/server/client/dist/assets/{cynefinDiagram-TSTJHNR4-DZ6iiJFo.js → cynefinDiagram-TSTJHNR4-CSHqcg9K.js} +1 -1
  46. package/server/client/dist/assets/{dagre-VKFMJZFB-DNELbhwC.js → dagre-VKFMJZFB-XAWCPOS0.js} +1 -1
  47. package/server/client/dist/assets/{diagram-FQU43EPY-CMEvZJ61.js → diagram-FQU43EPY-scBCDUnE.js} +1 -1
  48. package/server/client/dist/assets/{diagram-G47NLZAW-BYDtf6j9.js → diagram-G47NLZAW-nPKP5kd8.js} +1 -1
  49. package/server/client/dist/assets/{diagram-NH7WQ7WH-CmXfcCQA.js → diagram-NH7WQ7WH-DcWalY4j.js} +1 -1
  50. package/server/client/dist/assets/{diagram-OA4YK3LP-BflTKJxU.js → diagram-OA4YK3LP-CXTlHB5A.js} +4 -4
  51. package/server/client/dist/assets/{diagram-WEI45ONY-KW3fKqna.js → diagram-WEI45ONY-D1uvaJub.js} +1 -1
  52. package/server/client/dist/assets/{ebnfDiagram-CCIWWBDH-CbyhY5Nj.js → ebnfDiagram-CCIWWBDH-nHt3vRNt.js} +1 -1
  53. package/server/client/dist/assets/{erDiagram-Q63AITRT-CC2eZV8g.js → erDiagram-Q63AITRT-K6stUUV2.js} +1 -1
  54. package/server/client/dist/assets/{flowDiagram-23GEKE2U-unplo7Qm.js → flowDiagram-23GEKE2U-DDWtKrEp.js} +4 -4
  55. package/server/client/dist/assets/{ganttDiagram-NO4QXBWP-BcBK5HcZ.js → ganttDiagram-NO4QXBWP-Daqh7b4I.js} +1 -1
  56. package/server/client/dist/assets/{gitGraphDiagram-IHSO6WYX-DgVbbb5k.js → gitGraphDiagram-IHSO6WYX-D73CLkcG.js} +1 -1
  57. package/server/client/dist/assets/{image.api-CyQisInP.js → image.api-Bbwmcw0T.js} +1 -1
  58. package/server/client/dist/assets/index-PjoL3frH.js +1 -0
  59. package/server/client/dist/assets/{infoDiagram-FWYZ7A6U-N_D9RdfG.js → infoDiagram-FWYZ7A6U-Bc1YI5Iu.js} +1 -1
  60. package/server/client/dist/assets/{ishikawaDiagram-FXEZZL3T-BBKrzmB2.js → ishikawaDiagram-FXEZZL3T-BJDXbmaY.js} +1 -1
  61. package/server/client/dist/assets/{journeyDiagram-5HDEW3XC-NdCxLmQB.js → journeyDiagram-5HDEW3XC-DlyJx4xT.js} +1 -1
  62. package/server/client/dist/assets/{kanban-definition-HUTT4EX6-Ck7t4-tt.js → kanban-definition-HUTT4EX6-AmTeXesc.js} +1 -1
  63. package/server/client/dist/assets/manage-image-DCBgglLx.js +1 -0
  64. package/server/client/dist/assets/manage-image-detail-CLPXVSZf.js +1 -0
  65. package/server/client/dist/assets/{mcp-CCaFd0zm.js → mcp-Y2p95Lm9.js} +1 -1
  66. package/server/client/dist/assets/mcp-admin-adapter-z9OaMVrl.js +1 -0
  67. package/server/client/dist/assets/{mermaid.core-B14ZAmWA.js → mermaid.core-B528P54n.js} +4 -4
  68. package/server/client/dist/assets/{mindmap-definition-LN4V7U3C-BTUqSdcP.js → mindmap-definition-LN4V7U3C-C_6tKVmY.js} +1 -1
  69. package/server/client/dist/assets/{note-ui-CtijppHK.js → note-ui-Cv6quEYh.js} +1 -1
  70. package/server/client/dist/assets/{pegDiagram-2B236MQR-Cjcu3mVz.js → pegDiagram-2B236MQR-CGKCNdrV.js} +1 -1
  71. package/server/client/dist/assets/{pieDiagram-ENE6RG2P-BxI_c22Z.js → pieDiagram-ENE6RG2P-C3cMh7d5.js} +1 -1
  72. package/server/client/dist/assets/{placeholder-DR75IAi_.js → placeholder-D4Lkz_or.js} +1 -1
  73. package/server/client/dist/assets/properties-CwxDtivz.js +1 -0
  74. package/server/client/dist/assets/{quadrantDiagram-ABIIQ3AL-DGm0a-yR.js → quadrantDiagram-ABIIQ3AL-CVVzGxBd.js} +1 -1
  75. package/server/client/dist/assets/{railroadDiagram-RFXS5EU6-DmCDu89O.js → railroadDiagram-RFXS5EU6-wa-4aJR9.js} +1 -1
  76. package/server/client/dist/assets/{requirementDiagram-TGXJPOKE-CBpUt7Ig.js → requirementDiagram-TGXJPOKE-DyuC9W7q.js} +1 -1
  77. package/server/client/dist/assets/route-preload.js +2 -2
  78. package/server/client/dist/assets/{sankeyDiagram-HTMAVEWB-BjGnDZlp.js → sankeyDiagram-HTMAVEWB-B70y-MQg.js} +1 -1
  79. package/server/client/dist/assets/search-BlbdHNDA.js +1 -0
  80. package/server/client/dist/assets/search-admin-adapter-BLMYNm9w.js +1 -0
  81. package/server/client/dist/assets/{sequenceDiagram-DBY2YBRQ-B7rpCfS6.js → sequenceDiagram-DBY2YBRQ-mGkh3IEy.js} +1 -1
  82. package/server/client/dist/assets/{sizeCapture-X5ZJPWSS-CUJhD0uW.js → sizeCapture-X5ZJPWSS-BhnCNP5z.js} +1 -1
  83. package/server/client/dist/assets/sortable.esm-DgSW3bAA.js +5 -0
  84. package/server/client/dist/assets/{stateDiagram-2N3HPSRC-Y9-3XqRX.js → stateDiagram-2N3HPSRC-BqSKSfKi.js} +1 -1
  85. package/server/client/dist/assets/{stateDiagram-v2-6OUMAXLB-DbHXhT02.js → stateDiagram-v2-6OUMAXLB-Wl7lGTf3.js} +1 -1
  86. package/server/client/dist/assets/{swimlanes-5IMT3BWC-Cs1NO9ng.js → swimlanes-5IMT3BWC-CZ7ckdmY.js} +2 -2
  87. package/server/client/dist/assets/swimlanesDiagram-G3AALYLV-BZL3DQ-c.js +8 -0
  88. package/server/client/dist/assets/{tag.api-DNSQpqwk.js → tag.api-bU52U0tT.js} +1 -1
  89. package/server/client/dist/assets/time-DZKkPFUi.js +361 -0
  90. package/server/client/dist/assets/{timeline-definition-FHXFAJF6-8fqD4WNL.js → timeline-definition-FHXFAJF6-C-t9-x4M.js} +3 -3
  91. package/server/client/dist/assets/trash-BNeUFJnC.js +1 -0
  92. package/server/client/dist/assets/{url-9nrbQs7Z.js → url-BvjSC8PU.js} +1 -1
  93. package/server/client/dist/assets/useNoteMutate-VJYw0Vt_.js +9 -0
  94. package/server/client/dist/assets/useReminderMutate-DaO0QssD.js +121 -0
  95. package/server/client/dist/assets/{vennDiagram-L72KCM5P-BPqKkd5a.js → vennDiagram-L72KCM5P-CP014Nl5.js} +1 -1
  96. package/server/client/dist/assets/{wardleyDiagram-EHGQE667-Cq11kU2M.js → wardleyDiagram-EHGQE667-B7Ghd-94.js} +1 -1
  97. package/server/client/dist/assets/{xychartDiagram-FW5EYKEG-DJ6UzFj-.js → xychartDiagram-FW5EYKEG-C4YGS0YJ.js} +1 -1
  98. package/server/client/dist/index.html +6 -6
  99. package/server/dist/app.js +5 -5
  100. package/server/dist/features/note/graphql/note.query.resolver.js +9 -1
  101. package/server/dist/features/note/graphql/note.type-defs.js +2 -0
  102. package/server/dist/features/note/services/content-blocks.js +6 -1
  103. package/server/dist/features/reminder/graphql/reminder.query.resolver.js +62 -0
  104. package/server/dist/features/reminder/graphql/reminder.type-defs.js +25 -0
  105. package/server/dist/features/search/sqlite-vector-index.js +71 -138
  106. package/server/dist/features/view/graphql/view.query.resolver.js +40 -5
  107. package/server/dist/features/view/graphql/view.type-defs.js +23 -1
  108. package/server/dist/features/view/services/workspace.js +108 -5
  109. package/server/dist/main.js +1 -35
  110. package/server/dist/routes/api.js +6 -1
  111. package/server/dist/routes/client.js +15 -4
  112. package/server/dist/server.js +40 -0
  113. package/server/prisma/schema.prisma +1 -0
  114. package/server/client/dist/assets/ArrowRight.es-BEhnlwfe.js +0 -1
  115. package/server/client/dist/assets/AuxiliaryPanelHeader-B1k7pZpn.js +0 -1
  116. package/server/client/dist/assets/Calendar-BR83066I.js +0 -25
  117. package/server/client/dist/assets/Callout-BTd1BYtV.js +0 -1
  118. package/server/client/dist/assets/Copy.es-BoLjilxv.js +0 -1
  119. package/server/client/dist/assets/File.es-Ct7fscBg.js +0 -1
  120. package/server/client/dist/assets/Graph-DYUb7gQb.js +0 -46
  121. package/server/client/dist/assets/Graph-LY_IgMYy.js +0 -1
  122. package/server/client/dist/assets/Image.es-OYyN3BYt.js +0 -1
  123. package/server/client/dist/assets/Note-DnXDfDTk.js +0 -25
  124. package/server/client/dist/assets/PencilLine.es-0JpEgxFY.js +0 -1
  125. package/server/client/dist/assets/Plus.es-DRfLW_bI.js +0 -1
  126. package/server/client/dist/assets/Reminders-COr1OHvF.js +0 -1
  127. package/server/client/dist/assets/Search-BqPROwkA.js +0 -31
  128. package/server/client/dist/assets/SurfaceCard-t7pdfhEe.js +0 -1
  129. package/server/client/dist/assets/Tag-C4fNi3KQ.js +0 -1
  130. package/server/client/dist/assets/TagNotes-BXgkwXMd.js +0 -1
  131. package/server/client/dist/assets/Trash.es-Bo1VyK9Q.js +0 -1
  132. package/server/client/dist/assets/ViewChip-DlCkL4z4.js +0 -1
  133. package/server/client/dist/assets/ViewNotes-BJPiV66I.js +0 -1
  134. package/server/client/dist/assets/ViewSectionDialog-DhH8harC.js +0 -102
  135. package/server/client/dist/assets/Views-DMizFO-d.js +0 -1
  136. package/server/client/dist/assets/app-1lOWtkRA.js +0 -26
  137. package/server/client/dist/assets/app-BHmwRmwM.css +0 -1
  138. package/server/client/dist/assets/channel-CigOulR-.js +0 -1
  139. package/server/client/dist/assets/classDiagram-OUVF2IWQ-D13ISIOW.js +0 -1
  140. package/server/client/dist/assets/classDiagram-v2-EOCWNBFH-D13ISIOW.js +0 -1
  141. package/server/client/dist/assets/index-BBzpADtD.js +0 -1
  142. package/server/client/dist/assets/manage-image-azmo5iVw.js +0 -1
  143. package/server/client/dist/assets/manage-image-detail-ojV6kSsu.js +0 -1
  144. package/server/client/dist/assets/mcp-admin-adapter-PMmgPupc.js +0 -1
  145. package/server/client/dist/assets/note.api-D3oJmG3q.js +0 -356
  146. package/server/client/dist/assets/properties-BrNJLkxR.js +0 -1
  147. package/server/client/dist/assets/search-D-xKTPdO.js +0 -1
  148. package/server/client/dist/assets/search-admin-adapter-CJnJmL6w.js +0 -1
  149. package/server/client/dist/assets/swimlanesDiagram-G3AALYLV-DQnrq2ZC.js +0 -8
  150. package/server/client/dist/assets/time-D5LRMumI.js +0 -9
  151. package/server/client/dist/assets/trash-X_dnKA9T.js +0 -1
  152. package/server/client/dist/assets/useReminderMutate-BrGO9M0o.js +0 -82
@@ -1,7 +1,8 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { setImmediate as yieldToEventLoop } from "node:timers/promises";
3
5
  import { getLoadablePath } from "sqlite-vec";
4
- import sqlite3 from "sqlite3";
5
6
  const SEMANTIC_INDEX_SCHEMA_VERSION = 2;
6
7
  const SEARCH_DATABASE_SCHEMA_VERSION = 1;
7
8
  const SEARCH_DATABASE_SCHEMA_SQL = `
@@ -50,69 +51,27 @@ const openDatabase = async (filePath) => {
50
51
  if (filePath !== ":memory:") {
51
52
  await mkdir(path.dirname(filePath), { recursive: true });
52
53
  }
53
- const database = await new Promise((resolve, reject) => {
54
- const db = new sqlite3.Database(filePath, (error) => {
55
- if (error) {
56
- reject(error);
57
- return;
58
- }
59
- resolve(db);
60
- });
61
- });
62
- await new Promise((resolve, reject) => {
63
- database.loadExtension(getLoadablePath(), (error) => {
64
- if (error) {
65
- reject(error);
66
- return;
67
- }
68
- resolve();
69
- });
70
- });
71
- return database;
54
+ const database = new DatabaseSync(filePath, { allowExtension: true });
55
+ try {
56
+ database.loadExtension(getLoadablePath());
57
+ database.enableLoadExtension(false);
58
+ return database;
59
+ } catch (error) {
60
+ database.close();
61
+ throw error;
62
+ }
72
63
  };
73
64
  const run = (database, sql, parameters = []) => {
74
- return new Promise((resolve, reject) => {
75
- database.run(sql, parameters, function onRun(error) {
76
- if (error) {
77
- reject(error);
78
- return;
79
- }
80
- resolve(this);
81
- });
82
- });
65
+ return database.prepare(sql).run(...parameters);
83
66
  };
84
67
  const get = (database, sql, parameters = []) => {
85
- return new Promise((resolve, reject) => {
86
- database.get(sql, parameters, (error, row) => {
87
- if (error) {
88
- reject(error);
89
- return;
90
- }
91
- resolve(row);
92
- });
93
- });
68
+ return database.prepare(sql).get(...parameters);
94
69
  };
95
70
  const all = (database, sql, parameters = []) => {
96
- return new Promise((resolve, reject) => {
97
- database.all(sql, parameters, (error, rows) => {
98
- if (error) {
99
- reject(error);
100
- return;
101
- }
102
- resolve(rows);
103
- });
104
- });
71
+ return database.prepare(sql).all(...parameters);
105
72
  };
106
73
  const exec = (database, sql) => {
107
- return new Promise((resolve, reject) => {
108
- database.exec(sql, (error) => {
109
- if (error) {
110
- reject(error);
111
- return;
112
- }
113
- resolve();
114
- });
115
- });
74
+ database.exec(sql);
116
75
  };
117
76
  const initializeSearchDatabase = async (database) => {
118
77
  await exec(
@@ -134,19 +93,35 @@ const initializeSearchDatabase = async (database) => {
134
93
  await exec(database, `PRAGMA user_version = ${SEARCH_DATABASE_SCHEMA_VERSION};`);
135
94
  };
136
95
  const close = (database) => {
137
- return new Promise((resolve, reject) => {
138
- database.close((error) => {
139
- if (error) {
140
- reject(error);
141
- return;
142
- }
143
- resolve();
144
- });
145
- });
96
+ database.close();
146
97
  };
147
98
  const serializeEmbedding = (embedding) => {
148
99
  return Buffer.from(Float32Array.from(embedding).buffer);
149
100
  };
101
+ const INSERT_YIELD_INTERVAL = 250;
102
+ const insertChunks = async (database, chunks) => {
103
+ const statement = database.prepare(`
104
+ INSERT INTO search_chunks (
105
+ note_id,
106
+ chunk_index,
107
+ source_hash,
108
+ text,
109
+ embedding
110
+ ) VALUES (?, ?, ?, ?, ?)
111
+ `);
112
+ for (const [index, chunk] of chunks.entries()) {
113
+ statement.run(
114
+ chunk.noteId,
115
+ chunk.chunkIndex,
116
+ chunk.sourceHash,
117
+ chunk.text,
118
+ serializeEmbedding(chunk.embedding)
119
+ );
120
+ if ((index + 1) % INSERT_YIELD_INTERVAL === 0 && index + 1 < chunks.length) {
121
+ await yieldToEventLoop();
122
+ }
123
+ }
124
+ };
150
125
  const isSemanticIndexProfile = (value) => {
151
126
  if (!value || typeof value !== "object") {
152
127
  return false;
@@ -217,27 +192,7 @@ class SqliteSemanticVectorIndex {
217
192
  try {
218
193
  await run(database, "DELETE FROM search_chunks");
219
194
  await run(database, "DELETE FROM search_index_state");
220
- for (const chunk of chunks) {
221
- await run(
222
- database,
223
- `
224
- INSERT INTO search_chunks (
225
- note_id,
226
- chunk_index,
227
- source_hash,
228
- text,
229
- embedding
230
- ) VALUES (?, ?, ?, ?, ?)
231
- `,
232
- [
233
- chunk.noteId,
234
- chunk.chunkIndex,
235
- chunk.sourceHash,
236
- chunk.text,
237
- serializeEmbedding(chunk.embedding)
238
- ]
239
- );
240
- }
195
+ await insertChunks(database, chunks);
241
196
  await run(
242
197
  database,
243
198
  `
@@ -472,37 +427,35 @@ class SqliteSemanticVectorIndex {
472
427
  async getNoteSyncQueueStatus() {
473
428
  return this.runExclusive(async () => {
474
429
  const database = await this.getDatabase();
475
- const [queue, state, latestError] = await Promise.all([
476
- get(
477
- database,
478
- `
479
- SELECT
480
- COUNT(*) AS pendingNoteCount,
481
- MIN(first_queued_at) AS oldestQueuedAt
482
- FROM search_note_sync_queue
483
- `
484
- ),
485
- get(
486
- database,
487
- `
488
- SELECT
489
- last_synced_at AS lastSyncedAt,
490
- last_reconciled_at AS lastReconciledAt
491
- FROM search_note_sync_state
492
- WHERE id = 1
493
- `
494
- ),
495
- get(
496
- database,
497
- `
498
- SELECT last_error AS error
499
- FROM search_note_sync_queue
500
- WHERE last_error IS NOT NULL
501
- ORDER BY next_attempt_at DESC, note_id ASC
502
- LIMIT 1
503
- `
504
- )
505
- ]);
430
+ const queue = get(
431
+ database,
432
+ `
433
+ SELECT
434
+ COUNT(*) AS pendingNoteCount,
435
+ MIN(first_queued_at) AS oldestQueuedAt
436
+ FROM search_note_sync_queue
437
+ `
438
+ );
439
+ const state = get(
440
+ database,
441
+ `
442
+ SELECT
443
+ last_synced_at AS lastSyncedAt,
444
+ last_reconciled_at AS lastReconciledAt
445
+ FROM search_note_sync_state
446
+ WHERE id = 1
447
+ `
448
+ );
449
+ const latestError = get(
450
+ database,
451
+ `
452
+ SELECT last_error AS error
453
+ FROM search_note_sync_queue
454
+ WHERE last_error IS NOT NULL
455
+ ORDER BY next_attempt_at DESC, note_id ASC
456
+ LIMIT 1
457
+ `
458
+ );
506
459
  return {
507
460
  pendingNoteCount: queue?.pendingNoteCount ?? 0,
508
461
  oldestQueuedAt: queue?.oldestQueuedAt === null || queue?.oldestQueuedAt === void 0 ? null : new Date(queue.oldestQueuedAt).toISOString(),
@@ -526,27 +479,7 @@ class SqliteSemanticVectorIndex {
526
479
  await exec(database, "BEGIN IMMEDIATE;");
527
480
  try {
528
481
  await run(database, "DELETE FROM search_chunks WHERE note_id = ?", [noteId]);
529
- for (const chunk of chunks) {
530
- await run(
531
- database,
532
- `
533
- INSERT INTO search_chunks (
534
- note_id,
535
- chunk_index,
536
- source_hash,
537
- text,
538
- embedding
539
- ) VALUES (?, ?, ?, ?, ?)
540
- `,
541
- [
542
- chunk.noteId,
543
- chunk.chunkIndex,
544
- chunk.sourceHash,
545
- chunk.text,
546
- serializeEmbedding(chunk.embedding)
547
- ]
548
- );
549
- }
482
+ await insertChunks(database, chunks);
550
483
  await this.refreshIndexCounts(database);
551
484
  await exec(database, "COMMIT;");
552
485
  } catch (error) {
@@ -2,6 +2,7 @@ import { GraphQLError } from "graphql";
2
2
  import { InvalidNotePropertyInputError } from "../../../features/note/services/properties.js";
3
3
  import {
4
4
  getNotesByProperties,
5
+ getViewSectionBoardColumn,
5
6
  getViewSectionById,
6
7
  getViewSectionNotes,
7
8
  getViewWorkspace
@@ -18,17 +19,51 @@ const viewQueryResolvers = {
18
19
  pagination = {
19
20
  limit: 25,
20
21
  offset: 0
21
- }
22
+ },
23
+ sortBy,
24
+ sortOrder
22
25
  }) => {
23
- const sectionNotes = await getViewSectionNotes(id, {
24
- limit: Number(pagination.limit),
25
- offset: Number(pagination.offset)
26
- });
26
+ const sectionNotes = await getViewSectionNotes(
27
+ id,
28
+ {
29
+ limit: Number(pagination.limit),
30
+ offset: Number(pagination.offset)
31
+ },
32
+ { sortBy, sortOrder }
33
+ );
27
34
  if (!sectionNotes) {
28
35
  throw "NOT FOUND";
29
36
  }
30
37
  return sectionNotes;
31
38
  },
39
+ viewSectionBoardColumn: async (_, {
40
+ id,
41
+ optionValue = null,
42
+ pagination = {
43
+ limit: 5,
44
+ offset: 0
45
+ }
46
+ }) => {
47
+ try {
48
+ const columnNotes = await getViewSectionBoardColumn(id, optionValue, {
49
+ limit: Number(pagination.limit),
50
+ offset: Number(pagination.offset)
51
+ });
52
+ if (!columnNotes) {
53
+ throw "NOT FOUND";
54
+ }
55
+ return columnNotes;
56
+ } catch (error) {
57
+ if (error instanceof InvalidNotePropertyInputError) {
58
+ throw new GraphQLError(error.message, {
59
+ extensions: {
60
+ code: "INVALID_NOTE_PROPERTY_INPUT"
61
+ }
62
+ });
63
+ }
64
+ throw error;
65
+ }
66
+ },
32
67
  notesByProperties: async (_, {
33
68
  input,
34
69
  pagination = {
@@ -37,11 +37,25 @@ const viewType = gql`
37
37
 
38
38
  type ViewDisplayOptions {
39
39
  tableColumns: [ViewTableColumn!]!
40
+ tablePropertyKeys: [String!]!
41
+ boardGroupByPropertyKey: String
42
+ }
43
+
44
+ type ViewBoardNote {
45
+ id: ID!
46
+ title: String!
47
+ updatedAt: String!
48
+ }
49
+
50
+ type ViewBoardColumnNotes {
51
+ totalCount: Int!
52
+ notes: [ViewBoardNote!]!
40
53
  }
41
54
 
42
55
  enum ViewDisplayType {
43
56
  list
44
57
  table
58
+ board
45
59
  calendar
46
60
  }
47
61
 
@@ -84,6 +98,8 @@ const viewType = gql`
84
98
 
85
99
  input ViewDisplayOptionsInput {
86
100
  tableColumns: [ViewTableColumn!]
101
+ tablePropertyKeys: [String!]
102
+ boardGroupByPropertyKey: String
87
103
  }
88
104
 
89
105
  input ViewSectionInput {
@@ -110,7 +126,13 @@ const viewQuery = gql`
110
126
  type Query {
111
127
  viewWorkspace: ViewWorkspace!
112
128
  viewSection(id: ID!): ViewSection
113
- viewSectionNotes(id: ID!, pagination: PaginationInput): Notes!
129
+ viewSectionNotes(
130
+ id: ID!
131
+ pagination: PaginationInput
132
+ sortBy: ViewSortBy
133
+ sortOrder: ViewSortOrder
134
+ ): Notes!
135
+ viewSectionBoardColumn(id: ID!, optionValue: String, pagination: PaginationInput): ViewBoardColumnNotes!
114
136
  notesByProperties(input: NotesByPropertiesInput!, pagination: PaginationInput): Notes!
115
137
  }
116
138
  `;
@@ -12,6 +12,7 @@ const MAX_VIEW_SECTION_LIMIT = 20;
12
12
  const DEFAULT_VIEW_NOTES_QUERY_LIMIT = 20;
13
13
  const MAX_VIEW_NOTES_QUERY_LIMIT = 50;
14
14
  const MAX_VIEW_PROPERTY_FILTERS = 10;
15
+ const MAX_VIEW_TABLE_PROPERTY_COLUMNS = 6;
15
16
  const DEFAULT_VIEW_DISPLAY_TYPE = "list";
16
17
  const DEFAULT_VIEW_SORT_BY = "updatedAt";
17
18
  const DEFAULT_VIEW_SORT_ORDER = "desc";
@@ -54,7 +55,7 @@ const isViewTableColumn = (value) => {
54
55
  return value === "title" || value === "tags" || value === "properties" || value === "createdAt" || value === "updatedAt";
55
56
  };
56
57
  const normalizeViewDisplayType = (value) => {
57
- if (value === "table" || value === "calendar") {
58
+ if (value === "table" || value === "board" || value === "calendar") {
58
59
  return value;
59
60
  }
60
61
  return DEFAULT_VIEW_DISPLAY_TYPE;
@@ -73,9 +74,12 @@ const normalizeViewTableColumns = (columns) => {
73
74
  }
74
75
  return uniqueColumns.includes("title") ? uniqueColumns : ["title", ...uniqueColumns];
75
76
  };
77
+ const normalizeViewTablePropertyKeys = (keys) => Array.from(new Set((keys ?? []).map(normalizePropertyKey))).slice(0, MAX_VIEW_TABLE_PROPERTY_COLUMNS);
76
78
  const normalizeViewDisplayOptions = (options) => {
77
79
  return {
78
- tableColumns: normalizeViewTableColumns(options?.tableColumns)
80
+ tableColumns: normalizeViewTableColumns(options?.tableColumns),
81
+ tablePropertyKeys: normalizeViewTablePropertyKeys(options?.tablePropertyKeys),
82
+ boardGroupByPropertyKey: options?.boardGroupByPropertyKey ? normalizePropertyKey(options.boardGroupByPropertyKey) : null
79
83
  };
80
84
  };
81
85
  const normalizeFilterValue = ({
@@ -207,9 +211,16 @@ const normalizeViewSectionInput = (input) => {
207
211
  const sortBy = normalizeViewSortBy(input.sortBy);
208
212
  const sortOrder = normalizeViewSortOrder(input.sortOrder);
209
213
  const displayOptions = normalizeViewDisplayOptions(input.displayOptions);
214
+ const displayType = normalizeViewDisplayType(input.displayType);
215
+ if (displayType === "board" && !displayOptions.boardGroupByPropertyKey) {
216
+ throw new InvalidNotePropertyInputError("Board sections require a grouping property.");
217
+ }
218
+ if (displayType === "board" && propertyFilters.some((filter) => filter.key === displayOptions.boardGroupByPropertyKey)) {
219
+ throw new InvalidNotePropertyInputError("A board grouping property cannot also be used as a section filter.");
220
+ }
210
221
  return {
211
222
  title: trimmedTitle || buildDefaultSectionTitle(tagNames, propertyFilters),
212
- displayType: normalizeViewDisplayType(input.displayType),
223
+ displayType,
213
224
  displayOptions,
214
225
  tagNames,
215
226
  mode: input.mode === "or" ? "or" : "and",
@@ -358,6 +369,23 @@ const hydratePropertyFilters = async (db, filters, options = {}) => {
358
369
  };
359
370
  const buildStoredViewQuery = async (db, section) => {
360
371
  const propertyFilters = await hydratePropertyFilters(db, section.propertyFilters, { validateSelectOptions: true });
372
+ if (section.displayType === "board") {
373
+ const groupingProperty = await db.propertyDefinition.findUnique({
374
+ where: { key: section.displayOptions.boardGroupByPropertyKey ?? "" },
375
+ select: { valueType: true, options: { select: { id: true }, take: 1 } }
376
+ });
377
+ if (!groupingProperty) {
378
+ throw new InvalidNotePropertyInputError(
379
+ `Property ${section.displayOptions.boardGroupByPropertyKey ?? ""} is not defined.`
380
+ );
381
+ }
382
+ if (groupingProperty.valueType !== "select") {
383
+ throw new InvalidNotePropertyInputError("Board sections can only group by a select property.");
384
+ }
385
+ if (groupingProperty.options.length === 0) {
386
+ throw new InvalidNotePropertyInputError("Board grouping properties require at least one option.");
387
+ }
388
+ }
361
389
  return serializeStoredViewQuery({
362
390
  propertyFilters,
363
391
  sortBy: section.sortBy,
@@ -521,13 +549,26 @@ const buildViewNotesWhere = (query) => {
521
549
  const buildViewSectionWhere = (section) => {
522
550
  return buildViewNotesWhere(section);
523
551
  };
552
+ const buildViewBoardColumnWhere = (section, propertyDefinitionId, optionId) => {
553
+ const sectionWhere = buildViewSectionWhere(section);
554
+ const columnWhere = optionId === null ? {
555
+ properties: {
556
+ none: { propertyDefinitionId }
557
+ }
558
+ } : {
559
+ properties: {
560
+ some: { propertyDefinitionId, optionId }
561
+ }
562
+ };
563
+ return Object.keys(sectionWhere).length === 0 ? columnWhere : { AND: [sectionWhere, columnWhere] };
564
+ };
524
565
  const buildViewNotesOrderBy = (query) => {
525
566
  return [{ [query.sortBy]: query.sortOrder }, { id: "asc" }];
526
567
  };
527
568
  const buildViewSectionOrderBy = (section) => {
528
569
  return buildViewNotesOrderBy(section);
529
570
  };
530
- const getViewSectionNotes = async (id, pagination) => {
571
+ const getViewSectionNotes = async (id, pagination, sort = {}) => {
531
572
  const section = await models.viewSection.findUnique({
532
573
  where: { id: parseViewId(id) },
533
574
  include: orderedViewSectionTagsInclude
@@ -537,10 +578,14 @@ const getViewSectionNotes = async (id, pagination) => {
537
578
  }
538
579
  const serializedSection = serializeViewSection(section);
539
580
  const where = buildViewSectionWhere(serializedSection);
581
+ const effectiveSort = {
582
+ sortBy: sort.sortBy ?? serializedSection.sortBy,
583
+ sortOrder: sort.sortOrder ?? serializedSection.sortOrder
584
+ };
540
585
  const [totalCount, notes] = await Promise.all([
541
586
  models.note.count({ where }),
542
587
  models.note.findMany({
543
- orderBy: buildViewSectionOrderBy(serializedSection),
588
+ orderBy: buildViewNotesOrderBy(effectiveSort),
544
589
  where,
545
590
  take: Number(pagination.limit),
546
591
  skip: Number(pagination.offset)
@@ -551,6 +596,61 @@ const getViewSectionNotes = async (id, pagination) => {
551
596
  notes
552
597
  };
553
598
  };
599
+ const getViewSectionBoardColumn = async (id, optionValue, pagination) => {
600
+ const section = await models.viewSection.findUnique({
601
+ where: { id: parseViewId(id) },
602
+ include: orderedViewSectionTagsInclude
603
+ });
604
+ if (!section) {
605
+ return null;
606
+ }
607
+ const serializedSection = serializeViewSection(section);
608
+ if (serializedSection.displayType !== "board") {
609
+ throw new InvalidNotePropertyInputError("This section is not configured as a board.");
610
+ }
611
+ const groupingPropertyKey = serializedSection.displayOptions.boardGroupByPropertyKey;
612
+ if (!groupingPropertyKey) {
613
+ throw new InvalidNotePropertyInputError("Board sections require a grouping property.");
614
+ }
615
+ const groupingProperty = await models.propertyDefinition.findUnique({
616
+ where: { key: groupingPropertyKey },
617
+ select: {
618
+ id: true,
619
+ valueType: true,
620
+ options: {
621
+ orderBy: [{ order: "asc" }, { id: "asc" }],
622
+ select: { id: true, value: true }
623
+ }
624
+ }
625
+ });
626
+ if (!groupingProperty || groupingProperty.valueType !== "select") {
627
+ throw new InvalidNotePropertyInputError("The board grouping property is unavailable.");
628
+ }
629
+ const normalizedOptionValue = optionValue === null ? null : normalizeSelectFilterValue(optionValue);
630
+ const option = normalizedOptionValue === null ? null : groupingProperty.options.find((candidate) => candidate.value === normalizedOptionValue);
631
+ if (normalizedOptionValue !== null && !option) {
632
+ throw new InvalidNotePropertyInputError(
633
+ `Property ${groupingPropertyKey} option ${normalizedOptionValue} is not defined.`
634
+ );
635
+ }
636
+ const normalizedPagination = normalizeViewNotesPagination(pagination);
637
+ const where = buildViewBoardColumnWhere(serializedSection, groupingProperty.id, option?.id ?? null);
638
+ const [totalCount, notes] = await Promise.all([
639
+ models.note.count({ where }),
640
+ models.note.findMany({
641
+ orderBy: buildViewSectionOrderBy(serializedSection),
642
+ where,
643
+ take: normalizedPagination.limit,
644
+ skip: normalizedPagination.offset,
645
+ select: {
646
+ id: true,
647
+ title: true,
648
+ updatedAt: true
649
+ }
650
+ })
651
+ ]);
652
+ return { totalCount, notes };
653
+ };
554
654
  const getNotesByPropertiesWithDb = async (db, input, pagination) => {
555
655
  const normalizedQuery = normalizeViewNotesQueryInput(input);
556
656
  if (normalizedQuery.propertyFilters.length === 0) {
@@ -786,6 +886,7 @@ export {
786
886
  MAX_VIEW_SECTION_LIMIT,
787
887
  MIN_VIEW_SECTION_LIMIT,
788
888
  buildPropertyFilterWhere,
889
+ buildViewBoardColumnWhere,
789
890
  buildViewNotesWhere,
790
891
  buildViewSectionWhere,
791
892
  clampViewSectionLimit,
@@ -795,6 +896,7 @@ export {
795
896
  deleteViewTab,
796
897
  getNotesByProperties,
797
898
  getNotesByPropertiesWithDb,
899
+ getViewSectionBoardColumn,
798
900
  getViewSectionById,
799
901
  getViewSectionNotes,
800
902
  getViewWorkspace,
@@ -806,6 +908,7 @@ export {
806
908
  normalizeViewSectionInput,
807
909
  normalizeViewTabTitle,
808
910
  normalizeViewTableColumns,
911
+ normalizeViewTablePropertyKeys,
809
912
  normalizeViewTagNames,
810
913
  pickNextActiveViewTabId,
811
914
  reorderViewSections,
@@ -1,38 +1,4 @@
1
- import { createApp } from "./app.js";
2
- import { ensureNoteReferenceIndex } from "./features/note/services/note-reference-index.js";
3
- import { getDefaultSemanticSearchManager } from "./features/search/search-manager.js";
4
- import { logAuthConfig, resolveAuthConfig } from "./modules/auth-mode.js";
5
- import { startDataMaintenanceScheduler } from "./modules/data-maintenance.js";
6
- const PORT = Number(process.env.PORT || 6683);
7
- const HOST = process.env.HOST || "0.0.0.0";
8
- const startServer = async () => {
9
- const authConfig = resolveAuthConfig(process.env);
10
- logAuthConfig(authConfig);
11
- const app = createApp(authConfig);
12
- getDefaultSemanticSearchManager();
13
- const rebuiltReferenceCount = await ensureNoteReferenceIndex();
14
- if (rebuiltReferenceCount > 0) {
15
- process.stdout.write(`[maintenance] Rebuilt ${rebuiltReferenceCount} note reference rows
16
- `);
17
- }
18
- app.listen(PORT, HOST, () => {
19
- process.stdout.write(`http server listen on ${HOST}:${PORT} (auth: ${authConfig.mode})
20
- `);
21
- startDataMaintenanceScheduler({
22
- onResults: (results) => {
23
- for (const result of results) {
24
- process.stdout.write(`[maintenance] Reconciled ${result.processedCount} rows for ${result.key}
25
- `);
26
- }
27
- },
28
- onError: (error) => {
29
- const message = error instanceof Error ? error.message : "Unknown data maintenance error";
30
- process.stderr.write(`[maintenance] Background run failed: ${message}
31
- `);
32
- }
33
- });
34
- });
35
- };
1
+ import { startServer } from "./server.js";
36
2
  startServer().catch((error) => {
37
3
  const message = error instanceof Error ? error.message : "Unknown auth configuration error";
38
4
  process.stderr.write(`[auth] Startup failed: ${message}
@@ -83,7 +83,12 @@ const createApiRouter = (authConfig, mcpAdminService) => {
83
83
  requireSession,
84
84
  csrfProtection,
85
85
  useAsync(createSearchAdminReindexHandler())
86
- ).post("/image", sessionAccessRateLimit, requireSession, csrfProtection, useAsync(createUploadImageHandler())).get("/events", sessionAccessRateLimit, requireSession, csrfProtection, createServerEventsHandler());
86
+ ).post("/image", sessionAccessRateLimit, requireSession, csrfProtection, useAsync(createUploadImageHandler())).get("/events", sessionAccessRateLimit, requireSession, csrfProtection, createServerEventsHandler()).use((_req, res) => {
87
+ res.status(404).json({
88
+ code: "API_ROUTE_NOT_FOUND",
89
+ message: "The requested API route was not found."
90
+ }).end();
91
+ });
87
92
  };
88
93
  export {
89
94
  createApiRouter
@@ -4,6 +4,9 @@ import path from "path";
4
4
  import { createCsrfProtection, isAuthenticatedRequest } from "../modules/auth-guard.js";
5
5
  import { paths } from "../paths.js";
6
6
  const IMAGE_ASSET_RATE_LIMIT_MESSAGE = "Too many image asset requests. Please try again later.";
7
+ const isClientDocumentRequest = (req) => {
8
+ return req.method === "GET" && Boolean(req.headers.accept?.includes("text/html")) && path.extname(req.path) === "";
9
+ };
7
10
  const shouldBlockClientRoute = (authConfig, requestPath, authenticated) => {
8
11
  if (authConfig.mode !== "password" || authenticated) {
9
12
  return false;
@@ -52,7 +55,17 @@ const createClientRouteCsrfTokenMiddleware = (authConfig) => {
52
55
  csrfProtection(req, res, next);
53
56
  };
54
57
  };
55
- const createClientRouter = (authConfig) => Router().use(
58
+ const createProductionClientContentRouter = () => Router().use(express.static(paths.clientDist, { extensions: ["html"] })).get(/.*/, (req, res, next) => {
59
+ if (!isClientDocumentRequest(req)) {
60
+ next();
61
+ return;
62
+ }
63
+ res.sendFile("index.html", { root: paths.clientDist });
64
+ }).use((_req, res) => {
65
+ res.setHeader("X-Content-Type-Options", "nosniff");
66
+ res.status(404).end();
67
+ });
68
+ const createClientRouter = (authConfig, clientContentMiddleware = createProductionClientContentRouter()) => Router().use(
56
69
  "/assets/images",
57
70
  createImageAssetAuthRateLimit(authConfig),
58
71
  createProtectedImageAssetsMiddleware(authConfig),
@@ -78,9 +91,7 @@ const createClientRouter = (authConfig) => Router().use(
78
91
  return;
79
92
  }
80
93
  next();
81
- }).use(createClientRouteCsrfTokenMiddleware(authConfig)).use(express.static(paths.clientDist, { extensions: ["html"] })).get(/.*/, (_req, res) => {
82
- res.sendFile("index.html", { root: paths.clientDist });
83
- });
94
+ }).use(createClientRouteCsrfTokenMiddleware(authConfig)).use(clientContentMiddleware);
84
95
  export {
85
96
  createClientRouter
86
97
  };