@growthub/cli 0.3.58 β†’ 0.3.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/assets/worker-kits/growthub-zernio-social-v1/.env.example +5 -0
  2. package/assets/worker-kits/growthub-zernio-social-v1/QUICKSTART.md +36 -4
  3. package/assets/worker-kits/growthub-zernio-social-v1/bundles/growthub-zernio-social-v1.json +30 -1
  4. package/assets/worker-kits/growthub-zernio-social-v1/docs/growthub-agentic-social-platform-ui-shell.md +134 -0
  5. package/assets/worker-kits/growthub-zernio-social-v1/docs/local-adapters.md +2 -2
  6. package/assets/worker-kits/growthub-zernio-social-v1/growthub-meta/README.md +5 -8
  7. package/assets/worker-kits/growthub-zernio-social-v1/growthub-meta/kit-standard.md +1 -1
  8. package/assets/worker-kits/growthub-zernio-social-v1/kit.json +33 -1
  9. package/assets/worker-kits/growthub-zernio-social-v1/skills.md +1 -1
  10. package/assets/worker-kits/growthub-zernio-social-v1/studio/.env.example +3 -0
  11. package/assets/worker-kits/growthub-zernio-social-v1/studio/dist/assets/index-DTmBMuXr.js +78 -0
  12. package/assets/worker-kits/growthub-zernio-social-v1/studio/dist/assets/index-gHr-nTMF.css +1 -0
  13. package/assets/worker-kits/growthub-zernio-social-v1/studio/dist/index.html +14 -0
  14. package/assets/worker-kits/growthub-zernio-social-v1/studio/index.html +13 -0
  15. package/assets/worker-kits/growthub-zernio-social-v1/studio/package-lock.json +1677 -0
  16. package/assets/worker-kits/growthub-zernio-social-v1/studio/package.json +20 -0
  17. package/assets/worker-kits/growthub-zernio-social-v1/studio/serve.mjs +60 -0
  18. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/App.jsx +130 -0
  19. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/api.js +146 -0
  20. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/app.css +558 -0
  21. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/lib/rules.js +64 -0
  22. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/lib/templates.js +207 -0
  23. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/main.jsx +10 -0
  24. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Accounts.jsx +57 -0
  25. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Agent.jsx +167 -0
  26. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Analytics.jsx +164 -0
  27. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/ApiKeys.jsx +143 -0
  28. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Automations.jsx +122 -0
  29. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/CommentRules.jsx +592 -0
  30. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Compose.jsx +185 -0
  31. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Dashboard.jsx +87 -0
  32. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Inbox.jsx +144 -0
  33. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Queues.jsx +167 -0
  34. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Scheduled.jsx +85 -0
  35. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Sequences.jsx +160 -0
  36. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Templates.jsx +275 -0
  37. package/assets/worker-kits/growthub-zernio-social-v1/studio/vite.config.js +7 -0
  38. package/assets/worker-kits/growthub-zernio-social-v1/workers/zernio-social-operator/CLAUDE.md +3 -3
  39. package/dist/index.js +8541 -850
  40. package/package.json +1 -1
  41. package/assets/worker-kits/growthub-zernio-social-v1/docs/postiz-ui-shell-integration.md +0 -166
@@ -0,0 +1,207 @@
1
+ const STORAGE_KEY = 'zernio_comment_templates_v2';
2
+
3
+ export const TEMPLATE_TYPES = {
4
+ send_dm: { label: 'DM Only', color: 'blue', icon: 'πŸ“©' },
5
+ reply_comment: { label: 'Reply Only', color: 'purple', icon: 'πŸ’¬' },
6
+ both: { label: 'Reply + DM', color: 'green', icon: 'πŸ”' },
7
+ };
8
+
9
+ export const VARIABLES = [
10
+ { token: '{{firstName}}', desc: 'First name of commenter' },
11
+ { token: '{{username}}', desc: 'Platform handle (e.g. @ant0ni0)' },
12
+ { token: '{{triggerWord}}',desc: 'The keyword they typed' },
13
+ { token: '{{postLink}}', desc: 'Link to the original post' },
14
+ { token: '{{profileName}}',desc: 'Your profile/page name' },
15
+ ];
16
+
17
+ export const MATCH_TYPES = {
18
+ contains: 'Contains keyword',
19
+ exact: 'Exact match',
20
+ starts_with: 'Starts with keyword',
21
+ };
22
+
23
+ function read() {
24
+ try {
25
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
26
+ } catch {
27
+ return [];
28
+ }
29
+ }
30
+
31
+ function write(templates) {
32
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
33
+ }
34
+
35
+ export function getTemplates() {
36
+ return read();
37
+ }
38
+
39
+ export function saveTemplate(tpl) {
40
+ const all = read();
41
+ if (tpl.id) {
42
+ const idx = all.findIndex(t => t.id === tpl.id);
43
+ if (idx >= 0) { all[idx] = { ...tpl, updatedAt: new Date().toISOString() }; }
44
+ else all.push({ ...tpl, createdAt: new Date().toISOString() });
45
+ } else {
46
+ all.push({ ...tpl, id: `tpl_${Date.now()}`, createdAt: new Date().toISOString() });
47
+ }
48
+ write(all);
49
+ return all;
50
+ }
51
+
52
+ export function deleteTemplate(id) {
53
+ const all = read().filter(t => t.id !== id);
54
+ write(all);
55
+ return all;
56
+ }
57
+
58
+ export function exportTemplates() {
59
+ const blob = new Blob([JSON.stringify(read(), null, 2)], { type: 'application/json' });
60
+ const url = URL.createObjectURL(blob);
61
+ const a = document.createElement('a');
62
+ a.href = url;
63
+ a.download = `zernio-templates-${new Date().toISOString().slice(0,10)}.json`;
64
+ a.click();
65
+ URL.revokeObjectURL(url);
66
+ }
67
+
68
+ export function importTemplates(json) {
69
+ const imported = JSON.parse(json);
70
+ if (!Array.isArray(imported)) throw new Error('Expected an array of templates');
71
+ const existing = read();
72
+ const merged = [...existing];
73
+ for (const t of imported) {
74
+ if (!merged.find(e => e.id === t.id)) merged.push(t);
75
+ }
76
+ write(merged);
77
+ return merged;
78
+ }
79
+
80
+ export function previewTemplate(body, vars = {}) {
81
+ const sample = {
82
+ firstName: vars.firstName || 'Alex',
83
+ username: vars.username || '@alexsmith',
84
+ triggerWord: vars.triggerWord || 'X',
85
+ postLink: vars.postLink || 'https://x.com/example/status/123',
86
+ profileName: vars.profileName || 'Your Brand',
87
+ };
88
+ return body
89
+ .replace(/{{firstName}}/g, sample.firstName)
90
+ .replace(/{{username}}/g, sample.username)
91
+ .replace(/{{triggerWord}}/g, sample.triggerWord)
92
+ .replace(/{{postLink}}/g, sample.postLink)
93
+ .replace(/{{profileName}}/g, sample.profileName);
94
+ }
95
+
96
+ const CAL = '[BOOK_CALL_LINK]';
97
+
98
+ export const SEED_TEMPLATES = [
99
+
100
+ // ── 1. Winning Static Ads Playbook ──────────────────────────────────────
101
+ {
102
+ id: 'tpl_gh_ads_playbook',
103
+ name: 'Winning Ads Playbook',
104
+ type: 'both',
105
+ keyword_hint: 'ADS, PLAYBOOK, STATIC',
106
+ replyBody: `Sent! πŸ“© Check your DMs {{firstName}} β€” the ads playbook is on its way 🎯`,
107
+ dmBody: `Hey {{firstName}}! Here's the Winning Static Ads Playbook for 2026 πŸ‘‰ https://www.growthub.ai/f/blog/static-ads-2026\n\nQuick one β€” what's your biggest challenge with ads right now? Happy to take a look or chat: ${CAL}`,
108
+ createdAt: new Date().toISOString(),
109
+ },
110
+
111
+ // ── 2. Winning Prompts Playbook ──────────────────────────────────────────
112
+ {
113
+ id: 'tpl_gh_prompts_playbook',
114
+ name: 'Winning Prompts Playbook',
115
+ type: 'both',
116
+ keyword_hint: 'PROMPTS, AI, PLAYBOOK',
117
+ replyBody: `Sent! πŸ“© Your prompts playbook is in your DMs {{firstName}} πŸ€–`,
118
+ dmBody: `Hey {{firstName}}! Here's the Winning Prompts Playbook πŸ‘‰ https://www.growthub.ai/f/playbook\n\nWhich part of your workflow are you trying to speed up with AI? Drop a reply or book 10 mins: ${CAL}`,
119
+ createdAt: new Date().toISOString(),
120
+ },
121
+
122
+ // ── 3. Nano Banana Starter Kit ───────────────────────────────────────────
123
+ {
124
+ id: 'tpl_gh_nano_banana',
125
+ name: 'Nano Banana Starter Kit',
126
+ type: 'both',
127
+ keyword_hint: 'KIT, STARTER, NANO',
128
+ replyBody: `Sent! πŸ“© The Nano Banana Starter Kit is in your DMs {{firstName}} 🍌`,
129
+ dmBody: `Hey {{firstName}}! Here's your Nano Banana Starter Kit πŸ‘‰ https://v0-nano-banana-starter-kit.vercel.app/\n\nWhat are you building? Drop a reply and let's figure it out together, or grab 10 mins: ${CAL}`,
130
+ createdAt: new Date().toISOString(),
131
+ },
132
+
133
+ // ── 4. Free Competitor Ads Report ───────────────────────────────────────
134
+ {
135
+ id: 'tpl_gh_competitor_ads',
136
+ name: 'Free Competitor Ads Report',
137
+ type: 'both',
138
+ keyword_hint: 'COMPETITOR, REPORT, SPY',
139
+ replyBody: `Sent! πŸ“© Your free competitor ads report is in your DMs {{firstName}} πŸ”`,
140
+ dmBody: `Hey {{firstName}}! Grab your free competitor ads report here πŸ‘‰ https://www.growthub.ai/f/winning-ads-signup\n\nWho are you up against right now? Would love to show you what's actually working in your space β€” book 10 mins: ${CAL}`,
141
+ createdAt: new Date().toISOString(),
142
+ },
143
+
144
+ // ── 5. SEO / LLM / GEO Mastersheet ─────────────────────────────────────
145
+ {
146
+ id: 'tpl_gh_seo_mastersheet',
147
+ name: 'SEO Β· LLM Β· GEO Mastersheet',
148
+ type: 'both',
149
+ keyword_hint: 'SEO, LLM, GEO, SEARCH',
150
+ replyBody: `Sent! πŸ“© The SEOΒ·LLMΒ·GEO Mastersheet is in your DMs {{firstName}} πŸ—‚`,
151
+ dmBody: `Hey {{firstName}}! Here's the SEO Β· AEO Β· LLM Β· GEO Mastersheet πŸ‘‰ https://www.notion.so/growthub/SEO-AEO-LLM-GEO-Mastersheet-2e4d28ab978380dbbff0e56e7ee28082\n\nAre you trying to show up in AI search right now? Curious what you're working on β€” or grab 10 mins: ${CAL}`,
152
+ createdAt: new Date().toISOString(),
153
+ },
154
+
155
+ // ── 6. Free SEO / AEO / LLM Audit ──────────────────────────────────────
156
+ {
157
+ id: 'tpl_gh_seo_audit',
158
+ name: 'Free SEO Β· AEO Β· LLM Audit',
159
+ type: 'both',
160
+ keyword_hint: 'AUDIT, FREE, TRAFFIC',
161
+ replyBody: `Sent! πŸ“© Your free SEOΒ·AEO audit is in your DMs {{firstName}} πŸ“Š`,
162
+ dmBody: `Hey {{firstName}}! Run your free SEO Β· AEO Β· LLM audit here πŸ‘‰ https://www.growthub.ai/onboarding-agent\n\nWhat's your current traffic situation? Happy to walk through the results β€” or book 10 mins: ${CAL}`,
163
+ createdAt: new Date().toISOString(),
164
+ },
165
+
166
+ // ── 7. DTC Mega File ────────────────────────────────────────────────────
167
+ {
168
+ id: 'tpl_gh_dtc_mega',
169
+ name: 'DTC Mega File',
170
+ type: 'both',
171
+ keyword_hint: 'DTC, ECOM, BRAND',
172
+ replyBody: `Sent! πŸ“© The DTC Mega File is in your DMs {{firstName}} πŸ“¦`,
173
+ dmBody: `Hey {{firstName}}! Here's the DTC Mega File πŸ‘‰ https://growthub.notion.site/dtc-mega-file\n\nWhat stage is your brand at right now? Drop a reply or let's look at your numbers together: ${CAL}`,
174
+ createdAt: new Date().toISOString(),
175
+ },
176
+
177
+ // ── 8. AI Batch Image Generation Guide ──────────────────────────────────
178
+ {
179
+ id: 'tpl_gh_ai_images',
180
+ name: 'AI Batch Image Generation Guide',
181
+ type: 'both',
182
+ keyword_hint: 'IMAGES, BATCH, CREATIVE, AI',
183
+ replyBody: `Sent! πŸ“© The AI Batch Image Mastery Guide is in your DMs {{firstName}} 🎨`,
184
+ dmBody: `Hey {{firstName}}! Here's the AI Batch Image Generation Mastery Guide πŸ‘‰ https://www.notion.so/growthub/AI-Batch-Image-Generation-Mastery-Guide-303d28ab978380cc89ccef0fccff4d52\n\nAre you already using AI for your creative? Would love to hear what's working β€” or book 10 mins: ${CAL}`,
185
+ createdAt: new Date().toISOString(),
186
+ },
187
+
188
+ // ── 9. 500+ Proven Winning Hooks ────────────────────────────────────────
189
+ {
190
+ id: 'tpl_gh_hooks',
191
+ name: '500+ Proven Winning Hooks',
192
+ type: 'both',
193
+ keyword_hint: 'HOOKS, COPY, CONTENT',
194
+ replyBody: `Sent! πŸ“© 500+ winning hooks are in your DMs {{firstName}} πŸͺ`,
195
+ dmBody: `Hey {{firstName}}! Here's the 500+ Proven Winning Hooks library πŸ‘‰ https://www.notion.so/growthub/2d7d28ab9783802aa48dcda105f8c63f?v=8e6120c3ec8e401daa8eaefad2de89d6\n\nWhat content format are you focused on right now? Happy to help you pick the right hooks β€” or book 10 mins: ${CAL}`,
196
+ createdAt: new Date().toISOString(),
197
+ },
198
+
199
+ ];
200
+
201
+ export function seedIfEmpty() {
202
+ if (!read().length) write(SEED_TEMPLATES);
203
+ }
204
+
205
+ export function forceSeed() {
206
+ write(SEED_TEMPLATES);
207
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.jsx';
4
+ import './app.css';
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
@@ -0,0 +1,57 @@
1
+ import { useApp } from '../App.jsx';
2
+
3
+ const PLT_ICON = { twitter:'𝕏', linkedin:'in', instagram:'πŸ“Έ', facebook:'f', tiktok:'🎡', youtube:'β–Ά', bluesky:'πŸ¦‹', threads:'@', reddit:'r/', pinterest:'P', telegram:'✈', whatsapp:'W' };
4
+ const PLT_BG = { twitter:'#000', linkedin:'#0077b5', instagram:'#e1306c', facebook:'#1877f2', tiktok:'#010101', youtube:'#ff0000', bluesky:'#0085ff', threads:'#101010', reddit:'#ff4500', pinterest:'#e60023', telegram:'#0088cc', whatsapp:'#25d366' };
5
+
6
+ export default function Accounts() {
7
+ const { accounts } = useApp();
8
+
9
+ if (!accounts.length) {
10
+ return (
11
+ <div className="empty">
12
+ <div className="empty-icon">πŸ”—</div>
13
+ <div className="empty-msg">No accounts connected on this profile.<br />Connect platforms via Zernio dashboard β†’ Settings β†’ Accounts.</div>
14
+ </div>
15
+ );
16
+ }
17
+
18
+ return (
19
+ <div>
20
+ <div className="section-title mb16">{accounts.length} Connected Account{accounts.length !== 1 ? 's' : ''}</div>
21
+ <div className="table-wrap">
22
+ <table>
23
+ <thead>
24
+ <tr>
25
+ <th>Platform</th>
26
+ <th>Handle</th>
27
+ <th>Display Name</th>
28
+ <th>Account ID</th>
29
+ <th>Status</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ {accounts.map(a => (
34
+ <tr key={a._id || a.accountId}>
35
+ <td>
36
+ <div className="row">
37
+ <div
38
+ className="platform-icon"
39
+ style={{ background: PLT_BG[a.platform] || '#3f3f46', color: '#fff', width: 28, height: 28, fontSize: 12 }}
40
+ >
41
+ {PLT_ICON[a.platform] || a.platform?.[0]?.toUpperCase()}
42
+ </div>
43
+ <span style={{ textTransform: 'capitalize' }}>{a.platform}</span>
44
+ </div>
45
+ </td>
46
+ <td style={{ fontFamily: 'monospace', color: '#a1a1aa' }}>@{a.username}</td>
47
+ <td>{a.displayName || 'β€”'}</td>
48
+ <td style={{ fontFamily: 'monospace', fontSize: 11, color: '#52525b' }}>{a._id || a.accountId || 'β€”'}</td>
49
+ <td><span className="badge badge-green">Active</span></td>
50
+ </tr>
51
+ ))}
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,167 @@
1
+ import { useState } from 'react';
2
+ import { api, PROFILE_ID } from '../api.js';
3
+ import { useApp } from '../App.jsx';
4
+
5
+ const COMMANDS = [
6
+ { name: '/zernio campaign', desc: 'Full campaign: brief + calendar + publishing plan + captions + optional scheduling' },
7
+ { name: '/zernio calendar', desc: 'Content calendar from an existing brief' },
8
+ { name: '/zernio captions', desc: 'Caption copy deck β€” batch or single platform, 3 variants A/B/C' },
9
+ { name: '/zernio schedule', desc: 'Scheduling manifest from an existing calendar (produces JSON for submission here)' },
10
+ { name: '/zernio queue', desc: 'Define or update a recurring queue (time slots)' },
11
+ { name: '/zernio analytics', desc: 'Analytics briefing from API data or provided metrics' },
12
+ { name: '/zernio inbox', desc: 'Draft replies for DMs, comments, and reviews via unified inbox' },
13
+ { name: '/zernio proposal', desc: 'Client-ready proposal with platform mix and ROI projections' },
14
+ { name: '/zernio platforms', desc: 'Platform coverage report for client context' },
15
+ { name: '/zernio quick', desc: '30-second campaign snapshot for a domain or brand' },
16
+ ];
17
+
18
+ const STATUS = { pending: '⬜', busy: 'πŸ”΅', ok: 'βœ…', err: '❌' };
19
+
20
+ export default function Agent() {
21
+ const { accounts, profile, showToast } = useApp();
22
+ const [json, setJson] = useState('');
23
+ const [rows, setRows] = useState([]);
24
+ const [running, setRunning] = useState(false);
25
+
26
+ const context = `# Zernio Agent Context
27
+ Profile ID : ${PROFILE_ID || '(not set)'}
28
+ Profile : ${profile?.name || 'β€”'}
29
+ Timezone : ${profile?.timezone || 'America/New_York'}
30
+ API Base : https://zernio.com/api/v1
31
+
32
+ ## Connected Accounts
33
+ ${accounts.map(a => `- ${a.platform} | @${a.username} | accountId: ${a._id || a.accountId}`).join('\n') || '(none)'}
34
+
35
+ ## Usage
36
+ Paste a /zernio schedule manifest below and click Submit to push each post to Zernio.
37
+ Or run any /zernio command in Claude with this context block prepended.`;
38
+
39
+ const parse = () => {
40
+ try {
41
+ const parsed = JSON.parse(json);
42
+ const posts = parsed?.zernioSchedulingManifest?.posts
43
+ || parsed?.posts
44
+ || (Array.isArray(parsed) ? parsed : null);
45
+ if (!posts?.length) throw new Error('No posts array found. Expected { zernioSchedulingManifest: { posts: [...] } }');
46
+ return posts;
47
+ } catch (e) {
48
+ showToast(e.message, false);
49
+ return null;
50
+ }
51
+ };
52
+
53
+ const submit = async () => {
54
+ const posts = parse();
55
+ if (!posts) return;
56
+ setRows(posts.map(p => ({ id: p.clientPostId || p.id || Math.random(), label: (p.content || '').slice(0, 60) + '…', status: 'pending', msg: '' })));
57
+ setRunning(true);
58
+
59
+ for (let i = 0; i < posts.length; i++) {
60
+ const p = posts[i];
61
+ setRows(r => r.map((row, j) => j === i ? { ...row, status: 'busy' } : row));
62
+ try {
63
+ const ikey = p.clientPostId || `agent-${Date.now()}-${i}`;
64
+ const body = {
65
+ profileId: p.profileId || PROFILE_ID,
66
+ content: p.content,
67
+ platforms: p.platforms,
68
+ };
69
+ if (p.scheduledFor) body.scheduledFor = p.scheduledFor;
70
+ if (p.queueId) body.queueId = p.queueId;
71
+ if (p.media?.length) body.media = p.media;
72
+ if (p.timezone) body.timezone = p.timezone;
73
+
74
+ const res = await api.createPost(body, ikey);
75
+ const resId = res.id || res._id || 'ok';
76
+ setRows(r => r.map((row, j) => j === i ? { ...row, status: 'ok', msg: `ID: ${resId}` } : row));
77
+ await new Promise(res => setTimeout(res, 400));
78
+ } catch (e) {
79
+ setRows(r => r.map((row, j) => j === i ? { ...row, status: 'err', msg: e.message } : row));
80
+ }
81
+ }
82
+
83
+ setRunning(false);
84
+ showToast('Manifest submission complete');
85
+ };
86
+
87
+ const clear = () => { setJson(''); setRows([]); };
88
+
89
+ const okCount = rows.filter(r => r.status === 'ok').length;
90
+ const errCount = rows.filter(r => r.status === 'err').length;
91
+
92
+ return (
93
+ <div className="agent-layout">
94
+ <div className="cmd-ref">
95
+ <div className="section-title mb16">Command Reference</div>
96
+ {COMMANDS.map(c => (
97
+ <div key={c.name} className="cmd-item">
98
+ <div className="cmd-name">{c.name}</div>
99
+ <div className="cmd-desc">{c.desc}</div>
100
+ </div>
101
+ ))}
102
+
103
+ <hr className="divider" />
104
+ <div className="section-title mb8">Agent Context Block</div>
105
+ <div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 8 }}>Prepend to any Claude prompt</div>
106
+ <div className="context-box">{context}</div>
107
+ <button
108
+ className="btn btn-ghost btn-sm mt8"
109
+ style={{ width: '100%' }}
110
+ onClick={() => { navigator.clipboard.writeText(context); showToast('Copied to clipboard βœ“'); }}
111
+ >
112
+ Copy Context
113
+ </button>
114
+ </div>
115
+
116
+ <div className="manifest-panel">
117
+ <div className="card">
118
+ <div className="section-title mb12">Manifest Submitter</div>
119
+ <div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 10 }}>
120
+ Paste the JSON output from <span style={{ fontFamily: 'monospace', color: 'var(--accentl)' }}>/zernio schedule</span> and submit each post directly to the Zernio API.
121
+ </div>
122
+ <div className="field">
123
+ <label>Scheduling Manifest JSON</label>
124
+ <textarea
125
+ className="textarea"
126
+ style={{ minHeight: 200, fontFamily: 'monospace', fontSize: 12 }}
127
+ placeholder={'{\n "zernioSchedulingManifest": {\n "posts": [...]\n }\n}'}
128
+ value={json}
129
+ onChange={e => setJson(e.target.value)}
130
+ />
131
+ </div>
132
+ <div className="row-end">
133
+ <button className="btn btn-ghost btn-sm" onClick={clear}>Clear</button>
134
+ <button className="btn btn-secondary btn-sm" onClick={() => { const p = parse(); if (p) showToast(`${p.length} post(s) parsed β€” ready to submit`); }}>
135
+ Validate
136
+ </button>
137
+ <button className="btn btn-primary" onClick={submit} disabled={running || !json.trim()}>
138
+ {running ? <><span className="spinner" style={{ marginRight: 7 }} />Submitting…</> : 'Submit Manifest'}
139
+ </button>
140
+ </div>
141
+ </div>
142
+
143
+ {rows.length > 0 && (
144
+ <div className="card">
145
+ <div className="row mb12" style={{ justifyContent: 'space-between' }}>
146
+ <div className="section-title" style={{ marginBottom: 0 }}>Submission Progress</div>
147
+ <div style={{ fontSize: 12, color: 'var(--muted)' }}>
148
+ {okCount}/{rows.length} ok {errCount > 0 && <span style={{ color: 'var(--redl)' }}>Β· {errCount} failed</span>}
149
+ </div>
150
+ </div>
151
+ <div className="manifest-status">
152
+ {rows.map((row, i) => (
153
+ <div key={i} className={`m-row ${row.status}`}>
154
+ <span style={{ fontSize: 16 }}>{STATUS[row.status]}</span>
155
+ <span className="flex1" style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
156
+ {row.label}
157
+ </span>
158
+ {row.msg && <span style={{ fontSize: 11, opacity: 0.8 }}>{row.msg}</span>}
159
+ </div>
160
+ ))}
161
+ </div>
162
+ </div>
163
+ )}
164
+ </div>
165
+ </div>
166
+ );
167
+ }
@@ -0,0 +1,164 @@
1
+ import { useState } from 'react';
2
+ import { api, PROFILE_ID } from '../api.js';
3
+ import { useApp } from '../App.jsx';
4
+
5
+ const today = () => new Date().toISOString().slice(0, 10);
6
+ const daysAgo = (n) => new Date(Date.now() - n * 86400000).toISOString().slice(0, 10);
7
+
8
+ function fmt(n) {
9
+ if (n === undefined || n === null) return 'β€”';
10
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
11
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
12
+ return String(n);
13
+ }
14
+
15
+ function pct(n) {
16
+ if (n === undefined || n === null) return 'β€”';
17
+ return (parseFloat(n) * (n <= 1 ? 100 : 1)).toFixed(2) + '%';
18
+ }
19
+
20
+ export default function Analytics() {
21
+ const { showToast } = useApp();
22
+ const [from, setFrom] = useState(daysAgo(30));
23
+ const [to, setTo] = useState(today());
24
+ const [postData, setPostData] = useState(null);
25
+ const [accountData, setAccountData] = useState(null);
26
+ const [loading, setLoading] = useState(false);
27
+
28
+ const fetch = async () => {
29
+ if (!PROFILE_ID) { showToast('PROFILE_ID not set', false); return; }
30
+ setLoading(true);
31
+ setPostData(null);
32
+ setAccountData(null);
33
+ try {
34
+ const [posts, accounts] = await Promise.all([
35
+ api.getPostAnalytics(PROFILE_ID, from, to),
36
+ api.getAccountAnalytics(PROFILE_ID, from, to),
37
+ ]);
38
+ setPostData(posts);
39
+ setAccountData(accounts);
40
+ } catch (e) {
41
+ showToast(e.message, false);
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ };
46
+
47
+ const posts = postData?.posts || postData?.data || [];
48
+ const accounts = accountData?.accounts || accountData?.data || [];
49
+
50
+ return (
51
+ <div>
52
+ <div className="card mb24">
53
+ <div className="row" style={{ flexWrap: 'wrap', gap: 12 }}>
54
+ <div className="field flex1" style={{ marginBottom: 0 }}>
55
+ <label>From</label>
56
+ <input className="input" type="date" value={from} onChange={e => setFrom(e.target.value)} />
57
+ </div>
58
+ <div className="field flex1" style={{ marginBottom: 0 }}>
59
+ <label>To</label>
60
+ <input className="input" type="date" value={to} onChange={e => setTo(e.target.value)} />
61
+ </div>
62
+ <div style={{ alignSelf: 'flex-end' }}>
63
+ <button className="btn btn-primary" onClick={fetch} disabled={loading}>
64
+ {loading ? <><span className="spinner" style={{ marginRight: 7 }} />Fetching…</> : 'Fetch Analytics'}
65
+ </button>
66
+ </div>
67
+ <div style={{ alignSelf: 'flex-end' }}>
68
+ <button className="btn btn-ghost btn-sm" onClick={() => { setFrom(daysAgo(7)); setTo(today()); }}>Last 7d</button>
69
+ </div>
70
+ <div style={{ alignSelf: 'flex-end' }}>
71
+ <button className="btn btn-ghost btn-sm" onClick={() => { setFrom(daysAgo(30)); setTo(today()); }}>Last 30d</button>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ {!postData && !accountData && !loading && (
77
+ <div className="empty">
78
+ <div className="empty-icon">πŸ“Š</div>
79
+ <div className="empty-msg">Select a date range and click Fetch Analytics.</div>
80
+ </div>
81
+ )}
82
+
83
+ {accountData && (
84
+ <div className="mb24">
85
+ <div className="section-title mb12">Account Summary</div>
86
+ <div className="table-wrap">
87
+ <table>
88
+ <thead>
89
+ <tr>
90
+ <th>Platform</th>
91
+ <th>Impressions</th>
92
+ <th>Reach</th>
93
+ <th>Engagement Rate</th>
94
+ <th>Follower Growth</th>
95
+ <th>Link Clicks</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ {accounts.length ? accounts.map((a, i) => (
100
+ <tr key={i}>
101
+ <td style={{ textTransform: 'capitalize', fontWeight: 500 }}>{a.platform || a.accountId || 'β€”'}</td>
102
+ <td>{fmt(a.impressions)}</td>
103
+ <td>{fmt(a.reach)}</td>
104
+ <td>{pct(a.engagementRate)}</td>
105
+ <td style={{ color: a.followerGrowth > 0 ? 'var(--greenl)' : 'inherit' }}>
106
+ {a.followerGrowth !== undefined ? (a.followerGrowth > 0 ? '+' : '') + fmt(a.followerGrowth) : 'β€”'}
107
+ </td>
108
+ <td>{fmt(a.linkClicks)}</td>
109
+ </tr>
110
+ )) : (
111
+ <tr><td colSpan={6} style={{ textAlign: 'center', color: 'var(--muted)', padding: 20 }}>No account data for this period.</td></tr>
112
+ )}
113
+ </tbody>
114
+ </table>
115
+ </div>
116
+ </div>
117
+ )}
118
+
119
+ {postData && (
120
+ <div>
121
+ <div className="section-title mb12">Per-Post Performance</div>
122
+ <div className="table-wrap">
123
+ <table>
124
+ <thead>
125
+ <tr>
126
+ <th>Post</th>
127
+ <th>Platform</th>
128
+ <th>Scheduled</th>
129
+ <th>Impressions</th>
130
+ <th>Reach</th>
131
+ <th>Eng. Rate</th>
132
+ <th>Likes</th>
133
+ <th>Comments</th>
134
+ <th>Shares</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ {posts.length ? posts.map((p, i) => (
139
+ <tr key={i}>
140
+ <td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
141
+ {p.content?.slice(0, 60) || p.postId || 'β€”'}
142
+ </td>
143
+ <td style={{ textTransform: 'capitalize' }}>{p.platform || 'β€”'}</td>
144
+ <td style={{ fontSize: 11, color: 'var(--muted)' }}>
145
+ {p.scheduledFor ? new Date(p.scheduledFor).toLocaleDateString() : 'β€”'}
146
+ </td>
147
+ <td>{fmt(p.impressions)}</td>
148
+ <td>{fmt(p.reach)}</td>
149
+ <td>{pct(p.engagementRate)}</td>
150
+ <td>{fmt(p.likes)}</td>
151
+ <td>{fmt(p.comments)}</td>
152
+ <td>{fmt(p.shares)}</td>
153
+ </tr>
154
+ )) : (
155
+ <tr><td colSpan={9} style={{ textAlign: 'center', color: 'var(--muted)', padding: 20 }}>No post data for this period.</td></tr>
156
+ )}
157
+ </tbody>
158
+ </table>
159
+ </div>
160
+ </div>
161
+ )}
162
+ </div>
163
+ );
164
+ }