diff --git a/src/index.ts b/src/index.ts index ddf623b..f59db72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ import { import { handleMcp } from "./mcp.js"; import { SOURCES } from "./sources.js"; import { syncAll } from "./sync.js"; +import { renderDashboard, renderOpportunities, renderOpportunity, renderStories, renderStory } from "./web.js"; const app = express(); app.set("trust proxy", true); @@ -186,81 +187,19 @@ app.get("/stories/:id", requireWebAuth, async (req, res) => { if (!story) return res.status(404).send("Story thread not found"); const user = (req as any).user ?? {}; - const identity = user.name || user.preferred_username || user.email || "Signed-in user"; - const nodes = Array.isArray(story.nodes) ? story.nodes : []; - const edges = Array.isArray(story.edges) ? story.edges : []; - const checks = Array.isArray(story.research_checks) ? story.research_checks : []; - const counter = checks.find((c:any)=>c.check_type === "counter_evidence"); - - res.type("html").send(` - -${escapeHtml(story.title)} — Story Research - -
← Story Threads

${escapeHtml(story.title)}

${escapeHtml(story.question)}

-
${escapeHtml(identity)} · Sign out
-
-Status: ${escapeHtml(story.status)} · -Nodes: ${nodes.length} · -Edges: ${edges.length} -
-${["researching","review","develop","closed"].map(s=>`
`).join("")} -
-
-
-Mandatory counter-evidence pass: -${counter ? `${escapeHtml(counter.result)} · sources checked: ${escapeHtml((counter.sources_checked ?? []).join(", "))}

${escapeHtml(counter.summary)}

` : "not completed"} -
-
${storyGraphSvg(story)}
-

Research nodes

-
-${nodes.map((n:any)=>`
-${escapeHtml(n.node_type)} -

${escapeHtml(n.title)}

-

${escapeHtml(n.summary)}

-
${n.provenance_source_id ? `source ${escapeHtml(n.provenance_source_id)} · ` : ""}${n.confidence != null ? `confidence ${escapeHtml(n.confidence)} · ` : ""}${escapeHtml(n.created_by_name)}
-${n.evidence_url ? `

Open evidence ↗

` : ""} -
`).join("")} -
-

Relationships

-${edges.length ? edges.map((e:any)=>`
${escapeHtml(e.relationship)}
${escapeHtml(e.from_node_id.slice(0,8))} → ${escapeHtml(e.to_node_id.slice(0,8))}

${escapeHtml(e.reason)}

`).join("") : '
No graph edges yet.
'} -`); + const identity = String(user.name || user.preferred_username || user.email || "Signed-in user"); + return res.type("html").send(renderStory({ story, identity })); }); app.get("/stories", requireWebAuth, async (req, res) => { - const stories = await listStoryThreads({ limit: 100 }); - const user = (req as any).user ?? {}; - const identity = user.name || user.preferred_username || user.email || "Signed-in user"; + const rows = await listStoryThreads({ limit: 100 }); + const stories = (await Promise.all( + rows.map((row: any) => getStoryThread(String(row.id))) + )).filter(Boolean) as any[]; - res.type("html").send(` - -Story Threads — Media Intelligence - -
← Dashboard

360° Story Threads

Evidence networks that require an explicit counter-evidence pass before editorial review.

-
${escapeHtml(identity)} · Sign out
-${stories.length ? stories.map((s:any)=>`
-${escapeHtml(s.title)} -

${escapeHtml(s.question)}

-
${escapeHtml(s.status)} · ${s.node_count} nodes · ${s.edge_count} edges · counter-evidence: ${escapeHtml(s.counter_evidence_result ?? "pending")}
-
`).join("") : '
No story threads yet.
'} -`); + const user = (req as any).user ?? {}; + const identity = String(user.name || user.preferred_username || user.email || "Signed-in user"); + return res.type("html").send(renderStories({ stories, identity })); }); app.post("/messages/:id/ack", requireWebAuth, async (req, res) => { @@ -284,39 +223,8 @@ app.get("/opportunities/:id", requireWebAuth, async (req, res) => { if (!opportunity) return res.status(404).send("Opportunity not found"); const user = (req as any).user ?? {}; - const identity = user.name || user.preferred_username || user.email || "Signed-in user"; - const evidence = Array.isArray(opportunity.evidence) ? opportunity.evidence : []; - const notes = Array.isArray(opportunity.notes) ? opportunity.notes : []; - - res.type("html").send(` - -${escapeHtml(opportunity.title)} — Media Intelligence - -
← Opportunities

${escapeHtml(opportunity.title)}

-
${escapeHtml(identity)} · Sign out
-
-
${escapeHtml(opportunity.source_id)} · ${escapeHtml(nzTime(opportunity.published_at || opportunity.detected_at))}
-

${escapeHtml(opportunity.summary)}

-

Status: ${escapeHtml(opportunity.status)} · Score: ${escapeHtml(opportunity.score)}

-

Why surfaced: ${escapeHtml((opportunity.reasons ?? []).join(" · "))}

-

Open original source ↗

-
-${["watch","investigate","develop","ignore","closed"].map(status=>`
`).join("")} -
-
-

Evidence

-${evidence.length ? evidence.map((e:any)=>`
${escapeHtml(nzTime(e.retrieved_at))} · ${escapeHtml(e.content_hash?.slice(0,12))}
${escapeHtml(e.excerpt)}
Source ↗
`).join("") : '
No evidence stored.
'} -

Notes

-${notes.length ? notes.map((n:any)=>`
${escapeHtml(nzTime(n.created_at))}

${escapeHtml(n.note)}

`).join("") : '
No notes yet.
'} -`); + const identity = String(user.name || user.preferred_username || user.email || "Signed-in user"); + return res.type("html").send(renderOpportunity({ opportunity, identity })); }); app.get("/opportunities", requireWebAuth, async (req, res) => { @@ -324,44 +232,21 @@ app.get("/opportunities", requireWebAuth, async (req, res) => { const valid = new Set(["new","watch","investigate","develop","ignore","closed"]); const status = valid.has(requested) ? requested : undefined; const opportunities = await listOpportunities({ status, limit: 100 }); - const user = (req as any).user ?? {}; - const identity = user.name || user.preferred_username || user.email || "Signed-in user"; - res.type("html").send(` - -Opportunities — Media Intelligence - -
← Dashboard

Media Opportunities

Human triage queue. No release drafting or distribution happens here.

-
${escapeHtml(identity)} · Sign out
-
-Active -${["new","watch","investigate","develop","ignore","closed"].map(s=>`${s}`).join("")} -
-${opportunities.length ? opportunities.map((o:any)=>`
-${escapeHtml(o.title)} -
${escapeHtml(o.source_id)} · ${escapeHtml(nzTime(o.published_at || o.detected_at))} · status ${escapeHtml(o.status)} · score ${escapeHtml(o.score)}
-

${escapeHtml(o.summary)}

-
${escapeHtml((o.reasons ?? []).join(" · "))}
-
-${["watch","investigate","develop","ignore"].map(s=>`
`).join("")} -
-
`).join("") : '
No opportunities in this view.
'} -`); + const user = (req as any).user ?? {}; + const identity = String(user.name || user.preferred_username || user.email || "Signed-in user"); + return res.type("html").send(renderOpportunities({ opportunities, identity, status })); }); app.get("/", requireWebAuth, async (req, res) => { const sourceRows = await pool.query( - `SELECT s.id,s.name,s.last_synced_at,s.last_error,s.baseline_complete, + `SELECT s.id,s.name,s.source_type,s.source_class,s.publisher,s.authority_tier, + s.last_synced_at,s.last_error,s.baseline_complete, COUNT(sig.id)::int AS signal_count FROM sources s LEFT JOIN signals sig ON sig.source_id=s.id - GROUP BY s.id,s.name,s.last_synced_at,s.last_error,s.baseline_complete + GROUP BY s.id,s.name,s.source_type,s.source_class,s.publisher,s.authority_tier, + s.last_synced_at,s.last_error,s.baseline_complete ORDER BY s.id` ); const counts = await pool.query(` @@ -373,56 +258,21 @@ app.get("/", requireWebAuth, async (req, res) => { (SELECT COUNT(*) FROM story_threads WHERE status <> 'closed') AS story_threads `); const messages = await listAgentMessages({ acknowledged: false, limit: 10 }); - const c = counts.rows[0] ?? {}; - const user = (req as any).user ?? {}; - const identity = user.name || user.preferred_username || user.email || "Signed-in user"; + const storyRows = await listStoryThreads({ limit: 20 }); + const stories = (await Promise.all( + storyRows.map((row: any) => getStoryThread(String(row.id))) + )).filter(Boolean) as any[]; - res.type("html").send(` - -Media Intelligence - -
-

Media Intelligence MVP

Human-led NZ public-source monitoring behind a dedicated MCP.

Open Opportunities → · Open 360° Story Threads →

-
${escapeHtml(identity)}
Sign out
-
-
-
${c.signals ?? 0}signals
-
${c.opportunities ?? 0}opportunities
-
${c.new_opportunities ?? 0}new
-
${c.unread_messages ?? 0}agent messages
-
${c.story_threads ?? 0}story threads
-
-

Agent Activity

-${messages.length - ? messages.map((m:any)=>`
-
-
${escapeHtml(m.title)}
${escapeHtml(m.kind)} · ${escapeHtml(m.severity)}
-
${escapeHtml(nzTime(m.created_at))}
-
-

${escapeHtml(m.body)}

-
From ${escapeHtml(m.actor_name)} via ${escapeHtml(m.actor_client_id)}${m.related_type ? ` · ${escapeHtml(m.related_type)} ${escapeHtml(m.related_id ?? "")}` : ""}
-
-
`).join("") - : '
No unread agent messages.
'} -

Sources

-${sourceRows.rows.map((s:any)=>`
${s.name}
${s.id} · ${s.baseline_complete ? 'baseline ready' : 'awaiting baseline'}
${s.signal_count} stored signals
${s.last_error ? 'Error: '+s.last_error : (s.last_synced_at ? 'Last sync '+new Date(s.last_synced_at).toLocaleString() : 'Waiting for first sync')}
`).join("")} -

MCP endpoint: /api/mcp — OAuth protected separately from this browser session.

-`); + const user = (req as any).user ?? {}; + const identity = String(user.name || user.preferred_username || user.email || "Signed-in user"); + + return res.type("html").send(renderDashboard({ + identity, + counts: counts.rows[0] ?? {}, + messages, + sources: sourceRows.rows, + stories + })); }); async function start() {