2ndbrain 2026.1.31 → 2026.1.32
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/.claude/settings.local.json +2 -1
- package/package.json +1 -1
- package/src/db/migrate.js +1 -1
- package/src/web/server.js +241 -2
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"WebFetch(domain:api.x.ai)",
|
|
11
11
|
"WebFetch(domain:latenode.com)",
|
|
12
12
|
"Bash(claude:*)",
|
|
13
|
-
"Bash(printf:*)"
|
|
13
|
+
"Bash(printf:*)",
|
|
14
|
+
"Bash(ls -la \"c:\\\\dev\\\\fingerskier\\\\agent\\\\2ndbrain\\\\db\\\\migrations\"\" 2>/dev/null || echo \"No migrations directory \")"
|
|
14
15
|
]
|
|
15
16
|
}
|
|
16
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "2ndbrain",
|
|
3
|
-
"version": "2026.1.
|
|
3
|
+
"version": "2026.1.32",
|
|
4
4
|
"description": "Always-on Node.js service bridging Telegram messaging to Claude AI with knowledge graph, journal, project management, and semantic search.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/db/migrate.js
CHANGED
package/src/web/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createServer } from 'node:http';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { migrate, getMigrationFiles, ensureMigrationsTable } from '../db/migrate.js';
|
|
6
7
|
|
|
7
8
|
// Resolve project root (two directories up from src/web/)
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -31,7 +32,7 @@ const SETTINGS_FIELDS = [
|
|
|
31
32
|
section: 'Claude',
|
|
32
33
|
fields: [
|
|
33
34
|
{ key: 'CLAUDE_MODEL', label: 'Model', hint: 'Default: claude-sonnet-4-20250514' },
|
|
34
|
-
{ key: 'CLAUDE_THINKING', label: 'Thinking', hint: 'Enable extended thinking
|
|
35
|
+
{ key: 'CLAUDE_THINKING', label: 'Thinking', type: 'boolean', hint: 'Enable extended thinking' },
|
|
35
36
|
{ key: 'CLAUDE_TIMEOUT', label: 'Timeout (ms)', hint: 'Default: 120000' },
|
|
36
37
|
{ key: 'CLAUDE_MAX_BUDGET', label: 'Max Budget (USD)', hint: 'Max cost per invocation (e.g. 0.50)' },
|
|
37
38
|
],
|
|
@@ -74,7 +75,7 @@ const SETTINGS_FIELDS = [
|
|
|
74
75
|
fields: [
|
|
75
76
|
{ key: 'WEB_PORT', label: 'Port', hint: 'Default: 3000' },
|
|
76
77
|
{ key: 'WEB_BIND', label: 'Bind Address', hint: 'Default: 127.0.0.1' },
|
|
77
|
-
{ key: 'AUTO_OPEN_BROWSER', label: 'Auto Open Browser',
|
|
78
|
+
{ key: 'AUTO_OPEN_BROWSER', label: 'Auto Open Browser', type: 'boolean', hint: 'Default: true' },
|
|
78
79
|
],
|
|
79
80
|
},
|
|
80
81
|
{
|
|
@@ -127,6 +128,8 @@ class WebServer {
|
|
|
127
128
|
app.post('/settings', (req, res) => this._handleSaveSettings(req, res));
|
|
128
129
|
app.get('/logs', (req, res) => this._handleLogs(req, res));
|
|
129
130
|
app.get('/health', (req, res) => this._handleHealth(req, res));
|
|
131
|
+
app.get('/database', (req, res) => this._handleDatabase(req, res));
|
|
132
|
+
app.post('/database/migrate', (req, res) => this._handleRunMigrations(req, res));
|
|
130
133
|
|
|
131
134
|
// Start listening
|
|
132
135
|
const server = createServer(app);
|
|
@@ -310,6 +313,86 @@ class WebServer {
|
|
|
310
313
|
res.status(httpStatus).json(health);
|
|
311
314
|
}
|
|
312
315
|
|
|
316
|
+
// -----------------------------------------------------------------------
|
|
317
|
+
// Database page
|
|
318
|
+
// -----------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
async _handleDatabase(req, res) {
|
|
321
|
+
const data = {
|
|
322
|
+
dbAvailable: true,
|
|
323
|
+
migrations: { applied: [], pending: [], total: 0 },
|
|
324
|
+
tables: [],
|
|
325
|
+
dbVersion: '',
|
|
326
|
+
dbSize: '',
|
|
327
|
+
message: null,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (req.query.migrated) {
|
|
331
|
+
const count = parseInt(req.query.migrated, 10) || 0;
|
|
332
|
+
data.message = { type: 'success', text: `Successfully applied ${count} migration(s).` };
|
|
333
|
+
} else if (req.query.error) {
|
|
334
|
+
data.message = { type: 'error', text: req.query.error };
|
|
335
|
+
} else if (req.query.noop === '1') {
|
|
336
|
+
data.message = { type: 'success', text: 'No pending migrations to apply.' };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
await ensureMigrationsTable();
|
|
341
|
+
|
|
342
|
+
const appliedResult = await this._db.query(
|
|
343
|
+
'SELECT name, applied_at FROM schema_migrations ORDER BY name',
|
|
344
|
+
);
|
|
345
|
+
const appliedSet = new Set(appliedResult.rows.map((r) => r.name));
|
|
346
|
+
const allFiles = getMigrationFiles();
|
|
347
|
+
|
|
348
|
+
data.migrations.applied = appliedResult.rows;
|
|
349
|
+
data.migrations.pending = allFiles.filter((f) => !appliedSet.has(f));
|
|
350
|
+
data.migrations.total = allFiles.length;
|
|
351
|
+
|
|
352
|
+
const tablesResult = await this._db.query(`
|
|
353
|
+
SELECT
|
|
354
|
+
relname AS name,
|
|
355
|
+
n_live_tup AS row_count,
|
|
356
|
+
pg_size_pretty(pg_total_relation_size(c.oid)) AS size
|
|
357
|
+
FROM pg_class c
|
|
358
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
359
|
+
LEFT JOIN pg_stat_user_tables s ON s.relid = c.oid
|
|
360
|
+
WHERE n.nspname = 'public'
|
|
361
|
+
AND c.relkind = 'r'
|
|
362
|
+
ORDER BY pg_total_relation_size(c.oid) DESC
|
|
363
|
+
`);
|
|
364
|
+
data.tables = tablesResult.rows;
|
|
365
|
+
|
|
366
|
+
const versionResult = await this._db.query('SELECT version()');
|
|
367
|
+
data.dbVersion = versionResult.rows[0]?.version || '';
|
|
368
|
+
|
|
369
|
+
const sizeResult = await this._db.query(
|
|
370
|
+
'SELECT pg_size_pretty(pg_database_size(current_database())) AS size',
|
|
371
|
+
);
|
|
372
|
+
data.dbSize = sizeResult.rows[0]?.size || '';
|
|
373
|
+
} catch (err) {
|
|
374
|
+
data.dbAvailable = false;
|
|
375
|
+
this._logger.error('web', `Database page error: ${err.message}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
res.send(databaseHTML(data));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async _handleRunMigrations(_req, res) {
|
|
382
|
+
try {
|
|
383
|
+
const applied = await migrate();
|
|
384
|
+
if (applied.length === 0) {
|
|
385
|
+
res.redirect('/database?noop=1');
|
|
386
|
+
} else {
|
|
387
|
+
this._logger.info('web', `Applied ${applied.length} migration(s) via web admin.`);
|
|
388
|
+
res.redirect(`/database?migrated=${applied.length}`);
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
this._logger.error('web', `Migration failed via web admin: ${err.message}`);
|
|
392
|
+
res.redirect(`/database?error=${encodeURIComponent(err.message)}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
313
396
|
// -----------------------------------------------------------------------
|
|
314
397
|
// .env file helpers
|
|
315
398
|
// -----------------------------------------------------------------------
|
|
@@ -587,6 +670,24 @@ function layoutHTML(title, content) {
|
|
|
587
670
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
588
671
|
margin-bottom: 0.25rem;
|
|
589
672
|
}
|
|
673
|
+
.checkbox-group {
|
|
674
|
+
display: flex;
|
|
675
|
+
align-items: center;
|
|
676
|
+
gap: 0.5rem;
|
|
677
|
+
padding-top: 0.35rem;
|
|
678
|
+
}
|
|
679
|
+
.checkbox-group input[type="checkbox"] {
|
|
680
|
+
width: 1.1rem;
|
|
681
|
+
height: 1.1rem;
|
|
682
|
+
accent-color: #238636;
|
|
683
|
+
cursor: pointer;
|
|
684
|
+
}
|
|
685
|
+
.checkbox-label {
|
|
686
|
+
font-size: 0.85rem;
|
|
687
|
+
color: #8b949e;
|
|
688
|
+
padding-top: 0;
|
|
689
|
+
cursor: pointer;
|
|
690
|
+
}
|
|
590
691
|
button[type="submit"] {
|
|
591
692
|
background: #238636;
|
|
592
693
|
color: #fff;
|
|
@@ -680,6 +781,7 @@ function layoutHTML(title, content) {
|
|
|
680
781
|
<a href="/">Dashboard</a>
|
|
681
782
|
<a href="/settings">Settings</a>
|
|
682
783
|
<a href="/logs">Logs</a>
|
|
784
|
+
<a href="/database">Database</a>
|
|
683
785
|
</nav>
|
|
684
786
|
<div class="container">
|
|
685
787
|
${content}
|
|
@@ -808,6 +910,15 @@ function settingsHTML(config, message) {
|
|
|
808
910
|
value="" placeholder="Enter new value to change"
|
|
809
911
|
autocomplete="off" />
|
|
810
912
|
</div>`;
|
|
913
|
+
} else if (field.type === 'boolean') {
|
|
914
|
+
const checked = String(value).toLowerCase() === 'true' ? ' checked' : '';
|
|
915
|
+
inputArea = `
|
|
916
|
+
<div class="checkbox-group">
|
|
917
|
+
<input type="hidden" name="${esc(field.key)}" value="false" />
|
|
918
|
+
<input type="checkbox" id="${esc(field.key)}" name="${esc(field.key)}"
|
|
919
|
+
value="true"${checked} />
|
|
920
|
+
<label for="${esc(field.key)}" class="checkbox-label">Enabled</label>
|
|
921
|
+
</div>`;
|
|
811
922
|
} else {
|
|
812
923
|
inputArea = `<input type="text" name="${esc(field.key)}"
|
|
813
924
|
value="${esc(String(value))}" />`;
|
|
@@ -872,6 +983,134 @@ function logsHTML(logs, activeLevel) {
|
|
|
872
983
|
`);
|
|
873
984
|
}
|
|
874
985
|
|
|
986
|
+
/**
|
|
987
|
+
* Database admin page showing migration status and table metadata.
|
|
988
|
+
*/
|
|
989
|
+
function databaseHTML(data) {
|
|
990
|
+
const alert = data.message
|
|
991
|
+
? `<div class="alert alert-${esc(data.message.type)}">${esc(data.message.text)}</div>`
|
|
992
|
+
: '';
|
|
993
|
+
|
|
994
|
+
const dbWarn = data.dbAvailable
|
|
995
|
+
? ''
|
|
996
|
+
: '<div class="db-warn">Database is unavailable. Cannot retrieve database information.</div>';
|
|
997
|
+
|
|
998
|
+
const stats = data.dbAvailable
|
|
999
|
+
? `
|
|
1000
|
+
<div class="grid">
|
|
1001
|
+
<div class="card">
|
|
1002
|
+
<div class="stat-label">Total Migrations</div>
|
|
1003
|
+
<div class="stat-value">${data.migrations.total}</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
<div class="card">
|
|
1006
|
+
<div class="stat-label">Applied</div>
|
|
1007
|
+
<div class="stat-value" style="color:#3fb950;">${data.migrations.applied.length}</div>
|
|
1008
|
+
</div>
|
|
1009
|
+
<div class="card">
|
|
1010
|
+
<div class="stat-label">Pending</div>
|
|
1011
|
+
<div class="stat-value"${data.migrations.pending.length > 0 ? ' style="color:#d29922;"' : ''}>${data.migrations.pending.length}</div>
|
|
1012
|
+
</div>
|
|
1013
|
+
<div class="card">
|
|
1014
|
+
<div class="stat-label">Database Size</div>
|
|
1015
|
+
<div class="stat-value">${esc(data.dbSize)}</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
</div>`
|
|
1018
|
+
: '';
|
|
1019
|
+
|
|
1020
|
+
const versionSection = data.dbVersion
|
|
1021
|
+
? `<p class="muted" style="margin-bottom:1rem;font-size:0.82rem;">${esc(data.dbVersion)}</p>`
|
|
1022
|
+
: '';
|
|
1023
|
+
|
|
1024
|
+
let pendingSection = '';
|
|
1025
|
+
if (data.dbAvailable && data.migrations.pending.length > 0) {
|
|
1026
|
+
const pendingRows = data.migrations.pending
|
|
1027
|
+
.map(
|
|
1028
|
+
(name) => `
|
|
1029
|
+
<tr>
|
|
1030
|
+
<td><span class="badge badge-warn">pending</span></td>
|
|
1031
|
+
<td style="font-family:monospace;">${esc(name)}</td>
|
|
1032
|
+
<td class="muted">--</td>
|
|
1033
|
+
</tr>`,
|
|
1034
|
+
)
|
|
1035
|
+
.join('');
|
|
1036
|
+
|
|
1037
|
+
pendingSection = `
|
|
1038
|
+
<h2>Pending Migrations</h2>
|
|
1039
|
+
<div class="card" style="overflow-x:auto;">
|
|
1040
|
+
<table>
|
|
1041
|
+
<thead><tr><th>Status</th><th>Migration</th><th>Applied At</th></tr></thead>
|
|
1042
|
+
<tbody>${pendingRows}</tbody>
|
|
1043
|
+
</table>
|
|
1044
|
+
<form method="POST" action="/database/migrate"
|
|
1045
|
+
onsubmit="return confirm('Run ${data.migrations.pending.length} pending migration(s)?\\n\\nThis will modify the database schema.');"
|
|
1046
|
+
style="margin-top:1rem;">
|
|
1047
|
+
<button type="submit" style="background:#d29922;">Run ${data.migrations.pending.length} Pending Migration${data.migrations.pending.length === 1 ? '' : 's'}</button>
|
|
1048
|
+
</form>
|
|
1049
|
+
</div>`;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
let appliedSection = '';
|
|
1053
|
+
if (data.dbAvailable && data.migrations.applied.length > 0) {
|
|
1054
|
+
const appliedRows = data.migrations.applied
|
|
1055
|
+
.map(
|
|
1056
|
+
(m) => `
|
|
1057
|
+
<tr>
|
|
1058
|
+
<td><span class="badge badge-ok">applied</span></td>
|
|
1059
|
+
<td style="font-family:monospace;">${esc(m.name)}</td>
|
|
1060
|
+
<td>${fmtTime(m.applied_at)}</td>
|
|
1061
|
+
</tr>`,
|
|
1062
|
+
)
|
|
1063
|
+
.join('');
|
|
1064
|
+
|
|
1065
|
+
appliedSection = `
|
|
1066
|
+
<h2>Applied Migrations</h2>
|
|
1067
|
+
<div class="card" style="overflow-x:auto;">
|
|
1068
|
+
<table>
|
|
1069
|
+
<thead><tr><th>Status</th><th>Migration</th><th>Applied At</th></tr></thead>
|
|
1070
|
+
<tbody>${appliedRows}</tbody>
|
|
1071
|
+
</table>
|
|
1072
|
+
</div>`;
|
|
1073
|
+
} else if (data.dbAvailable && data.migrations.applied.length === 0) {
|
|
1074
|
+
appliedSection = `
|
|
1075
|
+
<h2>Applied Migrations</h2>
|
|
1076
|
+
<div class="card"><p class="empty">No migrations have been applied yet.</p></div>`;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
let tablesSection = '';
|
|
1080
|
+
if (data.dbAvailable && data.tables.length > 0) {
|
|
1081
|
+
const tableRows = data.tables
|
|
1082
|
+
.map(
|
|
1083
|
+
(t) => `
|
|
1084
|
+
<tr>
|
|
1085
|
+
<td style="font-family:monospace;">${esc(t.name)}</td>
|
|
1086
|
+
<td style="text-align:right;">${t.row_count ?? '--'}</td>
|
|
1087
|
+
<td style="text-align:right;">${esc(t.size)}</td>
|
|
1088
|
+
</tr>`,
|
|
1089
|
+
)
|
|
1090
|
+
.join('');
|
|
1091
|
+
|
|
1092
|
+
tablesSection = `
|
|
1093
|
+
<h2>Tables</h2>
|
|
1094
|
+
<div class="card" style="overflow-x:auto;">
|
|
1095
|
+
<table>
|
|
1096
|
+
<thead><tr><th>Table</th><th style="text-align:right;">Rows (est.)</th><th style="text-align:right;">Size</th></tr></thead>
|
|
1097
|
+
<tbody>${tableRows}</tbody>
|
|
1098
|
+
</table>
|
|
1099
|
+
</div>`;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return layoutHTML('Database', `
|
|
1103
|
+
<h1>Database</h1>
|
|
1104
|
+
${alert}
|
|
1105
|
+
${dbWarn}
|
|
1106
|
+
${versionSection}
|
|
1107
|
+
${stats}
|
|
1108
|
+
${pendingSection}
|
|
1109
|
+
${appliedSection}
|
|
1110
|
+
${tablesSection}
|
|
1111
|
+
`);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
875
1114
|
// ---------------------------------------------------------------------------
|
|
876
1115
|
// Export
|
|
877
1116
|
// ---------------------------------------------------------------------------
|