Fix Suite org check: teamRootId is a comma-separated list, Number() made it NaN so every member read as outside the org (Marty hit this as #21); parse all roots + orgRootId fallback. Nav dedupe on /suite. Adds suite-meter module (per-position monthly quotas by level)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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}
|
||||
</style></head>
|
||||
<body>
|
||||
<header class="wrap nav"><a class="brand" href="/"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span id="brandName">RM Circle</span><small>The Circle Suite</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/tools">Promo Tools</a><a class="btn btn-primary" href="/my">My Dashboard</a></div></header>
|
||||
<header class="wrap nav"><a class="brand" href="/"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span id="brandName">RM Circle</span><small>The Circle Suite</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/tools">Promo Tools</a><a class="btn btn-primary" href="/">Home</a></div></header>
|
||||
<main class="su-wrap">
|
||||
<div class="su-hero">
|
||||
<div class="eyebrow" style="color:var(--gold)">THE PRODUCT · YOUR POSITION IS YOUR LICENSE</div>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user