← back to Norma
agents/instagram-agent/account-notes.js
84 lines
/**
* account-notes.js — per-account "what to post next from DW Shopify" planning notes for
* norma-instagram (:9810).
*
* Lets Steve attach a free-text brief to EACH of the 35 Instagram accounts telling that account
* what to post next from the DW Shopify store (product handles, collections, themes, timing…).
* The notes are a human-authored PLAN, read by whoever composes the next posts.
*
* Storage: a SINGLE data/account-notes.json keyed by ig_user_id — deliberately NOT stored inside
* the per-account live-media caches, because those get overwritten wholesale by every refresh
* sweep (which would clobber the notes). Keeping the plan in its own file makes it survive refreshes.
*
* Security: only accounts present in the roster (accounts.json + env default) can be written — an
* arbitrary ig_user_id is rejected, matching live-media.js.
*
* Routes:
* GET /notes → planning board (public/notes-viewer.html)
* GET /api/live/account-notes[?account=<id>] → all notes (or one)
* POST /api/live/account-notes {account, note} → save/overwrite one account's note
*/
const fs = require('fs');
const path = require('path');
const { listAccounts } = require('./live-media');
const DATA = path.join(__dirname, 'data');
const NOTES_FILE = path.join(DATA, 'account-notes.json');
const VIEWER = path.join(__dirname, 'public', 'notes-viewer.html');
const MAX_NOTE_LENGTH = 4000; // per-account note character cap
function readNotes() {
try { return JSON.parse(fs.readFileSync(NOTES_FILE, 'utf8')); } catch { return {}; }
}
function writeNotes(obj) {
try { fs.mkdirSync(DATA, { recursive: true }); } catch { /* ignore */ }
fs.writeFileSync(NOTES_FILE, JSON.stringify(obj, null, 2));
}
function rosterIds() {
const s = new Set();
for (const a of listAccounts()) s.add(a.ig_user_id);
return s;
}
function registerNotesRoutes(app) {
app.get('/notes', (req, res) => {
if (!fs.existsSync(VIEWER)) return res.status(404).send('notes-viewer.html missing');
res.type('html').send(fs.readFileSync(VIEWER, 'utf8'));
});
// Read: all notes, or one account's, joined onto the roster so the board can render every account.
app.get('/api/live/account-notes', (req, res) => {
try {
const notes = readNotes();
if (req.query.account) {
const id = String(req.query.account);
return res.json({ account: id, ...(notes[id] || { note: '', updated_at: null }) });
}
const accounts = listAccounts().map((a) => ({
handle: a.handle, ig_user_id: a.ig_user_id, page_name: a.page_name || '',
note: notes[a.ig_user_id]?.note || '',
updated_at: notes[a.ig_user_id]?.updated_at || null,
}));
res.json({ accounts });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Save: overwrite one account's note. Reversible — it's a single keyed value; clearing = empty note.
app.post('/api/live/account-notes', (req, res) => {
try {
const b = req.body || {};
const id = String(b.account || '');
if (!rosterIds().has(id)) return res.status(400).json({ error: 'unknown account (not in roster)' });
const note = typeof b.note === 'string' ? b.note.slice(0, MAX_NOTE_LENGTH) : '';
const notes = readNotes();
if (note.trim() === '') delete notes[id]; // empty = clear the note
else notes[id] = { note, updated_at: new Date().toISOString() };
writeNotes(notes);
res.json({ ok: true, account: id, note, updated_at: notes[id] ? notes[id].updated_at : null });
} catch (e) { res.status(500).json({ error: e.message }); }
});
}
module.exports = { registerNotesRoutes, readNotes };