feat: add blog draft generator and fix broken routes
Blog drafting section (dev-only): - BlogDraftGenerator gathers project data (gaps, proposals, stats) as context and calls Claude to produce Medium-style blog posts - DB schema: blog_drafts table with title, content, tags, cost tracking - Web UI: list, generate (async with live preview), detail (rendered + source toggle), edit, and export routes - 6 writing styles: deep-dive, overview, opinion, listicle, comparison, series-post - Nav link added to sidebar under Proposals Bug fixes found via route testing (scripts/test_all_routes.py): - /authors/<id>: Draft.status → Draft.states (correct attribute name) - /false-positives: add missing `import re` in ratings.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -218,6 +218,26 @@ CREATE TABLE IF NOT EXISTS proposal_gaps (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proposal_gaps_gap ON proposal_gaps(gap_id);
|
||||
|
||||
-- Blog drafts (generated or manually written)
|
||||
CREATE TABLE IF NOT EXISTS blog_drafts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
subtitle TEXT DEFAULT '',
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
status TEXT DEFAULT 'draft',
|
||||
style TEXT DEFAULT 'deep-dive',
|
||||
tags TEXT DEFAULT '[]',
|
||||
topic TEXT DEFAULT '',
|
||||
instructions TEXT DEFAULT '',
|
||||
content_md TEXT DEFAULT '',
|
||||
model_used TEXT DEFAULT '',
|
||||
input_tokens INTEGER DEFAULT 0,
|
||||
output_tokens INTEGER DEFAULT 0,
|
||||
cost_usd REAL DEFAULT 0,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
-- Annotations (user notes + tags per draft)
|
||||
CREATE TABLE IF NOT EXISTS annotations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -1169,6 +1189,62 @@ class Database:
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
# --- Blog Drafts ---
|
||||
|
||||
def all_blog_drafts(self) -> list[dict]:
|
||||
rows = self.conn.execute(
|
||||
"SELECT * FROM blog_drafts ORDER BY updated_at DESC"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def get_blog_draft(self, draft_id: int) -> dict | None:
|
||||
row = self.conn.execute(
|
||||
"SELECT * FROM blog_drafts WHERE id = ?", (draft_id,)
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def upsert_blog_draft(self, draft: dict) -> int:
|
||||
"""Insert or update a blog draft. Returns the draft ID."""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
if draft.get("id"):
|
||||
self.conn.execute(
|
||||
"""UPDATE blog_drafts SET title=?, subtitle=?, slug=?, status=?,
|
||||
style=?, tags=?, topic=?, instructions=?, content_md=?,
|
||||
model_used=?, input_tokens=?, output_tokens=?, cost_usd=?,
|
||||
updated_at=?
|
||||
WHERE id=?""",
|
||||
(draft["title"], draft.get("subtitle", ""), draft["slug"],
|
||||
draft.get("status", "draft"), draft.get("style", "deep-dive"),
|
||||
json.dumps(draft.get("tags", [])), draft.get("topic", ""),
|
||||
draft.get("instructions", ""), draft.get("content_md", ""),
|
||||
draft.get("model_used", ""), draft.get("input_tokens", 0),
|
||||
draft.get("output_tokens", 0), draft.get("cost_usd", 0),
|
||||
now, draft["id"]),
|
||||
)
|
||||
self.conn.commit()
|
||||
return draft["id"]
|
||||
else:
|
||||
cur = self.conn.execute(
|
||||
"""INSERT INTO blog_drafts (title, subtitle, slug, status, style,
|
||||
tags, topic, instructions, content_md, model_used,
|
||||
input_tokens, output_tokens, cost_usd, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(draft["title"], draft.get("subtitle", ""), draft["slug"],
|
||||
draft.get("status", "draft"), draft.get("style", "deep-dive"),
|
||||
json.dumps(draft.get("tags", [])), draft.get("topic", ""),
|
||||
draft.get("instructions", ""), draft.get("content_md", ""),
|
||||
draft.get("model_used", ""), draft.get("input_tokens", 0),
|
||||
draft.get("output_tokens", 0), draft.get("cost_usd", 0),
|
||||
now, now),
|
||||
)
|
||||
self.conn.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
def delete_blog_draft(self, draft_id: int) -> bool:
|
||||
cur = self.conn.execute("DELETE FROM blog_drafts WHERE id = ?", (draft_id,))
|
||||
self.conn.commit()
|
||||
return cur.rowcount > 0
|
||||
|
||||
# --- Refs ---
|
||||
|
||||
def insert_refs(self, draft_name: str, refs: list[tuple[str, str]]) -> None:
|
||||
|
||||
Reference in New Issue
Block a user