From 57fa33c6df10954c934e2781675f4599ea7da503 Mon Sep 17 00:00:00 2001 From: Hoid Date: Mon, 16 Feb 2026 19:55:32 +0000 Subject: [PATCH] Add docfast-support CLI tool for FreeScout ticket management --- bin/docfast-support | 225 +++++++++++++++++++++++++++ projects/business/memory/sessions.md | 34 ++++ projects/business/memory/state.json | 17 +- skills/business/SKILL.md | 26 +++- 4 files changed, 288 insertions(+), 14 deletions(-) create mode 100755 bin/docfast-support diff --git a/bin/docfast-support b/bin/docfast-support new file mode 100755 index 0000000..76d29e4 --- /dev/null +++ b/bin/docfast-support @@ -0,0 +1,225 @@ +#!/usr/bin/env node +// DocFast Support CLI — FreeScout API client for support ticket management +// Env vars (in .credentials/docfast.env): +// FREESCOUT_URL — FreeScout instance URL (e.g. https://support.cloonar.com) +// FREESCOUT_API_KEY — API key (FreeScout → Manage → API Keys) +// FREESCOUT_MAILBOX_ID — Mailbox ID for support@docfast.dev + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const http = require('http'); + +const credFile = process.env.CRED_FILE || path.join(__dirname, '..', '.credentials', 'docfast.env'); +const env = {}; +if (fs.existsSync(credFile)) { + fs.readFileSync(credFile, 'utf8').split('\n').forEach(l => { + const m = l.match(/^([A-Z_]+)=(.+)$/); + if (m) env[m[1]] = m[2].trim(); + }); +} + +const BASE_URL = env.FREESCOUT_URL; +const API_KEY = env.FREESCOUT_API_KEY; +const MAILBOX_ID = env.FREESCOUT_MAILBOX_ID; + +if (!BASE_URL || !API_KEY) { + console.error('Missing FREESCOUT_URL or FREESCOUT_API_KEY in credentials.'); + process.exit(1); +} + +// --- HTTP helper --- +function apiRequest(method, endpoint, body = null) { + return new Promise((resolve, reject) => { + const url = new URL(`/api${endpoint}`, BASE_URL); + const mod = url.protocol === 'https:' ? https : http; + const opts = { + method, + hostname: url.hostname, + port: url.port, + path: url.pathname + url.search, + headers: { + 'X-FreeScout-API-Key': API_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + }; + const req = mod.request(opts, res => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: JSON.parse(data) }); } + catch { resolve({ status: res.statusCode, data }); } + }); + }); + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +} + +// --- Commands --- + +async function cmdTickets(args) { + const status = getFlag(args, '--status') || 'active'; // active|pending|closed|spam + const page = getFlag(args, '--page') || '1'; + let endpoint = `/conversations?page=${page}`; + if (MAILBOX_ID) endpoint += `&mailboxId=${MAILBOX_ID}`; + if (status) endpoint += `&status=${status}`; + + const res = await apiRequest('GET', endpoint); + if (res.status !== 200) { console.error('Error:', res.status, JSON.stringify(res.data)); process.exit(1); } + + const conversations = res.data._embedded?.conversations || []; + if (conversations.length === 0) { console.log('No tickets found.'); return; } + + console.log('ID\tSTATUS\tSUBJECT\tCUSTOMER\tCREATED\tTHREADS'); + for (const c of conversations) { + const customer = c.customer?.email || c.customer?.firstName || 'unknown'; + const created = c.createdAt?.split('T')[0] || '-'; + console.log(`${c.id}\t${c.status}\t${c.subject}\t${customer}\t${created}\t${c.threads || '-'}`); + } + if (res.data.page?.totalPages > 1) { + console.log(`\nPage ${res.data.page.current}/${res.data.page.totalPages} (${res.data.page.total} total)`); + } +} + +async function cmdView(args) { + const id = args[0]; + if (!id) { console.error('Usage: docfast-support view '); process.exit(1); } + + const res = await apiRequest('GET', `/conversations/${id}`); + if (res.status !== 200) { console.error('Error:', res.status, JSON.stringify(res.data)); process.exit(1); } + + const c = res.data; + console.log(`Ticket #${c.id}: ${c.subject}`); + console.log(`Status: ${c.status} | Customer: ${c.customer?.email || 'unknown'} | Created: ${c.createdAt}`); + console.log('---'); + + // Get threads (messages) + const tRes = await apiRequest('GET', `/conversations/${id}/threads`); + if (tRes.status === 200) { + const threads = tRes.data._embedded?.threads || []; + for (const t of threads) { + const from = t.createdBy?.email || t.customer?.email || 'system'; + const type = t.type || 'message'; // customer|message|note + const date = t.createdAt?.replace('T', ' ').slice(0, 16) || '-'; + console.log(`\n[${date}] ${type.toUpperCase()} from ${from}:`); + // Strip HTML tags for readability + const body = (t.body || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').trim(); + console.log(body); + } + } +} + +async function cmdReply(args) { + const id = getFlag(args, '--ticket'); + const message = getFlag(args, '--message'); + const draft = args.includes('--draft'); + const status = getFlag(args, '--status'); // active|pending|closed + + if (!id || !message) { + console.error('Usage: docfast-support reply --ticket --message "..." [--draft] [--status active|pending|closed]'); + process.exit(1); + } + + const body = { + type: draft ? 'note' : 'message', + body: message, + status: status || (draft ? undefined : 'active'), + }; + + // If draft, create as internal note (visible to agents only) + if (draft) { + body.type = 'note'; + } + + const res = await apiRequest('POST', `/conversations/${id}/threads`, body); + if (res.status === 201 || res.status === 200) { + console.log(draft ? `Draft note added to ticket #${id}` : `Reply sent to ticket #${id}`); + if (status) console.log(`Status set to: ${status}`); + } else { + console.error('Error:', res.status, JSON.stringify(res.data)); + process.exit(1); + } +} + +async function cmdClose(args) { + const id = args[0]; + if (!id) { console.error('Usage: docfast-support close '); process.exit(1); } + + const res = await apiRequest('PUT', `/conversations/${id}`, { status: 'closed' }); + if (res.status === 200 || res.status === 204) { + console.log(`Ticket #${id} closed.`); + } else { + console.error('Error:', res.status, JSON.stringify(res.data)); + process.exit(1); + } +} + +async function cmdStats() { + const statuses = ['active', 'pending', 'closed']; + console.log('STATUS\tCOUNT'); + for (const s of statuses) { + let endpoint = `/conversations?status=${s}&page=1`; + if (MAILBOX_ID) endpoint += `&mailboxId=${MAILBOX_ID}`; + const res = await apiRequest('GET', endpoint); + const total = res.data?.page?.total || 0; + console.log(`${s}\t${total}`); + } +} + +async function cmdMailboxes() { + const res = await apiRequest('GET', '/mailboxes'); + if (res.status !== 200) { console.error('Error:', res.status, JSON.stringify(res.data)); process.exit(1); } + const mailboxes = res.data._embedded?.mailboxes || []; + console.log('ID\tNAME\tEMAIL'); + for (const m of mailboxes) { + console.log(`${m.id}\t${m.name}\t${m.email || '-'}`); + } +} + +// --- Helpers --- + +function getFlag(args, flag) { + const idx = args.indexOf(flag); + if (idx === -1) return null; + return args[idx + 1] || null; +} + +// --- Main --- + +const [cmd, ...args] = process.argv.slice(2); + +const commands = { + tickets: cmdTickets, + view: cmdView, + reply: cmdReply, + close: cmdClose, + stats: cmdStats, + mailboxes: cmdMailboxes, +}; + +if (!cmd || !commands[cmd]) { + console.log(`DocFast Support — FreeScout ticket management + +Usage: docfast-support [args] + +Commands: + tickets [--status active|pending|closed|spam] List tickets (default: active) + view View ticket with all messages + reply --ticket --message "..." Send reply to customer + [--draft] Save as internal note instead + [--status active|pending|closed] Set ticket status after reply + close Close a ticket + stats Ticket counts by status + mailboxes List mailboxes (find your mailbox ID) + +Env vars (in .credentials/docfast.env): + FREESCOUT_URL FreeScout instance URL + FREESCOUT_API_KEY API key + FREESCOUT_MAILBOX_ID Mailbox ID for filtering (optional)`); + process.exit(0); +} + +commands[cmd](args).catch(e => { console.error(e.message); process.exit(1); }); diff --git a/projects/business/memory/sessions.md b/projects/business/memory/sessions.md index 9e1e1e9..af237df 100644 --- a/projects/business/memory/sessions.md +++ b/projects/business/memory/sessions.md @@ -978,3 +978,37 @@ - **Budget:** €181.71 remaining, Revenue: €9 - **Open bugs:** 0 CRITICAL, 1 HIGH (BUG-049 — investor action needed), 5 MEDIUM, 3 LOW - **Blockers:** BUG-049 requires investor to enable Stripe invoice emails in Dashboard + +## Session 46 — 2026-02-16 19:41 UTC (Monday Evening — Subagent) +- **Server health:** UP, PostgreSQL 16.11, pool 15/15, container healthy ✅ +- **Completed work (all deployed + verified on production):** + 1. ✅ **STATUS PAGE** — Created styled /status page at https://docfast.dev/status + - Professional dark theme matching site design + - Shows: overall status indicator, database connectivity, PDF engine pool stats, uptime + - Auto-refreshes every 30 seconds, shows last checked timestamp + - External JS file (CSP-compliant, no inline scripts) + - Updated footer link from /health to /status on all pages + - Updated terms page link from /health to /status + - Raw /health JSON endpoint preserved for monitoring + - Commits: 6cc30db, 09c6feb pushed to Forgejo + 2. ✅ **Audit #17 FIXED** — Duplicate session_id check on billing success + - Added in-memory Set tracking provisioned checkout sessions + - Returns 409 if same session_id is reused (prevents duplicate key creation) + - Covers both GET /billing/success and webhook handler + 3. ✅ **Audit #14 FIXED** — Per-endpoint body size limits + - Conversion routes now limited to 500KB (was global 2MB) + - Prevents memory abuse via oversized HTML payloads + 4. ✅ **Audit #22 FIXED** — Removed unused `getPoolStats` import from convert.ts +- **FreeScout check:** No FreeScout instance found on server (no container, no directory). FreeScout credentials not in docfast.env. Investor needs to provide FreeScout API URL + key, or set up the instance. +- **BUG-049:** Noted. Invoice toggle is a Stripe Dashboard setting. Requires investor action (Settings → Emails → enable invoice emails). Not blocking launch. +- **Investor Test:** + 1. Trust with money? **Yes** ✅ + 2. Data loss? **Protected** ✅ — Local + off-site BorgBackup + 3. Free tier abuse? **Mitigated** ✅ + 4. Key recovery? **Yes** ✅ + 5. False features? **Clean** ✅ +- **Budget:** €181.71 remaining, Revenue: €9 +- **Open bugs:** 0 CRITICAL, 1 HIGH (BUG-049 — investor action), 3 MEDIUM (#10, #12, #15), 2 LOW (#18, #25) +- **Blockers:** + - BUG-049: Investor needs to enable Stripe invoice emails + - FreeScout: No instance running, need API access or setup instructions diff --git a/projects/business/memory/state.json b/projects/business/memory/state.json index fff5c1b..c99715b 100644 --- a/projects/business/memory/state.json +++ b/projects/business/memory/state.json @@ -3,7 +3,7 @@ "phaseLabel": "Build Production-Grade Product", "status": "near-launch-ready", "product": "DocFast \u2014 HTML/Markdown to PDF API", - "currentPriority": "1) BUG-049 invoice fix (investor action). 2) Marketing launch prep. 3) Remaining MEDIUM audit items.", + "currentPriority": "1) BUG-049 invoice fix (investor action). 2) FreeScout setup needed. 3) Remaining MEDIUM audit items (#10, #12, #15). 4) Marketing launch prep.", "ownerDirectives_PRIORITY": "Process these IN ORDER. Do not skip.", "ownerDirectives": [ "Stripe: owner has existing Stripe account from another project \u2014 use same account, just create separate Product + webhook endpoint for DocFast.", @@ -15,7 +15,7 @@ "CI/CD PIPELINE: Forgejo Actions workflow created. Needs 3 repository secrets added in Forgejo settings (SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY).", "REPRODUCIBLE INFRASTRUCTURE: DONE \u2014 setup.sh, docker-compose, configs, disaster recovery docs all in infrastructure/ directory.", "PRO PLAN LIMITS: DONE \u2014 Set to 2,500 PDFs/month at \u20ac9/mo. Competitive with html2pdf.app. Enforced in code, updated on landing page + JSON-LD + Stripe.", - "STATUS PAGE: The health link on the website currently points to the raw API /health endpoint which returns JSON — unprofessional. Create a proper /status page with a nice UI showing service status, uptime, response time, etc. Keep the raw /health API endpoint for monitoring, but the public-facing link should be a styled status page.", + "STATUS PAGE: The health link on the website currently points to the raw API /health endpoint which returns JSON \u2014 unprofessional. Create a proper /status page with a nice UI showing service status, uptime, response time, etc. Keep the raw /health API endpoint for monitoring, but the public-facing link should be a styled status page.", "SUPPORT EMAIL LIVE: support@docfast.dev is now active in FreeScout. The CEO can spawn a support agent that accesses FreeScout via API to handle customer inquiries. Update the website contact/support references to use this address.", "BUG-049 HIGH: Pro customers do not receive an invoice after payment. This is legally required in Austria/EU. Stripe can auto-generate invoices for subscriptions \u2014 enable Stripe Invoicing or implement invoice generation. Customer must receive a proper invoice with: company name, ATU number, invoice number, date, amount, VAT breakdown.", "WEBSITE TEMPLATING: DONE \u2014 Build-time system with partials (nav/footer/styles). Source in public/src/, build with node scripts/build-html.cjs." @@ -51,7 +51,9 @@ "websiteTemplating": true, "websiteTemplatingNote": "Build-time HTML templating with shared nav/footer partials. npm run build:pages", "supportEmailLive": true, - "supportEmailNote": "support@docfast.dev on footer, impressum, terms, openapi.json, landing page" + "supportEmailNote": "support@docfast.dev on footer, impressum, terms, openapi.json, landing page", + "statusPage": true, + "statusPageNote": "Styled /status page live at https://docfast.dev/status. Auto-refreshes, shows DB + pool stats." }, "loadTestResults": { "sequential": "~2.1s per PDF, ~28/min", @@ -99,16 +101,13 @@ "MEDIUM": [ "Audit #10: Usage data written on every request (should batch)", "Audit #12: In-memory caches can diverge from DB", - "Audit #14: No per-endpoint body size limits", - "Audit #15: Browser pool queue no per-key fairness", - "Audit #17: No duplicate session_id check on billing success" + "Audit #15: Browser pool queue no per-key fairness" ], "LOW": [ "Audit #18: Rate limit store potential memory growth", - "Audit #22: Unused import in convert.ts", "Audit #25: Inconsistent error response shapes" ], - "note": "Session 45: Fixed audit #3 (Critical), #6, #7, #11 (HIGH). Added support@docfast.dev to all pages." + "note": "Session 46: Fixed audit #14, #17, #22. Added styled /status page. 3 MEDIUM, 2 LOW remaining." }, "blockers": [], "resolvedBlockers": [ @@ -117,5 +116,5 @@ "Off-site backups \u2014 DONE 2026-02-16, Hetzner Storage Box configured with BorgBackup" ], "startDate": "2026-02-14", - "sessionCount": 45 + "sessionCount": 46 } \ No newline at end of file diff --git a/skills/business/SKILL.md b/skills/business/SKILL.md index a3577b2..29e3034 100644 --- a/skills/business/SKILL.md +++ b/skills/business/SKILL.md @@ -106,11 +106,27 @@ Report EVERY issue to projects/business/memory/bugs.md ## Support Monitoring -Every session, spawn a support agent to check FreeScout for new support requests at support@docfast.dev. The agent should: -1. Check for unread/open tickets -2. Triage and respond to simple inquiries autonomously (API usage questions, docs links, etc.) -3. Escalate complex issues (billing disputes, bugs, feature requests) to the CEO for decision -4. Log all interactions in `projects/business/memory/support-log.md` +Every session, spawn a support agent to check FreeScout for new support requests at support@docfast.dev. + +**Support tool:** `~/.openclaw/workspace/bin/docfast-support` +```bash +docfast-support tickets # List open tickets +docfast-support tickets --status pending # List pending tickets +docfast-support view # View ticket + all messages +docfast-support reply --ticket --message "..." # Send reply to customer +docfast-support reply --ticket --message "..." --draft # Save as internal note +docfast-support reply --ticket --message "..." --status closed # Reply + close +docfast-support close # Close ticket +docfast-support stats # Ticket counts by status +docfast-support mailboxes # List mailboxes +``` + +The support agent should: +1. Run `docfast-support tickets` to check for open tickets +2. `docfast-support view ` to read the full conversation +3. Triage and respond to simple inquiries autonomously (API usage, docs, how-to) +4. For complex issues (billing, bugs, feature requests): create a draft note with analysis, then escalate to CEO +5. Log all interactions in `projects/business/memory/support-log.md` Never let support tickets go unanswered. Response time matters for customer trust.