diff --git a/src/db.ts b/src/db.ts index d4f1d7b..f4b0be3 100644 --- a/src/db.ts +++ b/src/db.ts @@ -305,7 +305,11 @@ export async function sourceStatus() { const result = await pool.query(` SELECT s.*, (SELECT COUNT(*)::int FROM signals sig WHERE sig.source_id=s.id) AS signal_count, - (SELECT COUNT(*)::int FROM opportunities o JOIN signals sig ON sig.id=o.signal_id WHERE sig.source_id=s.id) AS opportunity_count + (SELECT COUNT(*)::int + FROM opportunities o + JOIN signals sig ON sig.id=o.signal_id + WHERE sig.source_id=s.id + AND o.status <> 'closed') AS opportunity_count FROM sources s ORDER BY s.id `); @@ -317,7 +321,9 @@ export async function listOpportunities(args: { status?: string; since?: string; const where: string[] = []; if (args.status) { values.push(args.status); - where.push(`o.status=$${values.length}`); + where.push(`o.status=${values.length}`); + } else { + where.push("o.status <> 'closed'"); } if (args.since) { values.push(args.since); diff --git a/src/index.ts b/src/index.ts index fe9ee1e..af0e23e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ import express from "express"; import { acknowledgeAgentMessage, + getOpportunity, listAgentMessages, + listOpportunities, + markOpportunity, migrate, pool, seedSources @@ -112,6 +115,89 @@ app.post("/messages/:id/ack", requireWebAuth, async (req, res) => { return res.redirect("/"); }); +app.post("/opportunities/:id/status", requireWebAuth, async (req, res) => { + const id = String(req.params.id); + const status = String(req.body.status ?? ""); + await markOpportunity(id, status); + return res.redirect(req.body.returnTo === "detail" ? `/opportunities/${encodeURIComponent(id)}` : "/opportunities"); +}); + +app.get("/opportunities/:id", requireWebAuth, async (req, res) => { + const opportunity = await getOpportunity(String(req.params.id)); + 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.summary)}
+Status: ${escapeHtml(opportunity.status)} · Score: ${escapeHtml(opportunity.score)}
+Why surfaced: ${escapeHtml((opportunity.reasons ?? []).join(" · "))}
+ +${escapeHtml(e.excerpt)}Source ↗${escapeHtml(n.note)}
${escapeHtml(o.summary)}
+ +Human-led NZ public-source monitoring behind a dedicated MCP.
Human-led NZ public-source monitoring behind a dedicated MCP.