commit 32839ef04999bc356b43ff794123fdb82158c6c3 Author: Johan Jongsma Date: Sun Feb 1 08:03:51 2026 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d82971 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +*.log +.DS_Store + diff --git a/dashboard-server b/dashboard-server new file mode 100755 index 0000000..f27d0d0 Binary files /dev/null and b/dashboard-server differ diff --git a/data/briefing.json b/data/briefing.json new file mode 100644 index 0000000..a8b27fe --- /dev/null +++ b/data/briefing.json @@ -0,0 +1,53 @@ +{ + "timestamp": "2026-01-28T23:30:00Z", + "type": "evening", + "markets": { + "indices": [ + { "symbol": "S&P 500", "value": "7,000", "change": "+0.3%", "direction": "up", "note": "Historic first!" }, + { "symbol": "NASDAQ", "value": "23,857", "change": "+0.17%", "direction": "up" }, + { "symbol": "NVDA", "value": "+1.5%", "change": "up", "direction": "up", "note": "China AI chip hopes" } + ], + "stocks": [], + "commodities": [], + "forex": [] + }, + "weather": { + "status": "clear", + "headline": "St. Pete — Clear", + "temp": "43°F – 71°F", + "details": "No tropical threats. Hurricane season starts June. Thunderstorms in Panhandle — you're clear.", + "alerts": [] + }, + "techNews": [ + { + "headline": "🦞 Clawdbot → Moltbot (Rename)", + "source": "Forbes / The Register", + "summary": "Anthropic asked for name change (trademark). Now 'Moltbot' + 'Molty'. Same lobster soul, new shell. Security vuln (proxy misconfig) also fixed." + }, + { + "headline": "Claude = Best AI Coding Assistant", + "source": "Wikipedia", + "summary": "As of Jan 2026, widely considered #1 for coding (with GPT-5.2 close). Went viral over winter holidays." + }, + { + "headline": "DeepSeek: Manifold-Constrained Hyper-Connections", + "source": "Times of India", + "summary": "New training method scales AI without ballooning compute. Direct challenge to 'bigger = smarter' consensus." + }, + { + "headline": "Chinese AI Models Hit 15% Global Share", + "source": "TrendForce", + "summary": "DeepSeek + Alibaba Qwen driving adoption. R1 model (Jan 2025) still disrupting with high performance at low cost." + }, + { + "headline": "Anthropic: 'Wake Up to AI Risks'", + "source": "The Guardian", + "summary": "Dario Amodei warns humanity entering phase that will 'test who we are as a species'. World needs to wake up." + }, + { + "headline": "ServiceNow + Anthropic Partnership", + "source": "Morningstar", + "summary": "Claude is now default model powering ServiceNow Build Agent. Enterprise AI push." + } + ] +} diff --git a/data/briefings.json b/data/briefings.json new file mode 100644 index 0000000..b9afb65 --- /dev/null +++ b/data/briefings.json @@ -0,0 +1,89 @@ +{ + "briefings": [ + { + "id": "172a3946", + "date": "2025-01-28", + "title": "Afternoon Brief - Jan 28", + "weather": "St. Petersburg FL — mild evening", + "markets": "Not checked this cycle", + "news": "ClawdNode gateway working, notifications + calls flowing", + "tasks": "✅ Git user hardened on Zurich VPS\n✅ ClawdNode v0.1 built (10 Kotlin files)\n✅ Gateway protocol fix\n✅ Memory review system\n✅ Claude Code updated (2.1.20 → 2.1.22)\n🔲 ClawdNode: rebuild + test\n🔲 OpenVAS vuln scan\n🔲 Uptime Kuma setup", + "summary": "ClawdNode breakthrough — notifications and calls flowing through custom gateway. Phone answered command OK (TelecomManager implementation pending). Flutter styleguide blocking deployment — priority for 7pm sprint.", + "created_at": "2026-01-28T22:41:56.65781346Z" + }, + { + "id": "f5c12407", + "date": "2026-01-29", + "title": "Morning Brief - Jan 29, 2026", + "weather": "St. Petersburg FL: ☀️ Clear, 47°F now. High 64°F / Low 39°F. No rain expected.", + "markets": "/ES 7,023 (+0.2%) | /CL $62.76 (+0.6%) | Gold $5,550 (+3.9% 🚀) | TSLA $431 (+0.1%, pre-mkt +1.8%) | NABL ~$7.50", + "news": "• Anthropic revises Claude constitution, hints at AI consciousness (Fortune/TechCrunch)\n• Anthropic seeking $350B valuation (Vice)\n• Moltbot (fka Clawdbot) goes viral - WIRED: \"Taking Over Silicon Valley\"\n• TechCrunch deep-dive on Moltbot published 2 days ago", + "summary": "Gold surging nearly 4% on dollar weakness. Fed decision today. Clawdbot/Moltbot getting serious mainstream press coverage.", + "created_at": "2026-01-29T14:01:29.389328336Z" + }, + { + "id": "bd403f5d", + "date": "2026-01-29", + "title": "Afternoon Brief - Jan 29", + "weather": "n/a", + "markets": "n/a", + "news": "n/a", + "tasks": "✅ Flutter InouText style library complete\n✅ Sora fonts (7 weights) installed\n✅ Azure Files backup POC files created\n✅ Morning brief delivered\n🔲 OpenVAS scan still running\n🔲 Top menu behavior verification pending", + "summary": "Productive day. inou Flutter now has centralized typography (InouText.*). Azure Files POC scaffolded but needs Go install to test. OpenVAS scan on home IP in progress.", + "created_at": "2026-01-29T22:00:14.776990456Z" + }, + { + "id": "8e79d9bb", + "date": "2026-01-30", + "title": "Morning Brief - Jan 30, 2026", + "weather": "St. Pete FL: Weather service timeout - check locally", + "markets": "/ES 7,023 (+0.2%) recovering from -0.9% overnight | /CL $62.76 (+0.6%) | Gold $5,050 (-5.7% crash from ATH!) | TSLA $416.56 (-3.2%) | NABL $7.48 (-0.8%)", + "news": "🦞 MAJOR: Clawdbot officially rebranded to Moltbot after Anthropic trademark request - 60k+ GitHub stars, NYT Hard Fork podcast coverage today | 📉 Gold crashes 5.7% to $5,000 as Warsh Fed pick strengthens dollar | ⚠️ Security researchers flagging Moltbot exposed admin ports", + "summary": "Moltbot rebrand is THE story - viral growth continues despite (or because of?) security concerns. Gold selloff after hitting ATH. Markets mixed on Fed chair pick.", + "created_at": "2026-01-30T14:01:29.27006189Z" + }, + { + "id": "57f78885", + "date": "2026-01-30", + "title": "Morning Brief — Jan 30, 2026", + "weather": "Service timed out", + "markets": "/ES: 7,023 (+0.2%), /CL: $62.76 (+0.6%), Gold: $5,050 (-5.7% crash from ATH), TSLA: $416.56 (-3.2%), NABL: $7.48 (-0.8%)", + "news": "🦞 Clawdbot → Moltbot rebrand (Anthropic trademark request). 60k+ GitHub stars, NYT Hard Fork podcast today. Security researchers flagging exposed admin ports and poisoned skills.\n\n📉 Gold -5.7% selloff as Kevin Warsh Fed chair pick strengthens dollar.", + "tasks": "S short position winning (fell 5.7% yesterday, Goldman cut PT to $16.50)", + "summary": "Gold crashed 5.7% on Fed chair news. Moltbot rebrand is official. Your S short continues to print.", + "created_at": "2026-01-30T15:01:11.886794741Z" + }, + { + "id": "5dcba631", + "date": "2026-01-30", + "title": "Afternoon Brief - Jan 30", + "weather": "n/a", + "markets": "NABL $7.49 (+2.67%)", + "news": "Dutch coalition formed (D66/VVD/CDA), PVV in crisis", + "tasks": "✅ Chrome relay + K2.5 browser agent working\n✅ Browser SKILL.md + TOOLS.md updated\n✅ Screensaver disabled on james\n🔲 Proton Mail skill (potential project)", + "summary": "Browser optimization day. Chrome relay working for authenticated sites, K2.5 agent created for cheap browsing tasks. Email triage discussed - may build proton-mail skill using IMAP to Bridge.", + "created_at": "2026-01-30T22:00:11.549426164Z" + }, + { + "id": "fe6d3974", + "date": "2026-01-31", + "title": "Morning Brief - January 31, 2026", + "weather": "🥶 COLD SNAP: 49-58°F today, low 36°F tonight (wind chill 27°F!). Windy gusts to 39mph. Unusual for Florida.", + "markets": "📉 Fri close: S\u0026P -0.4% (6,039), Nasdaq -0.9%, Dow -0.4% (but Jan positive). TSLA $430 (+3.3%). Gold CRASHED 9% to $4,880 after hitting $5,625 record. Silver -28%. Oil $65.85. BTC ~$83,900. 10Y: 4.25%", + "news": "🔥 Trump nominates Kevin Warsh to replace Powell as Fed Chair. OpenClaw rebrand getting major press (Forbes, CNET, IBM, HN frontpage). Anthropic: Claude constitution revamp, seeking $350B valuation.", + "summary": "Gold/silver record highs → massive pullback. Fed Chair bombshell. OpenClaw making waves. Cold weekend ahead.", + "created_at": "2026-01-31T14:01:07.575988996Z" + }, + { + "id": "90f4436a", + "date": "2026-01-31", + "title": "Afternoon Brief - Jan 31", + "weather": "n/a", + "markets": "n/a", + "news": "OpenClaw 2026.1.30 released", + "tasks": "• Mail agent refactored (pure API)\n• inou-mobile project created (6 features)\n• Nuclei scan: 34 info, 0 critical\n• Email review: 5 action items", + "summary": "Productive Saturday. Mail agent is now a clean IMAP/SMTP API — I handle all triage. inou-mobile Flutter project bootstrapped with OCR, voice, biometrics. Security scan on inou.com passed clean. 5 emails need your attention (Barclays fraud alert, UPS fees, DigiKey, Cryo-Cell renewal, Health Link invoice).", + "created_at": "2026-01-31T22:00:18.214044416Z" + } + ] +} \ No newline at end of file diff --git a/data/deliveries.json b/data/deliveries.json new file mode 100644 index 0000000..59b98a7 --- /dev/null +++ b/data/deliveries.json @@ -0,0 +1,15 @@ +{ + "deliveries": [ + { + "id": "20a209a9", + "carrier": "UPS", + "retailer": "Tri-Med", + "description": "Tri-Med Medical Supplies Order #3386", + "expected_date": "2026-01-27", + "status": "delivered", + "notes": "Sophia medical supplies. Import fee $43.25 paid.", + "created_at": "2026-02-01T06:30:58.42184085Z", + "updated_at": "2026-02-01T06:30:58.421840934Z" + } + ] +} \ No newline at end of file diff --git a/data/news.json b/data/news.json new file mode 100644 index 0000000..befafd9 --- /dev/null +++ b/data/news.json @@ -0,0 +1,137 @@ +{ + "items": [ + { + "id": "be53de27", + "title": "📧 Mail Agent Complete", + "body": "IMAP triage service fully operational. Service running, webhook to /hooks/mail configured, code pushed to git@zurich.inou.com:mail-agent.git", + "type": "success", + "source": "james", + "timestamp": "2026-01-31T12:18:34.029948077Z" + }, + { + "id": "258d00e1", + "title": "🛡️ SentinelOne (S) Stock", + "body": "$14.06 (-0.07%). Near 52-week low ($13.46). YTD +6.3%, 1yr +41.7%. Short position underwater.", + "type": "error", + "source": "Yahoo Finance", + "timestamp": "2026-01-30T16:44:08.903372981Z" + }, + { + "id": "eb38d258", + "title": "📉 Markets Down", + "body": "Dow -400pts, S\u0026P 500 and Nasdaq falling. Warsh expected to get Fed chair nod. Inflation report weighing on investors. 10-yr Treasury yields rising, gold slumping.", + "type": "warning", + "source": "MarketWatch", + "timestamp": "2026-01-30T16:44:08.883164076Z" + }, + { + "id": "3bd7d16f", + "title": "🥶 Weekend Cold Snap Alert", + "body": "St. Petersburg FL: Sat 36-56°F, Sun 35-46°F. Lows in mid-30s. Protect plants/pets.", + "type": "warning", + "source": "Open-Meteo", + "timestamp": "2026-01-30T16:43:56.172216406Z" + }, + { + "id": "d9d3de6d", + "title": "🦞 OpenClaw Rebrand", + "body": "Project formerly known as Clawdbot → Moltbot → now OpenClaw. Hacker News thread active (posted 4 min ago). IBM Think coverage. Wikipedia updated.", + "type": "info", + "source": "Hacker News/IBM", + "timestamp": "2026-01-30T16:43:56.152204714Z" + }, + { + "id": "ed66fd37", + "title": "🦞 Moltbot → OpenClaw", + "body": "Another rebrand! Now openclaw.ai. 100k+ GitHub stars, 2M visitors/week. \"The lobster has molted into its final form.\"", + "type": "info", + "source": "@openclaw", + "timestamp": "2026-01-30T16:08:10.873278893Z" + }, + { + "id": "9678ef17", + "title": "❄️ Cold Snap This Weekend", + "body": "Unusually cold for Florida. Sat Feb 1: 35-46°F. Sun Feb 2: 41-54°F. Bundle up.", + "type": "warning", + "source": "Weather", + "timestamp": "2026-01-30T15:51:08.892460168Z" + }, + { + "id": "ff3fa10f", + "title": "Moltbot on NYT Hard Fork", + "body": "Podcast feature today. 60k+ GitHub stars. Security researchers flagging exposed admin ports and poisoned skills.", + "type": "info", + "source": "Tech", + "timestamp": "2026-01-30T15:45:17.539675069Z" + }, + { + "id": "b45e3ba0", + "title": "S (SentinelOne) -5.7%", + "body": "Goldman cut PT to $16.50 from $19. Weak Q4 guidance ($271M vs $273.2M expected). Your short winning.", + "type": "success", + "source": "Markets", + "timestamp": "2026-01-30T15:45:17.51939529Z" + }, + { + "id": "0b604cce", + "title": "Gold Crashes -5.7%", + "body": "Dropped from ATH ($5,050) as Kevin Warsh Fed chair pick firms up dollar.", + "type": "warning", + "source": "Markets", + "timestamp": "2026-01-30T15:45:17.495471762Z" + }, + { + "id": "3a0a49bd", + "title": "MoltMobile Ready", + "body": "Android app renamed \u0026 restructured. Audio playback, SUPER ATTENTION MODE 🚨, and Remote Browser implemented. Gateway ready — just needs MiniMax API key.", + "type": "success", + "source": "james", + "timestamp": "2026-01-29T23:00:01.092986645Z" + }, + { + "id": "e60edc2a", + "title": "🦞 Clawdbot → Moltbot", + "body": "Anthropic trademark request. Rebrand happened Jan 27. Security vuln (proxy misconfig) also patched. npm install -g moltbot@latest", + "type": "warning", + "source": "Forbes/Register", + "timestamp": "2026-01-28T23:31:04.72383776Z" + }, + { + "id": "93f95d6d", + "title": "Memory Features Enabled", + "body": "memoryFlush + sessionMemory now active. Auto-save before compaction, search all past sessions.", + "type": "info", + "source": "Config", + "timestamp": "2026-01-28T23:28:10.385545581Z" + }, + { + "id": "cce42405", + "title": "S\u0026P 500 Hits 7,000", + "body": "Historic milestone — first time ever. Tech concentration at 34%.", + "type": "success", + "source": "Markets", + "timestamp": "2026-01-28T23:28:10.369853409Z" + }, + { + "id": "9315a907", + "title": "Dashboard Restructured", + "body": "News on left, tasks on right. News API added.", + "type": "success", + "timestamp": "2026-01-28T23:16:12.826580181Z" + }, + { + "id": "1", + "title": "Dashboard Created", + "body": "Built this dashboard so we can track tasks and I can post updates. You get a Signal ping with a link when something's new.", + "type": "info", + "timestamp": "2026-01-26T19:50:00Z" + }, + { + "id": "2", + "title": "Screenshot Skill Working", + "body": "Screenshot server deployed on Mac Mini. Both instances (James \u0026 CC) can now pull screenshots via HTTP.", + "type": "success", + "timestamp": "2026-01-26T19:45:00Z" + } + ] +} \ No newline at end of file diff --git a/data/tasks.json b/data/tasks.json new file mode 100644 index 0000000..6c1eb7f --- /dev/null +++ b/data/tasks.json @@ -0,0 +1,190 @@ +{ + "tasks": [ + { + "id": "1", + "title": "Flutter Styleguide", + "priority": "high", + "status": "in-progress", + "owner": "johan", + "domain": "inou", + "notes": "Typography section updated. Still need: Settings, Genetics, Notes, Supplements, Peptides, Empty State sections.", + "created_at": "0001-01-01T00:00:00Z", + "updated": "2026-01-28T09:25:23Z" + }, + { + "id": "2", + "title": "Screenshot Skill", + "priority": "medium", + "status": "done", + "owner": "james", + "domain": "Infrastructure", + "notes": "Go server running on Mac Mini :9123. Both James and CC updated.", + "created_at": "0001-01-01T00:00:00Z", + "updated": "2026-01-28T09:25:23Z" + }, + { + "id": "65381e9b", + "title": "Flutter Styleguide - match original", + "priority": "high", + "status": "done", + "owner": "james", + "domain": "inou", + "notes": "Typography fixed to match CSS. Added Settings, Genetics, Supplements, Peptides, Empty State sections.", + "created_at": "2026-01-28T09:38:30.886723188Z", + "updated": "2026-01-30T01:04:17Z" + }, + { + "id": "7e51cdd9", + "title": "Apply new styleguide to all pages", + "priority": "high", + "status": "done", + "owner": "james", + "domain": "inou", + "notes": "Added missing sections to app styleguide:\n- Settings (LLM selector, Units dropdown)\n- Genetics (variants with Ask AI)\n- Notes (photos, timeline entries)\n- Supplements (timing, dosage)\n- Peptides (active/completed cycles)\n- Empty State pattern\n\nBoth design/flutter/ and app/lib/design/ now have complete styleguides matching web.", + "created_at": "2026-01-28T09:38:35.046193253Z", + "updated": "2026-01-30T01:06:21Z" + }, + { + "id": "b31a2233", + "title": "Azure Files Backup - new project", + "priority": "high", + "status": "in-progress", + "owner": "james", + "domain": "Personal", + "notes": "Feature complete! Latest: commit 18ce1fa.\n\n✅ Go 1.22/1.24 installed\n✅ FlatBuffer schema + generated code\n✅ FlatBuffer serializer (full round-trip, ~3μs serialize, ~2μs deserialize)\n✅ Azure SDK integration (real client + upload methods)\n✅ Web UI (Go + htmx + Tailwind)\n✅ Postgres integration (TreeStore methods, connection helper, web UI wired)\n✅ Integration tests (CRUD, tree ops, stats)\n✅ Bug fixes: dirname, scanner path concatenation\n✅ Tree differ: addition detection implemented\n✅ Backup handler: complete (chunking, dedup, XOR hash)\n✅ Restore handler: complete (reassemble, upload to Azure)\n✅ Azure client upload methods (PutFile, CreateDirectory)\n✅ Comprehensive test suite\n✅ All tests passing\n✅ Pushed to git@zurich.inou.com:azure-backup.git\n\n🔲 Azure free trial account (needs Johan)\n🔲 End-to-end test (blocked by Azure)\n\nRun: PATH=/usr/local/go/bin:$PATH go run ./cmd/web --addr=:8080", + "created_at": "2026-01-28T09:38:40.823760714Z", + "updated": "2026-02-01T01:07:12Z" + }, + { + "id": "63b96b35", + "text": "Set up OpenVAS vulnerability scanner", + "title": "OpenVAS", + "priority": "medium", + "status": "pending", + "owner": "johan", + "domain": "Infrastructure", + "created_at": "2026-01-28T10:12:17.26905489Z" + }, + { + "id": "d5fcdd3e", + "text": "Set up Uptime Kuma monitoring", + "title": "Uptime Kuma", + "priority": "medium", + "status": "pending", + "owner": "johan", + "domain": "Infrastructure", + "created_at": "2026-01-28T10:12:17.286579243Z" + }, + { + "id": "69acd59d", + "title": "Get browsing working for Moltbot", + "priority": "high", + "status": "done", + "owner": "james", + "domain": "Infrastructure", + "notes": "Browser was not running. Started clawd profile successfully.\n- browser:start works\n- browser:open works\n- browser:snapshot works\n- Tested on example.com and news.ycombinator.com\n\nBrowser runs headless with Playwright Chromium at /home/johan/.cache/ms-playwright/chromium-1208/\nUser data stored at /home/johan/.clawdbot/browser/clawd/user-data", + "created_at": "2026-01-28T23:31:51.295564296Z", + "updated": "2026-01-30T01:07:02Z" + }, + { + "id": "8d01b5df", + "title": "Add Let's Encrypt cert for OpenVAS (Zurich)", + "priority": "medium", + "status": "done", + "owner": "james", + "domain": "Infrastructure", + "notes": "Installed Caddy 2.10.2 as reverse proxy on Zurich server.\n\nSetup:\n- Caddy auto-obtains LE cert for zurich.inou.com\n- Reverse proxies HTTPS :443 → localhost:9392\n- Certificate auto-renews\n\nNew URL: https://zurich.inou.com (no port needed)\nOld URL: https://zurich.inou.com:9392 (still works with cert warning)", + "created_at": "2026-01-28T23:36:09.91271142Z", + "updated": "2026-01-30T01:08:50Z" + }, + { + "id": "13283514", + "title": "Configure OpenVAS SOC2 scan for inou.com", + "priority": "high", + "status": "done", + "owner": "james", + "domain": "inou", + "notes": "Scan started via Task Wizard. Target: inou.com. Task: Immediate scan of IP inou.com (Status: Requested). Full scan running in OpenVAS at https://zurich.inou.com:9392", + "created_at": "2026-01-28T23:38:17.837376332Z", + "updated": "2026-01-29T10:18:13Z" + }, + { + "id": "690de572", + "title": "Mail Agent - IMAP triage service", + "priority": "medium", + "status": "done", + "owner": "james", + "domain": "Infrastructure", + "notes": "✅ COMPLETE\n\n- IMAP API running (port 8025)\n- IMAP IDLE watching INBOX\n- Webhook to /hooks/mail configured\n- I receive new mail and decide: archive, delete, reply, or escalate\n- Systemd service: mail-agent.service\n\nNo L1 model needed — I handle all triage directly.", + "created_at": "2026-01-31T01:09:07.185371312Z", + "updated": "2026-01-31T12:16:18Z" + }, + { + "id": "3d3e6483", + "text": "Mikhail FiL plan. Do NOT renew - he is leaving in ~1 month.", + "title": "Mint Mobile renewal coming up", + "priority": "low", + "status": "pending", + "owner": "johan", + "domain": "Personal", + "notes": "Email received Jan 26. This is an upsell for 12-month plan. Let it lapse.", + "created_at": "2026-01-31T10:13:26.640600581Z" + }, + { + "id": "bdedb0c2", + "text": "Shipment has import fees that need payment", + "title": "UPS Import Fees Due", + "priority": "high", + "status": "pending", + "owner": "johan", + "domain": "Personal", + "created_at": "2026-01-31T10:14:40.918414184Z" + }, + { + "id": "ad5b1211", + "text": "Mobile phlebotomy invoice received - needs payment", + "title": "Health Link Phlebotomy Invoice #000046", + "priority": "medium", + "status": "pending", + "owner": "johan", + "domain": "Personal", + "created_at": "2026-01-31T10:14:40.926185677Z" + }, + { + "id": "25f4e8ed", + "text": "Add missing headers: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, HSTS, CSP, Referrer-Policy, Permissions-Policy", + "title": "Add security headers to Caddy", + "priority": "high", + "status": "done", + "owner": "james", + "domain": "Infrastructure", + "notes": "Added security headers via Tailscale SSH. Verified with curl.", + "created_at": "2026-01-31T23:50:16.714091775Z", + "updated": "2026-02-01T00:00:45Z" + }, + { + "id": "6aeed4ac", + "text": "Set up ~/dev/docs/soc2/nuclei-scans/ with baseline report from Jan 31", + "title": "Create nuclei reporting structure", + "priority": "medium", + "status": "done", + "owner": "james", + "domain": "Infrastructure", + "notes": "Created ~/dev/docs/soc2/nuclei-scans/2026-01/ with baseline report", + "created_at": "2026-01-31T23:50:16.731454991Z", + "updated": "2026-01-31T23:51:53Z" + }, + { + "id": "270424d3", + "text": "Cron job for Sundays, critical/high templates only", + "title": "Add weekly nuclei light scan", + "priority": "low", + "status": "done", + "owner": "james", + "domain": "Infrastructure", + "notes": "Added nuclei-weekly cron job (Sundays 10am ET)", + "created_at": "2026-01-31T23:50:16.74671578Z", + "updated": "2026-01-31T23:51:53Z" + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2d38e06 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module james-dashboard + +go 1.22.5 + +require github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7790d7c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/index.html b/index.html new file mode 100644 index 0000000..8be86b2 --- /dev/null +++ b/index.html @@ -0,0 +1,606 @@ + + + + + + James Dashboard + + + + + +
+
+

James Dashboard

+
+ Loading... +
+
+ + +
+
+ Loading briefing... +
+
+ + +
+ Agents: + +
+ +
+ +
+
+
+
+
+ Alerts +
+ 0 +
+
+
No alerts
+
+
+ +
+
+
+
+ James — Pending +
+ 0 +
+
+
No pending tasks
+
+
+
+ + +
+
+
+
+
+ Johan — Pending +
+ 0 +
+
+
No pending tasks
+
+
+ +
+
+
+
+ In Progress +
+ 0 +
+
+
Nothing in progress
+
+
+
+ + +
+
+
+
+
+ Done + +
+ 0 +
+
+
No completed tasks
+
+
+
+
+
+ + + + diff --git a/james-dashboard b/james-dashboard new file mode 100755 index 0000000..88d7ab7 Binary files /dev/null and b/james-dashboard differ diff --git a/server.go b/server.go new file mode 100644 index 0000000..1dc0273 --- /dev/null +++ b/server.go @@ -0,0 +1,1065 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + "sort" + "strings" + "sync" + "syscall" + "time" + + "github.com/google/uuid" +) + +// ============================================ +// TASKS +// ============================================ + +type Task struct { + ID string `json:"id"` + Text string `json:"text,omitempty"` + Title string `json:"title,omitempty"` + Priority string `json:"priority"` + Status string `json:"status,omitempty"` + Owner string `json:"owner,omitempty"` // "johan" or "james" + Domain string `json:"domain,omitempty"` // Kaseya, inou, Sophia, ClawdNode, Infrastructure, etc. + Notes string `json:"notes,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + Updated string `json:"updated,omitempty"` +} + +type TaskStore struct { + Tasks []Task `json:"tasks"` + mu sync.RWMutex + path string +} + +func NewTaskStore(path string) (*TaskStore, error) { + store := &TaskStore{path: path, Tasks: []Task{}} + + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return store, store.save() + } + return nil, err + } + + var wrapper struct { + Tasks []Task `json:"tasks"` + } + if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Tasks != nil { + store.Tasks = wrapper.Tasks + return store, nil + } + + if err := json.Unmarshal(data, &store.Tasks); err != nil { + store.Tasks = []Task{} + return store, store.save() + } + + return store, nil +} + +func (s *TaskStore) save() error { + wrapper := struct { + Tasks []Task `json:"tasks"` + }{Tasks: s.Tasks} + data, err := json.MarshalIndent(wrapper, "", " ") + if err != nil { + return err + } + return os.WriteFile(s.path, data, 0644) +} + +func (s *TaskStore) Add(task Task) (Task, error) { + s.mu.Lock() + defer s.mu.Unlock() + + task.ID = uuid.New().String()[:8] + task.CreatedAt = time.Now() + if task.Priority == "" { + task.Priority = "medium" + } + if task.Status == "" { + task.Status = "pending" + } + if task.Owner == "" { + task.Owner = "james" + } + + s.Tasks = append(s.Tasks, task) + return task, s.save() +} + +func (s *TaskStore) Update(id string, updates Task) (Task, bool) { + s.mu.Lock() + defer s.mu.Unlock() + + for i, task := range s.Tasks { + if task.ID == id { + if updates.Title != "" { + s.Tasks[i].Title = updates.Title + } + if updates.Text != "" { + s.Tasks[i].Text = updates.Text + } + if updates.Priority != "" { + s.Tasks[i].Priority = updates.Priority + } + if updates.Status != "" { + s.Tasks[i].Status = updates.Status + } + if updates.Owner != "" { + s.Tasks[i].Owner = updates.Owner + } + if updates.Domain != "" { + s.Tasks[i].Domain = updates.Domain + } + if updates.Notes != "" { + s.Tasks[i].Notes = updates.Notes + } + s.Tasks[i].Updated = time.Now().Format(time.RFC3339) + s.save() + return s.Tasks[i], true + } + } + return Task{}, false +} + +func (s *TaskStore) List() []Task { + s.mu.RLock() + defer s.mu.RUnlock() + return s.Tasks +} + +func (s *TaskStore) Delete(id string) bool { + s.mu.Lock() + defer s.mu.Unlock() + + for i, task := range s.Tasks { + if task.ID == id { + s.Tasks = append(s.Tasks[:i], s.Tasks[i+1:]...) + s.save() + return true + } + } + return false +} + +// ============================================ +// NEWS +// ============================================ + +type NewsItem struct { + ID string `json:"id"` + Title string `json:"title"` + Body string `json:"body"` + Type string `json:"type"` // info, success, warning, error + Source string `json:"source,omitempty"` + URL string `json:"url,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + +type NewsStore struct { + Items []NewsItem `json:"items"` + mu sync.RWMutex + path string +} + +func NewNewsStore(path string) (*NewsStore, error) { + store := &NewsStore{path: path, Items: []NewsItem{}} + + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return store, store.save() + } + return nil, err + } + + var wrapper struct { + Items []NewsItem `json:"items"` + } + if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Items != nil { + store.Items = wrapper.Items + return store, nil + } + + return store, store.save() +} + +func (s *NewsStore) save() error { + wrapper := struct { + Items []NewsItem `json:"items"` + }{Items: s.Items} + data, err := json.MarshalIndent(wrapper, "", " ") + if err != nil { + return err + } + return os.WriteFile(s.path, data, 0644) +} + +func (s *NewsStore) Add(item NewsItem) (NewsItem, error) { + s.mu.Lock() + defer s.mu.Unlock() + + item.ID = uuid.New().String()[:8] + item.Timestamp = time.Now() + if item.Type == "" { + item.Type = "info" + } + + // Prepend (newest first) + s.Items = append([]NewsItem{item}, s.Items...) + + // Keep only last 50 items + if len(s.Items) > 50 { + s.Items = s.Items[:50] + } + + return item, s.save() +} + +func (s *NewsStore) List(limit int) []NewsItem { + s.mu.RLock() + defer s.mu.RUnlock() + + if limit > 0 && limit < len(s.Items) { + return s.Items[:limit] + } + return s.Items +} + +func (s *NewsStore) Delete(id string) bool { + s.mu.Lock() + defer s.mu.Unlock() + + for i, item := range s.Items { + if item.ID == id { + s.Items = append(s.Items[:i], s.Items[i+1:]...) + s.save() + return true + } + } + return false +} + +func (s *NewsStore) Clear() error { + s.mu.Lock() + defer s.mu.Unlock() + s.Items = []NewsItem{} + return s.save() +} + +// ============================================ +// BRIEFINGS +// ============================================ + +type Briefing struct { + ID string `json:"id"` + Date string `json:"date"` + Title string `json:"title"` + Weather string `json:"weather,omitempty"` + Markets string `json:"markets,omitempty"` + News string `json:"news,omitempty"` + Tasks string `json:"tasks,omitempty"` + Summary string `json:"summary,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +type BriefingStore struct { + Briefings []Briefing `json:"briefings"` + mu sync.RWMutex + path string +} + +func NewBriefingStore(path string) (*BriefingStore, error) { + store := &BriefingStore{path: path, Briefings: []Briefing{}} + + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return store, store.save() + } + return nil, err + } + + var wrapper struct { + Briefings []Briefing `json:"briefings"` + } + if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Briefings != nil { + store.Briefings = wrapper.Briefings + return store, nil + } + + return store, store.save() +} + +func (s *BriefingStore) save() error { + wrapper := struct { + Briefings []Briefing `json:"briefings"` + }{Briefings: s.Briefings} + data, err := json.MarshalIndent(wrapper, "", " ") + if err != nil { + return err + } + return os.WriteFile(s.path, data, 0644) +} + +func (s *BriefingStore) Add(b Briefing) (Briefing, error) { + s.mu.Lock() + defer s.mu.Unlock() + + b.ID = uuid.New().String()[:8] + b.CreatedAt = time.Now() + if b.Date == "" { + b.Date = time.Now().Format("2006-01-02") + } + if b.Title == "" { + b.Title = "Morning Brief" + } + + s.Briefings = append(s.Briefings, b) + return b, s.save() +} + +func (s *BriefingStore) List(limit int) []Briefing { + s.mu.RLock() + defer s.mu.RUnlock() + + // Sort by date descending + sorted := make([]Briefing, len(s.Briefings)) + copy(sorted, s.Briefings) + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].CreatedAt.After(sorted[j].CreatedAt) + }) + + if limit > 0 && limit < len(sorted) { + return sorted[:limit] + } + return sorted +} + +func (s *BriefingStore) Get(id string) (Briefing, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, b := range s.Briefings { + if b.ID == id { + return b, true + } + } + return Briefing{}, false +} + +func (s *BriefingStore) Delete(id string) bool { + s.mu.Lock() + defer s.mu.Unlock() + + for i, b := range s.Briefings { + if b.ID == id { + s.Briefings = append(s.Briefings[:i], s.Briefings[i+1:]...) + s.save() + return true + } + } + return false +} + +// ============================================ +// DELIVERIES +// ============================================ + +type Delivery struct { + ID string `json:"id"` + Carrier string `json:"carrier,omitempty"` // UPS, FedEx, USPS, etc. + Retailer string `json:"retailer,omitempty"` // Amazon, Nordstrom, etc. + Description string `json:"description"` // What's in the package + TrackingNumber string `json:"tracking_number,omitempty"` + TrackingURL string `json:"tracking_url,omitempty"` + ExpectedDate string `json:"expected_date,omitempty"` // "2026-02-03" or "Monday" + Status string `json:"status"` // shipped, in_transit, out_for_delivery, delayed, delivered + Notes string `json:"notes,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type DeliveryStore struct { + Deliveries []Delivery `json:"deliveries"` + mu sync.RWMutex + path string +} + +func NewDeliveryStore(path string) (*DeliveryStore, error) { + store := &DeliveryStore{path: path, Deliveries: []Delivery{}} + + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return store, store.save() + } + return nil, err + } + + var wrapper struct { + Deliveries []Delivery `json:"deliveries"` + } + if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Deliveries != nil { + store.Deliveries = wrapper.Deliveries + return store, nil + } + + return store, store.save() +} + +func (s *DeliveryStore) save() error { + wrapper := struct { + Deliveries []Delivery `json:"deliveries"` + }{Deliveries: s.Deliveries} + data, err := json.MarshalIndent(wrapper, "", " ") + if err != nil { + return err + } + return os.WriteFile(s.path, data, 0644) +} + +func (s *DeliveryStore) Add(d Delivery) (Delivery, error) { + s.mu.Lock() + defer s.mu.Unlock() + + d.ID = uuid.New().String()[:8] + d.CreatedAt = time.Now() + d.UpdatedAt = time.Now() + if d.Status == "" { + d.Status = "shipped" + } + + s.Deliveries = append(s.Deliveries, d) + return d, s.save() +} + +func (s *DeliveryStore) Update(id string, updates Delivery) (Delivery, bool) { + s.mu.Lock() + defer s.mu.Unlock() + + for i, d := range s.Deliveries { + if d.ID == id { + if updates.Carrier != "" { + s.Deliveries[i].Carrier = updates.Carrier + } + if updates.Retailer != "" { + s.Deliveries[i].Retailer = updates.Retailer + } + if updates.Description != "" { + s.Deliveries[i].Description = updates.Description + } + if updates.TrackingNumber != "" { + s.Deliveries[i].TrackingNumber = updates.TrackingNumber + } + if updates.TrackingURL != "" { + s.Deliveries[i].TrackingURL = updates.TrackingURL + } + if updates.ExpectedDate != "" { + s.Deliveries[i].ExpectedDate = updates.ExpectedDate + } + if updates.Status != "" { + s.Deliveries[i].Status = updates.Status + } + if updates.Notes != "" { + s.Deliveries[i].Notes = updates.Notes + } + s.Deliveries[i].UpdatedAt = time.Now() + s.save() + return s.Deliveries[i], true + } + } + return Delivery{}, false +} + +func (s *DeliveryStore) List() []Delivery { + s.mu.RLock() + defer s.mu.RUnlock() + + // Return non-delivered items, sorted by expected date + var active []Delivery + for _, d := range s.Deliveries { + if d.Status != "delivered" { + active = append(active, d) + } + } + + sort.Slice(active, func(i, j int) bool { + return active[i].ExpectedDate < active[j].ExpectedDate + }) + + return active +} + +func (s *DeliveryStore) ListAll() []Delivery { + s.mu.RLock() + defer s.mu.RUnlock() + return s.Deliveries +} + +func (s *DeliveryStore) Get(id string) (Delivery, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, d := range s.Deliveries { + if d.ID == id { + return d, true + } + } + return Delivery{}, false +} + +func (s *DeliveryStore) Delete(id string) bool { + s.mu.Lock() + defer s.mu.Unlock() + + for i, d := range s.Deliveries { + if d.ID == id { + s.Deliveries = append(s.Deliveries[:i], s.Deliveries[i+1:]...) + s.save() + return true + } + } + return false +} + +// Find by tracking number or description match +func (s *DeliveryStore) FindByTracking(tracking string) (Delivery, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, d := range s.Deliveries { + if d.TrackingNumber == tracking { + return d, true + } + } + return Delivery{}, false +} + +// ============================================ +// AGENTS (reads from OpenClaw config) +// ============================================ + +type Agent struct { + ID string `json:"id"` + Name string `json:"name"` + Emoji string `json:"emoji"` + URL string `json:"url"` + Default bool `json:"default,omitempty"` +} + +func getAgents(gatewayIP string, gatewayPort string) []Agent { + // Read OpenClaw config + home, _ := os.UserHomeDir() + configPath := filepath.Join(home, ".openclaw", "openclaw.json") + + data, err := os.ReadFile(configPath) + if err != nil { + log.Printf("Failed to read openclaw config: %v", err) + return []Agent{} + } + + var config struct { + Agents struct { + List []struct { + ID string `json:"id"` + Name string `json:"name"` + Identity struct { + Name string `json:"name"` + Emoji string `json:"emoji"` + } `json:"identity"` + } `json:"list"` + } `json:"agents"` + } + + if err := json.Unmarshal(data, &config); err != nil { + log.Printf("Failed to parse openclaw config: %v", err) + return []Agent{} + } + + var agents []Agent + baseURL := "http://" + gatewayIP + ":" + gatewayPort + + for _, agentConfig := range config.Agents.List { + agent := Agent{ + ID: agentConfig.ID, + Name: agentConfig.Identity.Name, + Emoji: agentConfig.Identity.Emoji, + } + + // Set defaults if missing + if agent.Name == "" { + agent.Name = agentConfig.Name + } + if agent.Name == "" { + agent.Name = agentConfig.ID + } + if agent.Emoji == "" { + agent.Emoji = "🤖" + } + + // Build URL - main is default + if agentConfig.ID == "main" { + agent.Default = true + agent.URL = baseURL + "/" + } else { + agent.URL = baseURL + "/agents/" + agentConfig.ID + } + + agents = append(agents, agent) + } + + // Sort: default first, then alphabetically + sort.Slice(agents, func(i, j int) bool { + if agents[i].Default != agents[j].Default { + return agents[i].Default + } + return agents[i].ID < agents[j].ID + }) + + return agents +} + +// ============================================ +// MAIN +// ============================================ + +func main() { + port := flag.String("port", "9200", "port to listen on") + dir := flag.String("dir", ".", "directory to serve") + gatewayIP := flag.String("gateway-ip", "192.168.1.16", "OpenClaw gateway IP") + gatewayPort := flag.String("gateway-port", "18789", "OpenClaw gateway port") + flag.Parse() + + // Initialize stores + taskStore, err := NewTaskStore(filepath.Join(*dir, "data", "tasks.json")) + if err != nil { + log.Fatalf("Failed to initialize task store: %v", err) + } + + briefingStore, err := NewBriefingStore(filepath.Join(*dir, "data", "briefings.json")) + if err != nil { + log.Fatalf("Failed to initialize briefing store: %v", err) + } + + newsStore, err := NewNewsStore(filepath.Join(*dir, "data", "news.json")) + if err != nil { + log.Fatalf("Failed to initialize news store: %v", err) + } + + deliveryStore, err := NewDeliveryStore(filepath.Join(*dir, "data", "deliveries.json")) + if err != nil { + log.Fatalf("Failed to initialize delivery store: %v", err) + } + + mux := http.NewServeMux() + + // CORS middleware helper + cors := func(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + } + + // ========== TASKS API ========== + mux.HandleFunc("/api/tasks", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + switch r.Method { + case "GET": + json.NewEncoder(w).Encode(map[string]interface{}{ + "tasks": taskStore.List(), + }) + + case "POST": + var task Task + if err := json.NewDecoder(r.Body).Decode(&task); err != nil { + http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest) + return + } + if task.Text == "" && task.Title == "" { + http.Error(w, `{"error": "text or title required"}`, http.StatusBadRequest) + return + } + newTask, err := taskStore.Add(task) + if err != nil { + http.Error(w, `{"error": "failed to save task"}`, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(newTask) + + default: + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // Task by ID (DELETE, PATCH) + mux.HandleFunc("/api/tasks/", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + id := strings.TrimPrefix(r.URL.Path, "/api/tasks/") + if id == "" { + http.Error(w, `{"error": "task id required"}`, http.StatusBadRequest) + return + } + + switch r.Method { + case "DELETE": + if taskStore.Delete(id) { + json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "id": id}) + } else { + http.Error(w, `{"error": "task not found"}`, http.StatusNotFound) + } + + case "PATCH", "PUT": + var updates Task + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest) + return + } + if updated, ok := taskStore.Update(id, updates); ok { + json.NewEncoder(w).Encode(updated) + } else { + http.Error(w, `{"error": "task not found"}`, http.StatusNotFound) + } + + default: + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // ========== NEWS API ========== + mux.HandleFunc("/api/news", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + switch r.Method { + case "GET": + json.NewEncoder(w).Encode(map[string]interface{}{ + "items": newsStore.List(20), + }) + + case "POST": + var item NewsItem + if err := json.NewDecoder(r.Body).Decode(&item); err != nil { + http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest) + return + } + if item.Title == "" { + http.Error(w, `{"error": "title required"}`, http.StatusBadRequest) + return + } + newItem, err := newsStore.Add(item) + if err != nil { + http.Error(w, `{"error": "failed to save news"}`, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(newItem) + + case "DELETE": + // DELETE /api/news clears all news + if err := newsStore.Clear(); err != nil { + http.Error(w, `{"error": "failed to clear news"}`, http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(map[string]string{"status": "cleared"}) + + default: + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // News by ID + mux.HandleFunc("/api/news/", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + id := strings.TrimPrefix(r.URL.Path, "/api/news/") + if id == "" { + http.Error(w, `{"error": "news id required"}`, http.StatusBadRequest) + return + } + + switch r.Method { + case "DELETE": + if newsStore.Delete(id) { + json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "id": id}) + } else { + http.Error(w, `{"error": "news item not found"}`, http.StatusNotFound) + } + + default: + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // ========== BRIEFINGS API ========== + mux.HandleFunc("/api/briefings", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + switch r.Method { + case "GET": + limit := 30 // default + json.NewEncoder(w).Encode(map[string]interface{}{ + "briefings": briefingStore.List(limit), + }) + + case "POST": + var b Briefing + if err := json.NewDecoder(r.Body).Decode(&b); err != nil { + http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest) + return + } + newBriefing, err := briefingStore.Add(b) + if err != nil { + http.Error(w, `{"error": "failed to save briefing"}`, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(newBriefing) + + default: + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // Briefing by ID + mux.HandleFunc("/api/briefings/", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + id := strings.TrimPrefix(r.URL.Path, "/api/briefings/") + if id == "" { + http.Error(w, `{"error": "briefing id required"}`, http.StatusBadRequest) + return + } + + switch r.Method { + case "GET": + if b, ok := briefingStore.Get(id); ok { + json.NewEncoder(w).Encode(b) + } else { + http.Error(w, `{"error": "briefing not found"}`, http.StatusNotFound) + } + + case "DELETE": + if briefingStore.Delete(id) { + json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "id": id}) + } else { + http.Error(w, `{"error": "briefing not found"}`, http.StatusNotFound) + } + + default: + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // ========== DELIVERIES API ========== + mux.HandleFunc("/api/deliveries", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + switch r.Method { + case "GET": + // ?all=true returns everything including delivered + if r.URL.Query().Get("all") == "true" { + json.NewEncoder(w).Encode(map[string]interface{}{ + "deliveries": deliveryStore.ListAll(), + }) + } else { + json.NewEncoder(w).Encode(map[string]interface{}{ + "deliveries": deliveryStore.List(), + }) + } + + case "POST": + var d Delivery + if err := json.NewDecoder(r.Body).Decode(&d); err != nil { + http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest) + return + } + if d.Description == "" { + http.Error(w, `{"error": "description required"}`, http.StatusBadRequest) + return + } + newDelivery, err := deliveryStore.Add(d) + if err != nil { + http.Error(w, `{"error": "failed to save delivery"}`, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(newDelivery) + + default: + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // Delivery by ID + mux.HandleFunc("/api/deliveries/", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + id := strings.TrimPrefix(r.URL.Path, "/api/deliveries/") + if id == "" { + http.Error(w, `{"error": "delivery id required"}`, http.StatusBadRequest) + return + } + + switch r.Method { + case "GET": + if d, ok := deliveryStore.Get(id); ok { + json.NewEncoder(w).Encode(d) + } else { + http.Error(w, `{"error": "delivery not found"}`, http.StatusNotFound) + } + + case "PATCH", "PUT": + var updates Delivery + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest) + return + } + if updated, ok := deliveryStore.Update(id, updates); ok { + json.NewEncoder(w).Encode(updated) + } else { + http.Error(w, `{"error": "delivery not found"}`, http.StatusNotFound) + } + + case "DELETE": + if deliveryStore.Delete(id) { + json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "id": id}) + } else { + http.Error(w, `{"error": "delivery not found"}`, http.StatusNotFound) + } + + default: + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // ========== AGENTS API ========== + mux.HandleFunc("/api/agents", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + + if r.Method == "GET" { + agents := getAgents(*gatewayIP, *gatewayPort) + json.NewEncoder(w).Encode(map[string]interface{}{ + "agents": agents, + "gateway_ip": *gatewayIP, + "gateway_port": *gatewayPort, + }) + } else { + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + } + }) + + // Static files (fallback) + fs := http.FileServer(http.Dir(*dir)) + mux.Handle("/", fs) + + server := &http.Server{ + Addr: "0.0.0.0:" + *port, + Handler: mux, + } + + // Graceful shutdown + go func() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + log.Println("Shutting down gracefully...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + server.Shutdown(ctx) + }() + + log.Printf("James Dashboard serving %s on http://0.0.0.0:%s\n", *dir, *port) + log.Printf("API endpoints:\n") + log.Printf(" Tasks:\n") + log.Printf(" GET /api/tasks - list all tasks\n") + log.Printf(" POST /api/tasks - add task {title, priority, status, owner, notes}\n") + log.Printf(" PATCH /api/tasks/:id - update task\n") + log.Printf(" DELETE /api/tasks/:id - remove task\n") + log.Printf(" News:\n") + log.Printf(" GET /api/news - list news (newest first, max 20)\n") + log.Printf(" POST /api/news - add news {title, body, type, source, url}\n") + log.Printf(" DELETE /api/news - clear all news\n") + log.Printf(" DELETE /api/news/:id - remove news item\n") + log.Printf(" Briefings:\n") + log.Printf(" GET /api/briefings - list briefings (newest first)\n") + log.Printf(" POST /api/briefings - add briefing {title, date, weather, markets, news, summary}\n") + log.Printf(" GET /api/briefings/:id - get single briefing\n") + log.Printf(" DELETE /api/briefings/:id - remove briefing\n") + log.Printf(" Deliveries:\n") + log.Printf(" GET /api/deliveries - list active deliveries (excludes delivered)\n") + log.Printf(" GET /api/deliveries?all=true - list all deliveries\n") + log.Printf(" POST /api/deliveries - add delivery {description, carrier, retailer, tracking_number, expected_date, status}\n") + log.Printf(" GET /api/deliveries/:id - get single delivery\n") + log.Printf(" PATCH /api/deliveries/:id - update delivery\n") + log.Printf(" DELETE /api/deliveries/:id - remove delivery\n") + log.Printf(" Agents:\n") + log.Printf(" GET /api/agents - list OpenClaw agents (reads from config)\n") + log.Printf(" Gateway: http://%s:%s\n", *gatewayIP, *gatewayPort) + + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatal(err) + } +}