feat: add daily summaries feature

- Backend: daily_summaries table, API routes (GET/POST/PATCH) at /api/summaries
- Frontend: SummariesPage with calendar view, markdown rendering, stats bar, highlights
- Sidebar nav: added Summaries link between Activity and Chat
- Data population script for importing from memory files
- Bearer token + session auth support
This commit is contained in:
2026-01-30 04:42:10 +00:00
parent b5066a0d33
commit d5693a7624
8 changed files with 2012 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
/**
* Populate daily_summaries from ~/clawd/memory/*.md files.
* Usage: bun run src/scripts/populate-summaries.ts
*/
import { db } from "../db";
import { dailySummaries } from "../db/schema";
import { eq } from "drizzle-orm";
import { readdir, readFile } from "fs/promises";
import { join } from "path";
const MEMORY_DIR = process.env.MEMORY_DIR || "/home/clawdbot/clawd/memory";
function extractHighlights(content: string): { text: string }[] {
const highlights: { text: string }[] = [];
const lines = content.split("\n");
for (const line of lines) {
// Match ## headings as key sections
const h2Match = line.match(/^## (.+)/);
if (h2Match) {
highlights.push({ text: h2Match[1].trim() });
}
}
return highlights.slice(0, 20); // Cap at 20 highlights
}
function extractStats(content: string): Record<string, number> {
const lower = content.toLowerCase();
const stats: Record<string, number> = {};
// Count deploy mentions
const deployMatches = lower.match(/\b(deploy|deployed|deployment|redeployed)\b/g);
if (deployMatches) stats.deploys = deployMatches.length;
// Count commit/push mentions
const commitMatches = lower.match(/\b(commit|committed|pushed|push)\b/g);
if (commitMatches) stats.commits = commitMatches.length;
// Count task mentions
const taskMatches = lower.match(/\b(completed|task completed|hq-\d+.*completed)\b/g);
if (taskMatches) stats.tasksCompleted = taskMatches.length;
// Count feature mentions
const featureMatches = lower.match(/\b(feature|built|implemented|added|created)\b/g);
if (featureMatches) stats.featuresBuilt = Math.min(featureMatches.length, 30);
// Count fix mentions
const fixMatches = lower.match(/\b(fix|fixed|bug|bugfix|hotfix)\b/g);
if (fixMatches) stats.bugsFixed = fixMatches.length;
return stats;
}
async function main() {
console.log(`Reading memory files from ${MEMORY_DIR}...`);
const files = await readdir(MEMORY_DIR);
const mdFiles = files
.filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f))
.sort();
console.log(`Found ${mdFiles.length} memory files`);
for (const file of mdFiles) {
const date = file.replace(".md", "");
const filePath = join(MEMORY_DIR, file);
const content = await readFile(filePath, "utf-8");
const highlights = extractHighlights(content);
const stats = extractStats(content);
// Upsert
const existing = await db
.select()
.from(dailySummaries)
.where(eq(dailySummaries.date, date))
.limit(1);
if (existing.length > 0) {
await db
.update(dailySummaries)
.set({ content, highlights, stats, updatedAt: new Date() })
.where(eq(dailySummaries.date, date));
console.log(`Updated: ${date}`);
} else {
await db
.insert(dailySummaries)
.values({ date, content, highlights, stats });
console.log(`Inserted: ${date}`);
}
}
console.log("Done!");
process.exit(0);
}
main().catch((e) => {
console.error("Failed:", e);
process.exit(1);
});