botnote 0.1.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.
- package/README.md +277 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +320 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/client.d.ts +10 -0
- package/dist/db/client.js +16 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/migrate.d.ts +1 -0
- package/dist/db/migrate.js +57 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrations/0001_init.sql +89 -0
- package/dist/db/migrations/0002_tasks_due_at.sql +16 -0
- package/dist/db/migrations/0003_task_priority_and_sequence.sql +45 -0
- package/dist/db/migrations/0004_pinned.sql +8 -0
- package/dist/db/migrations/0005_nullable_title_and_parent.sql +5 -0
- package/dist/db/migrations/0006_project_color_icon.sql +3 -0
- package/dist/db/migrations/0007_tokens.sql +14 -0
- package/dist/db/migrations/0008_simplify_kinds_drop_actors.sql +21 -0
- package/dist/db/migrations/0009_sessions.sql +14 -0
- package/dist/db/migrations/0010_completed_at.sql +17 -0
- package/dist/db/migrations/0011_completed_at_backfill_fix.sql +14 -0
- package/dist/db/migrations/0012_completed_at_backfill_due.sql +16 -0
- package/dist/db/migrations/0013_due_at_noon_utc.sql +13 -0
- package/dist/db/schema.d.ts +811 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/mcp/http-client.d.ts +154 -0
- package/dist/mcp/http-client.js +113 -0
- package/dist/mcp/http-client.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +492 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/rest/routes.d.ts +3 -0
- package/dist/rest/routes.js +320 -0
- package/dist/rest/routes.js.map +1 -0
- package/dist/rest/server.d.ts +12 -0
- package/dist/rest/server.js +282 -0
- package/dist/rest/server.js.map +1 -0
- package/dist/service/embedding.d.ts +25 -0
- package/dist/service/embedding.js +112 -0
- package/dist/service/embedding.js.map +1 -0
- package/dist/service/entities.d.ts +22 -0
- package/dist/service/entities.js +174 -0
- package/dist/service/entities.js.map +1 -0
- package/dist/service/index.d.ts +8 -0
- package/dist/service/index.js +8 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/opening_brief.d.ts +14 -0
- package/dist/service/opening_brief.js +104 -0
- package/dist/service/opening_brief.js.map +1 -0
- package/dist/service/projects.d.ts +8 -0
- package/dist/service/projects.js +36 -0
- package/dist/service/projects.js.map +1 -0
- package/dist/service/search.d.ts +15 -0
- package/dist/service/search.js +101 -0
- package/dist/service/search.js.map +1 -0
- package/dist/service/sessions.d.ts +25 -0
- package/dist/service/sessions.js +89 -0
- package/dist/service/sessions.js.map +1 -0
- package/dist/service/tasks.d.ts +23 -0
- package/dist/service/tasks.js +130 -0
- package/dist/service/tasks.js.map +1 -0
- package/dist/service/tokens.d.ts +18 -0
- package/dist/service/tokens.js +41 -0
- package/dist/service/tokens.js.map +1 -0
- package/dist/service/types.d.ts +294 -0
- package/dist/service/types.js +132 -0
- package/dist/service/types.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +73 -0
- package/web/dist/assets/index-Bddg7h_x.css +1 -0
- package/web/dist/assets/index-DYW6stVx.js +542 -0
- package/web/dist/favicon.svg +13 -0
- package/web/dist/index.html +17 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { inArray, sql } from "drizzle-orm";
|
|
2
|
+
import { entities as entitiesTable } from "../db/schema.js";
|
|
3
|
+
const RRF_K = 60;
|
|
4
|
+
const KIND_WEIGHTS = {
|
|
5
|
+
decision: 0.3,
|
|
6
|
+
memory: 0.1,
|
|
7
|
+
task: 0.05,
|
|
8
|
+
comment: -0.1,
|
|
9
|
+
log: -0.2
|
|
10
|
+
};
|
|
11
|
+
function rrfScore(rank) {
|
|
12
|
+
if (rank == null)
|
|
13
|
+
return 0;
|
|
14
|
+
return 1 / (RRF_K + rank);
|
|
15
|
+
}
|
|
16
|
+
export async function search(db, input, opts = {}) {
|
|
17
|
+
const projectFilter = input.projectId
|
|
18
|
+
? sql `AND project_id = ${input.projectId}::uuid`
|
|
19
|
+
: sql ``;
|
|
20
|
+
const kindFilter = input.kind ? sql `AND kind = ${input.kind}` : sql ``;
|
|
21
|
+
const fetchN = Math.max(input.limit * 4, 40);
|
|
22
|
+
const bm25Rows = (await db.execute(sql `
|
|
23
|
+
SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(body_tsv, q) DESC) AS rank_pos
|
|
24
|
+
FROM entities, websearch_to_tsquery('simple', ${input.query}) q
|
|
25
|
+
WHERE body_tsv @@ q ${projectFilter} ${kindFilter}
|
|
26
|
+
ORDER BY ts_rank_cd(body_tsv, q) DESC
|
|
27
|
+
LIMIT ${fetchN}
|
|
28
|
+
`)).rows;
|
|
29
|
+
const ilikeFallback = bm25Rows.length === 0
|
|
30
|
+
? (await db.execute(sql `
|
|
31
|
+
SELECT id,
|
|
32
|
+
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rank_pos
|
|
33
|
+
FROM entities
|
|
34
|
+
WHERE (title ILIKE ${`%${input.query}%`} OR body ILIKE ${`%${input.query}%`})
|
|
35
|
+
${projectFilter} ${kindFilter}
|
|
36
|
+
LIMIT ${fetchN}
|
|
37
|
+
`)).rows
|
|
38
|
+
: [];
|
|
39
|
+
let vecRows = [];
|
|
40
|
+
if (opts.queryEmbedding && opts.queryEmbedding.length > 0) {
|
|
41
|
+
const vecLiteral = `[${opts.queryEmbedding.join(",")}]`;
|
|
42
|
+
vecRows = (await db.execute(sql `
|
|
43
|
+
SELECT id, ROW_NUMBER() OVER (ORDER BY body_vec <=> ${vecLiteral}::vector) AS rank_pos
|
|
44
|
+
FROM entities
|
|
45
|
+
WHERE body_vec IS NOT NULL ${projectFilter} ${kindFilter}
|
|
46
|
+
ORDER BY body_vec <=> ${vecLiteral}::vector
|
|
47
|
+
LIMIT ${fetchN}
|
|
48
|
+
`)).rows;
|
|
49
|
+
}
|
|
50
|
+
const timeRows = (await db.execute(sql `
|
|
51
|
+
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rank_pos
|
|
52
|
+
FROM entities
|
|
53
|
+
WHERE 1=1 ${projectFilter} ${kindFilter}
|
|
54
|
+
ORDER BY created_at DESC
|
|
55
|
+
LIMIT ${fetchN}
|
|
56
|
+
`)).rows;
|
|
57
|
+
const bm25Map = new Map();
|
|
58
|
+
for (const r of bm25Rows.length > 0 ? bm25Rows : ilikeFallback) {
|
|
59
|
+
bm25Map.set(r.id, Number(r.rank_pos));
|
|
60
|
+
}
|
|
61
|
+
const vecMap = new Map();
|
|
62
|
+
for (const r of vecRows)
|
|
63
|
+
vecMap.set(r.id, Number(r.rank_pos));
|
|
64
|
+
const timeMap = new Map();
|
|
65
|
+
for (const r of timeRows)
|
|
66
|
+
timeMap.set(r.id, Number(r.rank_pos));
|
|
67
|
+
const ids = new Set([...bm25Map.keys(), ...vecMap.keys()]);
|
|
68
|
+
if (ids.size === 0)
|
|
69
|
+
return [];
|
|
70
|
+
const idArr = [...ids];
|
|
71
|
+
const entitiesRows = await db
|
|
72
|
+
.select()
|
|
73
|
+
.from(entitiesTable)
|
|
74
|
+
.where(inArray(entitiesTable.id, idArr));
|
|
75
|
+
const entityMap = new Map();
|
|
76
|
+
for (const r of entitiesRows)
|
|
77
|
+
entityMap.set(r.id, r);
|
|
78
|
+
const hits = [];
|
|
79
|
+
for (const id of idArr) {
|
|
80
|
+
const entity = entityMap.get(id);
|
|
81
|
+
if (!entity)
|
|
82
|
+
continue;
|
|
83
|
+
const bm25 = rrfScore(bm25Map.get(id));
|
|
84
|
+
const cosine = rrfScore(vecMap.get(id));
|
|
85
|
+
const timeDecay = rrfScore(timeMap.get(id)) * 0.3;
|
|
86
|
+
const kindWeight = KIND_WEIGHTS[entity.kind] ?? 0;
|
|
87
|
+
const score = bm25 + cosine + timeDecay + kindWeight * 0.01;
|
|
88
|
+
hits.push({
|
|
89
|
+
entity,
|
|
90
|
+
score,
|
|
91
|
+
components: {
|
|
92
|
+
bm25: bm25 || undefined,
|
|
93
|
+
cosine: cosine || undefined,
|
|
94
|
+
timeDecay: timeDecay || undefined
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
hits.sort((a, b) => b.score - a.score);
|
|
99
|
+
return hits.slice(0, input.limit);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/service/search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import type { Database } from "../db/client.js";
|
|
3
|
+
import { type Session } from "../db/schema.js";
|
|
4
|
+
/** Verify a candidate master password against BOTNOTE_PASSWORD env. Returns
|
|
5
|
+
* false (not throws) when the env is unset, so callers can degrade safely. */
|
|
6
|
+
export declare function verifyMasterPassword(candidate: string): boolean;
|
|
7
|
+
export interface CreatedSession {
|
|
8
|
+
session: Session;
|
|
9
|
+
plaintext: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function createSession(db: Database["db"], opts?: {
|
|
12
|
+
userAgent?: string | null;
|
|
13
|
+
ttlDays?: number;
|
|
14
|
+
}): Promise<CreatedSession>;
|
|
15
|
+
/** Look up a session by cookie plaintext. Returns the row if it exists and
|
|
16
|
+
* has not expired; otherwise null. Bumps last_used_at on hit. */
|
|
17
|
+
export declare function consumeSession(db: Database["db"], plaintext: string): Promise<Session | null>;
|
|
18
|
+
/** Revoke a single session by cookie plaintext. No-op when the cookie does
|
|
19
|
+
* not match any row. */
|
|
20
|
+
export declare function revokeSession(db: Database["db"], plaintext: string): Promise<boolean>;
|
|
21
|
+
/** Periodic cleanup — called on daemon start. Deletes expired rows so the
|
|
22
|
+
* table doesn't bloat over months. */
|
|
23
|
+
export declare function pruneExpiredSessions(db: Database["db"]): Promise<number>;
|
|
24
|
+
/** Drizzle helper to noop a linter complaint when sql is unused. */
|
|
25
|
+
export declare const _sql: typeof sql;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { and, eq, gt, lt, sql } from "drizzle-orm";
|
|
3
|
+
import { sessions } from "../db/schema.js";
|
|
4
|
+
const DEFAULT_TTL_DAYS = 30;
|
|
5
|
+
function hashToken(plaintext) {
|
|
6
|
+
return createHash("sha256").update(plaintext).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
/** Constant-time equality on UTF-8 strings of arbitrary length. */
|
|
9
|
+
function constantTimeEqual(a, b) {
|
|
10
|
+
const ab = Buffer.from(a, "utf8");
|
|
11
|
+
const bb = Buffer.from(b, "utf8");
|
|
12
|
+
// Pad to equal length so timingSafeEqual doesn't throw. We compare lengths
|
|
13
|
+
// separately — if lengths differ, the result is false. Both branches still
|
|
14
|
+
// do a fixed-time compare of the same number of bytes.
|
|
15
|
+
const len = Math.max(ab.length, bb.length);
|
|
16
|
+
const a2 = Buffer.alloc(len);
|
|
17
|
+
const b2 = Buffer.alloc(len);
|
|
18
|
+
ab.copy(a2);
|
|
19
|
+
bb.copy(b2);
|
|
20
|
+
const same = timingSafeEqual(a2, b2);
|
|
21
|
+
return same && ab.length === bb.length;
|
|
22
|
+
}
|
|
23
|
+
/** Verify a candidate master password against BOTNOTE_PASSWORD env. Returns
|
|
24
|
+
* false (not throws) when the env is unset, so callers can degrade safely. */
|
|
25
|
+
export function verifyMasterPassword(candidate) {
|
|
26
|
+
const expected = process.env.BOTNOTE_PASSWORD;
|
|
27
|
+
if (!expected)
|
|
28
|
+
return false;
|
|
29
|
+
if (typeof candidate !== "string" || candidate.length === 0)
|
|
30
|
+
return false;
|
|
31
|
+
return constantTimeEqual(candidate, expected);
|
|
32
|
+
}
|
|
33
|
+
export async function createSession(db, opts = {}) {
|
|
34
|
+
const ttlDays = opts.ttlDays ?? DEFAULT_TTL_DAYS;
|
|
35
|
+
const plaintext = randomBytes(32).toString("hex");
|
|
36
|
+
const tokenHash = hashToken(plaintext);
|
|
37
|
+
const expiresAt = new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000);
|
|
38
|
+
const [row] = await db
|
|
39
|
+
.insert(sessions)
|
|
40
|
+
.values({
|
|
41
|
+
tokenHash,
|
|
42
|
+
expiresAt,
|
|
43
|
+
userAgent: opts.userAgent ?? null
|
|
44
|
+
})
|
|
45
|
+
.returning();
|
|
46
|
+
return { session: row, plaintext };
|
|
47
|
+
}
|
|
48
|
+
/** Look up a session by cookie plaintext. Returns the row if it exists and
|
|
49
|
+
* has not expired; otherwise null. Bumps last_used_at on hit. */
|
|
50
|
+
export async function consumeSession(db, plaintext) {
|
|
51
|
+
if (typeof plaintext !== "string" || plaintext.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
const tokenHash = hashToken(plaintext);
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const [row] = await db
|
|
56
|
+
.select()
|
|
57
|
+
.from(sessions)
|
|
58
|
+
.where(and(eq(sessions.tokenHash, tokenHash), gt(sessions.expiresAt, now)))
|
|
59
|
+
.limit(1);
|
|
60
|
+
if (!row)
|
|
61
|
+
return null;
|
|
62
|
+
await db
|
|
63
|
+
.update(sessions)
|
|
64
|
+
.set({ lastUsedAt: now })
|
|
65
|
+
.where(eq(sessions.id, row.id));
|
|
66
|
+
return row;
|
|
67
|
+
}
|
|
68
|
+
/** Revoke a single session by cookie plaintext. No-op when the cookie does
|
|
69
|
+
* not match any row. */
|
|
70
|
+
export async function revokeSession(db, plaintext) {
|
|
71
|
+
if (typeof plaintext !== "string" || plaintext.length === 0)
|
|
72
|
+
return false;
|
|
73
|
+
const tokenHash = hashToken(plaintext);
|
|
74
|
+
const result = await db.delete(sessions).where(eq(sessions.tokenHash, tokenHash));
|
|
75
|
+
// node-postgres returns rowCount on the result wrapper drizzle exposes via
|
|
76
|
+
// .rowCount; fall back to true when shape is opaque.
|
|
77
|
+
const r = result;
|
|
78
|
+
return r.rowCount === undefined ? true : r.rowCount > 0;
|
|
79
|
+
}
|
|
80
|
+
/** Periodic cleanup — called on daemon start. Deletes expired rows so the
|
|
81
|
+
* table doesn't bloat over months. */
|
|
82
|
+
export async function pruneExpiredSessions(db) {
|
|
83
|
+
const result = await db.delete(sessions).where(lt(sessions.expiresAt, new Date()));
|
|
84
|
+
const r = result;
|
|
85
|
+
return r.rowCount ?? 0;
|
|
86
|
+
}
|
|
87
|
+
/** Drizzle helper to noop a linter complaint when sql is unused. */
|
|
88
|
+
export const _sql = sql;
|
|
89
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/service/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Database } from "../db/client.js";
|
|
2
|
+
import { type Entity } from "../db/schema.js";
|
|
3
|
+
import type { TasksRangeInput } from "./types.js";
|
|
4
|
+
export interface TasksRangeResult {
|
|
5
|
+
scheduled: Entity[];
|
|
6
|
+
overdue: Entity[];
|
|
7
|
+
backlog: Entity[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Tasks in a date range, split into scheduled / overdue / backlog buckets.
|
|
11
|
+
*
|
|
12
|
+
* Each task has a "display date" that decides where it lands on the calendar:
|
|
13
|
+
* - status='done' → completedAt (when actually finished)
|
|
14
|
+
* - status='in_progress' → today (rolling — surfaces active work)
|
|
15
|
+
* - everything else → dueAt (when it's supposed to happen)
|
|
16
|
+
*
|
|
17
|
+
* `scheduled` returns any task whose display date falls inside [from, to).
|
|
18
|
+
* Done tasks without a completedAt (legacy / weird state) fall back to dueAt.
|
|
19
|
+
* `overdue` keeps the original meaning — past-due open work — and explicitly
|
|
20
|
+
* excludes done + in_progress so they don't double-surface (done shows on its
|
|
21
|
+
* completion day; in_progress already shows on today).
|
|
22
|
+
*/
|
|
23
|
+
export declare function tasksRange(db: Database["db"], input: TasksRangeInput): Promise<TasksRangeResult>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { and, asc, desc, eq, gte, inArray, isNotNull, isNull, lt, ne, or } from "drizzle-orm";
|
|
2
|
+
import { entities } from "../db/schema.js";
|
|
3
|
+
/**
|
|
4
|
+
* Tasks in a date range, split into scheduled / overdue / backlog buckets.
|
|
5
|
+
*
|
|
6
|
+
* Each task has a "display date" that decides where it lands on the calendar:
|
|
7
|
+
* - status='done' → completedAt (when actually finished)
|
|
8
|
+
* - status='in_progress' → today (rolling — surfaces active work)
|
|
9
|
+
* - everything else → dueAt (when it's supposed to happen)
|
|
10
|
+
*
|
|
11
|
+
* `scheduled` returns any task whose display date falls inside [from, to).
|
|
12
|
+
* Done tasks without a completedAt (legacy / weird state) fall back to dueAt.
|
|
13
|
+
* `overdue` keeps the original meaning — past-due open work — and explicitly
|
|
14
|
+
* excludes done + in_progress so they don't double-surface (done shows on its
|
|
15
|
+
* completion day; in_progress already shows on today).
|
|
16
|
+
*/
|
|
17
|
+
export async function tasksRange(db, input) {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const projectFilter = input.projectIds?.length
|
|
20
|
+
? inArray(entities.projectId, input.projectIds)
|
|
21
|
+
: undefined;
|
|
22
|
+
// Base filter applied to every bucket: only tasks, optional project filter,
|
|
23
|
+
// and the includeDone toggle (which gates done + archived together — both
|
|
24
|
+
// are terminal states that the day cell would otherwise mix into open work).
|
|
25
|
+
const baseConds = [eq(entities.kind, "task")];
|
|
26
|
+
if (!input.includeDone) {
|
|
27
|
+
baseConds.push(or(ne(entities.status, "done"), isNull(entities.status)));
|
|
28
|
+
baseConds.push(or(ne(entities.status, "archived"), isNull(entities.status)));
|
|
29
|
+
}
|
|
30
|
+
if (projectFilter)
|
|
31
|
+
baseConds.push(projectFilter);
|
|
32
|
+
// Three independent passes, unioned in JS. Drizzle's `or` keeps the SQL
|
|
33
|
+
// clear and the partial completed_at index keeps the done-pass cheap.
|
|
34
|
+
const inRange = (col) => {
|
|
35
|
+
const conds = [];
|
|
36
|
+
if (input.from)
|
|
37
|
+
conds.push(gte(col, input.from));
|
|
38
|
+
if (input.to)
|
|
39
|
+
conds.push(lt(col, input.to));
|
|
40
|
+
return conds;
|
|
41
|
+
};
|
|
42
|
+
// (a) Non-done, non-in_progress tasks scheduled by dueAt.
|
|
43
|
+
const dueByDueConds = [
|
|
44
|
+
...baseConds,
|
|
45
|
+
isNotNull(entities.dueAt),
|
|
46
|
+
or(ne(entities.status, "done"), isNull(entities.status)),
|
|
47
|
+
or(ne(entities.status, "in_progress"), isNull(entities.status)),
|
|
48
|
+
...inRange(entities.dueAt)
|
|
49
|
+
];
|
|
50
|
+
const dueByDue = await db
|
|
51
|
+
.select()
|
|
52
|
+
.from(entities)
|
|
53
|
+
.where(and(...dueByDueConds))
|
|
54
|
+
.orderBy(asc(entities.dueAt));
|
|
55
|
+
// (b) Done tasks rendered on completedAt. Only runs when the caller wants
|
|
56
|
+
// done items included — otherwise it returns nothing and the partial index
|
|
57
|
+
// is never touched.
|
|
58
|
+
let doneByCompleted = [];
|
|
59
|
+
if (input.includeDone) {
|
|
60
|
+
const doneConds = [
|
|
61
|
+
eq(entities.kind, "task"),
|
|
62
|
+
eq(entities.status, "done"),
|
|
63
|
+
isNotNull(entities.completedAt),
|
|
64
|
+
...inRange(entities.completedAt)
|
|
65
|
+
];
|
|
66
|
+
if (projectFilter)
|
|
67
|
+
doneConds.push(projectFilter);
|
|
68
|
+
doneByCompleted = await db
|
|
69
|
+
.select()
|
|
70
|
+
.from(entities)
|
|
71
|
+
.where(and(...doneConds))
|
|
72
|
+
.orderBy(asc(entities.completedAt));
|
|
73
|
+
}
|
|
74
|
+
// (c) in_progress tasks → today. Only relevant when "now" falls inside the
|
|
75
|
+
// requested window; otherwise the day cell would never render them anyway.
|
|
76
|
+
let inProgressToday = [];
|
|
77
|
+
const todayInRange = (!input.from || now >= new Date(input.from)) &&
|
|
78
|
+
(!input.to || now < new Date(input.to));
|
|
79
|
+
if (todayInRange) {
|
|
80
|
+
const ipConds = [eq(entities.kind, "task"), eq(entities.status, "in_progress")];
|
|
81
|
+
if (projectFilter)
|
|
82
|
+
ipConds.push(projectFilter);
|
|
83
|
+
inProgressToday = await db
|
|
84
|
+
.select()
|
|
85
|
+
.from(entities)
|
|
86
|
+
.where(and(...ipConds))
|
|
87
|
+
.orderBy(asc(entities.dueAt));
|
|
88
|
+
}
|
|
89
|
+
// Dedup by id — a status='done' task with a completedAt also matched (a) if
|
|
90
|
+
// its dueAt landed in range; (b) wins because it's the canonical display.
|
|
91
|
+
const scheduledMap = new Map();
|
|
92
|
+
for (const e of dueByDue)
|
|
93
|
+
scheduledMap.set(e.id, e);
|
|
94
|
+
for (const e of doneByCompleted)
|
|
95
|
+
scheduledMap.set(e.id, e);
|
|
96
|
+
for (const e of inProgressToday)
|
|
97
|
+
scheduledMap.set(e.id, e);
|
|
98
|
+
const scheduled = Array.from(scheduledMap.values());
|
|
99
|
+
// Overdue = past-due work that still needs attention. Done + in_progress
|
|
100
|
+
// tasks are intentionally excluded: done is shown on its completion day,
|
|
101
|
+
// in_progress is shown on today, neither belongs in an "overdue" alert.
|
|
102
|
+
const overdueConds = [
|
|
103
|
+
eq(entities.kind, "task"),
|
|
104
|
+
isNotNull(entities.dueAt),
|
|
105
|
+
lt(entities.dueAt, now),
|
|
106
|
+
or(ne(entities.status, "done"), isNull(entities.status)),
|
|
107
|
+
or(ne(entities.status, "in_progress"), isNull(entities.status)),
|
|
108
|
+
or(ne(entities.status, "archived"), isNull(entities.status))
|
|
109
|
+
];
|
|
110
|
+
if (projectFilter)
|
|
111
|
+
overdueConds.push(projectFilter);
|
|
112
|
+
const overdue = !input.includeDone
|
|
113
|
+
? await db
|
|
114
|
+
.select()
|
|
115
|
+
.from(entities)
|
|
116
|
+
.where(and(...overdueConds))
|
|
117
|
+
.orderBy(asc(entities.dueAt))
|
|
118
|
+
: [];
|
|
119
|
+
let backlog = [];
|
|
120
|
+
if (input.includeBacklog) {
|
|
121
|
+
backlog = await db
|
|
122
|
+
.select()
|
|
123
|
+
.from(entities)
|
|
124
|
+
.where(and(...baseConds, isNull(entities.dueAt)))
|
|
125
|
+
.orderBy(desc(entities.createdAt))
|
|
126
|
+
.limit(200);
|
|
127
|
+
}
|
|
128
|
+
return { scheduled, overdue, backlog };
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=tasks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../src/service/tasks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Database } from "../db/client.js";
|
|
2
|
+
import { type Token } from "../db/schema.js";
|
|
3
|
+
export interface CreatedToken {
|
|
4
|
+
token: Token;
|
|
5
|
+
/** The plaintext token. Only available immediately after creation. */
|
|
6
|
+
plaintext: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createToken(db: Database["db"], input: {
|
|
9
|
+
name: string;
|
|
10
|
+
}): Promise<CreatedToken>;
|
|
11
|
+
export declare function listTokens(db: Database["db"]): Promise<Token[]>;
|
|
12
|
+
export declare function revokeToken(db: Database["db"], id: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Look up a token by its plaintext. Returns null if not found.
|
|
15
|
+
* Bumps last_used_at on success. Not currently called by middleware; reserved
|
|
16
|
+
* for when auth enforcement is enabled.
|
|
17
|
+
*/
|
|
18
|
+
export declare function consumeToken(db: Database["db"], plaintext: string): Promise<Token | null>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { desc, eq } from "drizzle-orm";
|
|
3
|
+
import { tokens } from "../db/schema.js";
|
|
4
|
+
const TOKEN_PREFIX = "bn_";
|
|
5
|
+
function sha256(input) {
|
|
6
|
+
return createHash("sha256").update(input).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
export async function createToken(db, input) {
|
|
9
|
+
const plaintext = `${TOKEN_PREFIX}${randomBytes(24).toString("hex")}`;
|
|
10
|
+
const tokenHash = sha256(plaintext);
|
|
11
|
+
const prefix = plaintext.slice(0, 11); // bn_ + first 8 hex
|
|
12
|
+
const [row] = await db
|
|
13
|
+
.insert(tokens)
|
|
14
|
+
.values({ name: input.name, tokenHash, prefix })
|
|
15
|
+
.returning();
|
|
16
|
+
if (!row)
|
|
17
|
+
throw new Error("token insert returned no row");
|
|
18
|
+
return { token: row, plaintext };
|
|
19
|
+
}
|
|
20
|
+
export async function listTokens(db) {
|
|
21
|
+
return db.select().from(tokens).orderBy(desc(tokens.createdAt));
|
|
22
|
+
}
|
|
23
|
+
export async function revokeToken(db, id) {
|
|
24
|
+
const res = await db.delete(tokens).where(eq(tokens.id, id)).returning({ id: tokens.id });
|
|
25
|
+
return res.length > 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Look up a token by its plaintext. Returns null if not found.
|
|
29
|
+
* Bumps last_used_at on success. Not currently called by middleware; reserved
|
|
30
|
+
* for when auth enforcement is enabled.
|
|
31
|
+
*/
|
|
32
|
+
export async function consumeToken(db, plaintext) {
|
|
33
|
+
const tokenHash = sha256(plaintext);
|
|
34
|
+
const rows = await db.select().from(tokens).where(eq(tokens.tokenHash, tokenHash)).limit(1);
|
|
35
|
+
const row = rows[0];
|
|
36
|
+
if (!row)
|
|
37
|
+
return null;
|
|
38
|
+
await db.update(tokens).set({ lastUsedAt: new Date() }).where(eq(tokens.id, row.id));
|
|
39
|
+
return row;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/service/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,MAAM,EAAc,MAAM,iBAAiB,CAAC;AAErD,MAAM,YAAY,GAAG,KAAK,CAAC;AAE3B,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,EAAkB,EAClB,KAAuB;IAEvB,MAAM,SAAS,GAAG,GAAG,YAAY,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;IACtE,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,oBAAoB;IAC3D,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;SACnB,MAAM,CAAC,MAAM,CAAC;SACd,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;SAC/C,SAAS,EAAE,CAAC;IACf,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC1D,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,EAAkB;IACjD,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAkB,EAAE,EAAU;IAC9D,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1F,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,EAAkB,EAClB,SAAiB;IAEjB,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5F,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACrF,OAAO,GAAG,CAAC;AACb,CAAC"}
|