feat(observabilidade): tabela worklog_comments + parser HTML + importer MCP
- Schema worklog_comments (id, discussion, parent, datas, staff, campos parseados em JSON) - Parser HTML tolerante (h2/h3/h4) extrai title, task_ref, duration, work_items, files_modified, problems, patterns_text, actions - Módulo worklog-import com paginação MCP get_discussion_comments - Helper mcp-client.ts partilhado (gateway MCP JSON-RPC + SSE) - Dep runtime: node-html-parser Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,31 @@ export interface PatternRecord {
|
||||
consecutive_weeks: number
|
||||
}
|
||||
|
||||
export interface WorklogCommentRecord {
|
||||
id: number
|
||||
discussion_id: number
|
||||
created_at: string
|
||||
staff_id: number | null
|
||||
title: string | null
|
||||
task_ref: string | null
|
||||
duration_sec: number | null
|
||||
work_items: string[]
|
||||
files_modified: string[]
|
||||
problems: { problema: string; solucao: string }[]
|
||||
patterns_text: string[]
|
||||
actions: { tipo: string; descricao: string; prioridade: string | null }[]
|
||||
raw_html: string
|
||||
imported_at: string
|
||||
}
|
||||
|
||||
export interface WorklogFilters {
|
||||
discussion_id?: number
|
||||
task_ref?: string
|
||||
sinceIso?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface SessionsDb {
|
||||
upsertSession(meta: SessionMeta): void
|
||||
upsertMany(metas: SessionMeta[]): void
|
||||
@@ -37,6 +62,10 @@ export interface SessionsDb {
|
||||
upsertPattern(p: PatternRecord): void
|
||||
getPatternsByWeek(week: string): PatternRecord[]
|
||||
getConsecutiveWeeks(pattern_key: string, uptoWeek: string): number
|
||||
upsertWorklogComment(c: WorklogCommentRecord): { inserted: boolean }
|
||||
hasWorklogComment(id: number): boolean
|
||||
listWorklogComments(filters: WorklogFilters): WorklogCommentRecord[]
|
||||
countWorklogComments(filters?: WorklogFilters): number
|
||||
rawDb(): Database.Database
|
||||
close(): void
|
||||
}
|
||||
@@ -81,6 +110,25 @@ CREATE TABLE IF NOT EXISTS patterns (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_patterns_week ON patterns(week_iso);
|
||||
CREATE INDEX IF NOT EXISTS idx_patterns_key ON patterns(pattern_key);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS worklog_comments (
|
||||
id INTEGER PRIMARY KEY,
|
||||
discussion_id INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
staff_id INTEGER,
|
||||
title TEXT,
|
||||
task_ref TEXT,
|
||||
duration_sec INTEGER,
|
||||
work_items TEXT NOT NULL,
|
||||
files_modified TEXT NOT NULL,
|
||||
problems_json TEXT NOT NULL,
|
||||
patterns_text TEXT NOT NULL,
|
||||
actions_json TEXT NOT NULL,
|
||||
raw_html TEXT NOT NULL,
|
||||
imported_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wc_discussion ON worklog_comments(discussion_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_wc_task ON worklog_comments(task_ref);
|
||||
`
|
||||
|
||||
function rowToMeta(row: Record<string, unknown>): SessionMeta {
|
||||
@@ -272,6 +320,89 @@ export function openSessionsDb(dbPath: string): SessionsDb {
|
||||
}
|
||||
return count
|
||||
},
|
||||
upsertWorklogComment(c: WorklogCommentRecord): { inserted: boolean } {
|
||||
const existing = db.prepare('SELECT 1 FROM worklog_comments WHERE id = ?').get(c.id)
|
||||
const inserted = !existing
|
||||
db.prepare(`
|
||||
INSERT INTO worklog_comments (id, discussion_id, created_at, staff_id, title, task_ref,
|
||||
duration_sec, work_items, files_modified, problems_json, patterns_text, actions_json,
|
||||
raw_html, imported_at)
|
||||
VALUES (@id, @discussion_id, @created_at, @staff_id, @title, @task_ref,
|
||||
@duration_sec, @work_items, @files_modified, @problems_json, @patterns_text, @actions_json,
|
||||
@raw_html, @imported_at)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
discussion_id = excluded.discussion_id,
|
||||
created_at = excluded.created_at,
|
||||
staff_id = excluded.staff_id,
|
||||
title = excluded.title,
|
||||
task_ref = excluded.task_ref,
|
||||
duration_sec = excluded.duration_sec,
|
||||
work_items = excluded.work_items,
|
||||
files_modified = excluded.files_modified,
|
||||
problems_json = excluded.problems_json,
|
||||
patterns_text = excluded.patterns_text,
|
||||
actions_json = excluded.actions_json,
|
||||
raw_html = excluded.raw_html,
|
||||
imported_at = excluded.imported_at
|
||||
`).run({
|
||||
id: c.id,
|
||||
discussion_id: c.discussion_id,
|
||||
created_at: c.created_at,
|
||||
staff_id: c.staff_id,
|
||||
title: c.title,
|
||||
task_ref: c.task_ref,
|
||||
duration_sec: c.duration_sec,
|
||||
work_items: JSON.stringify(c.work_items),
|
||||
files_modified: JSON.stringify(c.files_modified),
|
||||
problems_json: JSON.stringify(c.problems),
|
||||
patterns_text: JSON.stringify(c.patterns_text),
|
||||
actions_json: JSON.stringify(c.actions),
|
||||
raw_html: c.raw_html,
|
||||
imported_at: c.imported_at,
|
||||
})
|
||||
return { inserted }
|
||||
},
|
||||
hasWorklogComment(id: number): boolean {
|
||||
return !!db.prepare('SELECT 1 FROM worklog_comments WHERE id = ?').get(id)
|
||||
},
|
||||
listWorklogComments(filters: WorklogFilters): WorklogCommentRecord[] {
|
||||
const parts: string[] = []
|
||||
const params: Record<string, unknown> = {}
|
||||
if (filters.discussion_id) { parts.push('discussion_id = @discussion_id'); params.discussion_id = filters.discussion_id }
|
||||
if (filters.task_ref) { parts.push('task_ref = @task_ref'); params.task_ref = filters.task_ref }
|
||||
if (filters.sinceIso) { parts.push('created_at >= @since'); params.since = filters.sinceIso }
|
||||
const where = parts.length ? 'WHERE ' + parts.join(' AND ') : ''
|
||||
const limit = filters.limit ?? 1000
|
||||
const offset = filters.offset ?? 0
|
||||
const rows = db.prepare(`SELECT * FROM worklog_comments ${where} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`)
|
||||
.all({ ...params, limit, offset }) as Record<string, unknown>[]
|
||||
return rows.map((r) => ({
|
||||
id: r.id as number,
|
||||
discussion_id: r.discussion_id as number,
|
||||
created_at: r.created_at as string,
|
||||
staff_id: (r.staff_id as number | null) ?? null,
|
||||
title: (r.title as string | null) ?? null,
|
||||
task_ref: (r.task_ref as string | null) ?? null,
|
||||
duration_sec: (r.duration_sec as number | null) ?? null,
|
||||
work_items: JSON.parse(r.work_items as string),
|
||||
files_modified: JSON.parse(r.files_modified as string),
|
||||
problems: JSON.parse(r.problems_json as string),
|
||||
patterns_text: JSON.parse(r.patterns_text as string),
|
||||
actions: JSON.parse(r.actions_json as string),
|
||||
raw_html: r.raw_html as string,
|
||||
imported_at: r.imported_at as string,
|
||||
}))
|
||||
},
|
||||
countWorklogComments(filters?: WorklogFilters): number {
|
||||
const parts: string[] = []
|
||||
const params: Record<string, unknown> = {}
|
||||
if (filters?.discussion_id) { parts.push('discussion_id = @discussion_id'); params.discussion_id = filters.discussion_id }
|
||||
if (filters?.task_ref) { parts.push('task_ref = @task_ref'); params.task_ref = filters.task_ref }
|
||||
if (filters?.sinceIso) { parts.push('created_at >= @since'); params.since = filters.sinceIso }
|
||||
const where = parts.length ? 'WHERE ' + parts.join(' AND ') : ''
|
||||
const row = db.prepare(`SELECT COUNT(*) as c FROM worklog_comments ${where}`).get(params) as { c: number }
|
||||
return row.c
|
||||
},
|
||||
rawDb(): Database.Database {
|
||||
return db
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user