Admin: award Suite capacity to any member as a team bonus

Marty asked for a way to grant impressions to anybody, labelled as an admin
bonus. Team Grants already did this for leaders, but only within their own
organisation and only out of a level pool.

adminGrant() is deliberately a different thing: it comes from no pool (the
team is not a position and has no allowance), it can reach ANY registered
position rather than only someone downline, and it carries a reason that
survives in the ledger. Recorded with by:0 so nothing downstream mistakes it
for a leader's grant — the member's own page renders those as "the team"
rather than "#0".

The route verifies the position exists on-chain before crediting it; a typo
would otherwise sit in the ledger crediting nobody, and it would be invisible
until someone went looking.

Awards land on top of whatever the level already allows and expire with the
month like every other allowance. The admin page lists what has been awarded
this month, so bonuses stay auditable rather than vanishing once given.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-30 05:40:00 -05:00
parent 59bd4bd4ab
commit 05afe5c5f6
4 changed files with 130 additions and 4 deletions
+39 -1
View File
@@ -112,6 +112,44 @@ function status(memberId, level, preview) {
};
}
// An admin bonus. Distinct from a member grant in three ways: it comes from no
// pool (the team is not a position and has no allowance), it can go to ANY
// member rather than only someone in the granter's own organisation, and it
// carries a note so the reason survives in the ledger. Recorded with by:0 so
// nothing downstream mistakes it for a leader's grant.
function adminGrant(opts) {
const to = Number(opts.to);
const tool = String(opts.tool || '');
const n = Math.floor(Number(opts.n) || 0);
if (!POOLS[tool]) throw new Error('That is not something that can be granted.');
if (!to) throw new Error('Which position is this for?');
if (n <= 0) throw new Error('How much?');
if (n > 1000000) throw new Error('That is more than anyone could use in a month.');
const all = readAll();
const mk = monthKey();
if (!all[mk]) all[mk] = [];
const rec = {
by: 0, to: to, tool: tool, n: n,
admin: true,
note: String(opts.note || '').replace(/\s+/g, ' ').trim().slice(0, 80) || 'Team bonus',
at: new Date().toISOString()
};
all[mk].push(rec);
const keep = Object.keys(all).sort().slice(-3);
const trimmed = {};
keep.forEach(function (k) { trimmed[k] = all[k]; });
writeAll(trimmed);
return rec;
}
// Everything the team has handed out this month, newest first — for the admin
// view, so bonuses are auditable rather than invisible once given.
function adminLog(limit) {
return rows().filter(function (r) { return r.admin; })
.slice(-(Math.max(1, Math.min(200, Number(limit) || 50)))).reverse();
}
// Hand capacity to someone. The caller is responsible for having verified that
// `to` is inside `by`'s organisation — that check needs the chain and lives in
// the route, not here.
@@ -148,4 +186,4 @@ function grant(opts) {
return rec;
}
module.exports = { init, MIN_LEVEL, POOLS, tools, poolFor, status, grant, receivedBy, givenBy, receivedDetail };
module.exports = { init, MIN_LEVEL, POOLS, tools, poolFor, status, grant, adminGrant, adminLog, receivedBy, givenBy, receivedDetail };