[object Object]

← back to Ticket System

live gdrive ticket export: hourly CSV sync to private Drive (com.steve.ticket-gdrive-sync)

9136e0b441bd296c00f9b1ab3df8d11fe46a5107 · 2026-07-27 19:03:42 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 9136e0b441bd296c00f9b1ab3df8d11fe46a5107
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 27 19:03:42 2026 -0700

    live gdrive ticket export: hourly CSV sync to private Drive (com.steve.ticket-gdrive-sync)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 gdrive-sync.sh   | 16 ++++++++++++++++
 ticket-export.js | 20 ++++++++++++++++++++
 2 files changed, 36 insertions(+)

diff --git a/gdrive-sync.sh b/gdrive-sync.sh
new file mode 100755
index 0000000..441762f
--- /dev/null
+++ b/gdrive-sync.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+# Keep the live Drive ticket export current — regenerate CSV + overwrite the same
+# private Drive file. Steve-approved 2026-07-27 ("keep in a live gdrive"). No public link.
+set -u
+ROOT="$HOME/Projects/ticket-system"
+NODE="$(command -v node || echo /opt/homebrew/bin/node)"
+RCLONE="$(command -v rclone || echo /opt/homebrew/bin/rclone)"
+TMP="$ROOT/tmp-tickets-live.csv"
+LOG="$ROOT/gdrive-sync.log"
+{
+  echo "=== gdrive-sync $(date '+%F %T') ==="
+  "$NODE" "$ROOT/ticket-export.js" > "$TMP" 2>/dev/null && \
+  "$RCLONE" copyto "$TMP" "gdrive:DW-Ticket-Board/tickets-live.csv" && \
+  echo "synced $(( $(wc -l < "$TMP") - 1 )) tickets" || echo "SYNC FAILED"
+  rm -f "$TMP"
+} >> "$LOG" 2>&1
diff --git a/ticket-export.js b/ticket-export.js
new file mode 100644
index 0000000..6b4cd6b
--- /dev/null
+++ b/ticket-export.js
@@ -0,0 +1,20 @@
+#!/usr/bin/env node
+// Export all tickets as CSV (stdout). Used by gdrive-sync.sh to keep the live
+// Drive copy current. 15 cols incl. created/updated/last-action dates + idle.
+const { execSync } = require('child_process');
+const now = Date.now();
+const q = s => { s = (s == null ? '' : String(s)).replace(/\r?\n/g, ' ').replace(/"/g, '""'); return /[",]/.test(s) ? `"${s}"` : s; };
+const fmt = d => { if (!d) return ''; const x = new Date(d); return isNaN(x) ? '' : x.toISOString().slice(0, 16).replace('T', ' '); };
+const tk = JSON.parse(execSync(`curl -s -u admin:DW2024! http://127.0.0.1:9794/api/tickets`, { encoding: 'utf8', maxBuffer: 1 << 26 }) || '[]');
+const ord = { doing: 0, blocked: 1, open: 2, done: 3 };
+tk.sort((a, b) => (ord[a.status] ?? 9) - (ord[b.status] ?? 9) || String(a.id).localeCompare(String(b.id), undefined, { numeric: true }));
+const rows = [['ID', 'Short', 'Status', 'Project', 'Assignee', 'Created By', 'Title', 'Created', 'Updated', 'Last Action', 'Last By', 'Idle (h)', '# Actions', '# Comments', 'Last Action Text']];
+for (const t of tk) {
+  const acts = (t.actions || []).slice().sort((a, b) => +new Date(b.ts) - +new Date(a.ts));
+  const last = acts[0] || null;
+  const idle = last ? ((now - +new Date(last.ts)) / 3600000).toFixed(1) : '';
+  rows.push([t.id, (String(t.id).match(/^(TK-\d+)/) || ['', t.id])[1], t.status, t.project || '', t.assignee || '', t.agent || '',
+    t.title || '', fmt(t.created_at), fmt(t.updated_at), fmt(last?.ts), last?.agent || '', idle,
+    (t.actions || []).length, (t.comments || []).length, (last?.text || '').slice(0, 180)].map(q).join(','));
+}
+process.stdout.write(rows.join('\n') + '\n');

← ae90cac schedule ticket-reaper hourly (com.steve.ticket-reaper, --bl  ·  back to Ticket System  ·  ticket IDs: new tickets start at TK-10000 (floor); legacy + f159cc1 →