diff --git a/public/suite.html b/public/suite.html index 85a18ca..d5927c1 100644 --- a/public/suite.html +++ b/public/suite.html @@ -34,7 +34,7 @@ .su-foot{margin-top:34px;padding-top:14px;border-top:1px solid var(--line);font-size:12.5px;color:var(--muted);line-height:1.6;text-align:center} - +
THE PRODUCT ยท YOUR POSITION IS YOUR LICENSE
diff --git a/server.js b/server.js index 6515449..f929123 100644 --- a/server.js +++ b/server.js @@ -648,8 +648,12 @@ async function handleApi(req,res,pathname){ } if(!d||!d.registered)return json(res,404,{error:'Position not found.'}); const cfg=getConfig(); - const root=Number(cfg.teamRootId||21); - const inOrg=Number(d.id)===root||(Array.isArray(d.uplineChain)&&d.uplineChain.map(Number).includes(root)); + // teamRootId is a comma-separated LIST of team roots (e.g. "21,137,139"). + // Number() on that yields NaN -> every member reads as outside the org, + // which is exactly the bug Marty hit signing in as #21. Parse the list. + const roots=String(cfg.teamRootId||cfg.orgRootId||'21').split(',').map(x=>Number(x.trim())).filter(Boolean); + const chainIds=Array.isArray(d.uplineChain)?d.uplineChain.map(Number):[]; + const inOrg=roots.some(r=>Number(d.id)===r||chainIds.includes(r)); // Team-beta gate: while suiteAllowlist is set (comma-separated ids), // only those positions light up; everyone else sees the beta notice. // Clearing the allowlist opens the Suite to the whole org - no deploy. diff --git a/suite-meter.js b/suite-meter.js new file mode 100644 index 0000000..d73b418 --- /dev/null +++ b/suite-meter.js @@ -0,0 +1,83 @@ +// Circle Suite usage metering. Per-position, per-tool, per-calendar-month +// counters with level-based quotas. Storage matches the house pattern: one +// JSON file in the data volume, read/written on demand (low volume, human +// scale โ€” a member generating copy is not a hot path). +// +// Quotas rise with level because cost should scale with commitment, and the +// meter is ALWAYS visible to the member: no silent throttling, no surprise +// cutoffs. Hitting a cap shows what unlocks more, never a dead end. +'use strict'; +const fs = require('fs'); +const path = require('path'); + +let DATA_DIR = null; +function init(opts) { DATA_DIR = opts.dataDir; } +function file() { return path.join(DATA_DIR, 'suite-usage.json'); } +function readAll() { try { return JSON.parse(fs.readFileSync(file(), 'utf8')); } catch (e) { return {}; } } +function writeAll(v) { try { fs.writeFileSync(file(), JSON.stringify(v)); } catch (e) {} } +function monthKey() { const d = new Date(); return d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0'); } + +// tool -> minimum level, and per-level monthly allowance (index = level-1). +const TOOLS = { + copy: { minLevel: 2, quota: [0, 150, 250, 400, 600, 800, 1000, 1500], label: 'Copy Engine' }, + page: { minLevel: 2, quota: [0, 3, 5, 15, 25, 50, 50, 50], label: 'Page Builder' }, + email: { minLevel: 3, quota: [0, 0, 100, 200, 300, 500, 500, 500], label: 'Email Engine' }, + video: { minLevel: 3, quota: [0, 0, 4, 8, 12, 20, 20, 20], label: 'Video Maker' } +}; + +function quotaFor(tool, level) { + const t = TOOLS[tool]; + if (!t) return 0; + const lv = Math.max(1, Math.min(8, Number(level) || 1)); + if (lv < t.minLevel) return 0; + return t.quota[lv - 1] || 0; +} + +function used(memberId, tool) { + const all = readAll(); + const m = all[monthKey()] || {}; + const rec = m[String(memberId)] || {}; + return Number(rec[tool] || 0); +} + +// Can this position use this tool right now? Returns the full meter state so +// the UI can render it without a second call. +function check(memberId, level, tool) { + const t = TOOLS[tool]; + if (!t) return { allowed: false, reason: 'unknown-tool', used: 0, limit: 0, remaining: 0 }; + const limit = quotaFor(tool, level); + const u = used(memberId, tool); + if (limit <= 0) { + return { allowed: false, reason: 'locked', minLevel: t.minLevel, used: u, limit: 0, remaining: 0, label: t.label }; + } + const remaining = Math.max(0, limit - u); + return { + allowed: remaining > 0, + reason: remaining > 0 ? 'ok' : 'quota', + used: u, limit, remaining, label: t.label, resets: monthKey() + }; +} + +function record(memberId, tool, n) { + const all = readAll(); + const mk = monthKey(); + if (!all[mk]) all[mk] = {}; + const id = String(memberId); + if (!all[mk][id]) all[mk][id] = {}; + all[mk][id][tool] = Number(all[mk][id][tool] || 0) + (Number(n) || 1); + // keep only the last 3 months of counters โ€” the file stays tiny forever + const keep = Object.keys(all).sort().slice(-3); + const trimmed = {}; + keep.forEach(function (k) { trimmed[k] = all[k]; }); + writeAll(trimmed); + return all[mk][id][tool]; +} + +// Every meter for one position โ€” powers the Suite's usage strip. +function meters(memberId, level) { + const out = {}; + Object.keys(TOOLS).forEach(function (tool) { out[tool] = check(memberId, level, tool); }); + return out; +} + +module.exports = { init, check, record, meters, quotaFor, TOOLS };