Add docfast-support CLI tool for FreeScout ticket management

This commit is contained in:
Hoid 2026-02-16 19:55:32 +00:00
parent 8b6712736a
commit 57fa33c6df
4 changed files with 288 additions and 14 deletions

225
bin/docfast-support Executable file
View file

@ -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 <ticket-id>'); 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(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/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 <id> --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 <ticket-id>'); 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 <command> [args]
Commands:
tickets [--status active|pending|closed|spam] List tickets (default: active)
view <ticket-id> View ticket with all messages
reply --ticket <id> --message "..." Send reply to customer
[--draft] Save as internal note instead
[--status active|pending|closed] Set ticket status after reply
close <ticket-id> 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); });

View file

@ -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

View file

@ -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
}

View file

@ -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 <ticket-id> # View ticket + all messages
docfast-support reply --ticket <id> --message "..." # Send reply to customer
docfast-support reply --ticket <id> --message "..." --draft # Save as internal note
docfast-support reply --ticket <id> --message "..." --status closed # Reply + close
docfast-support close <ticket-id> # 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 <id>` 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.