Fix circular import, primary-admin NULL trap, and add gateway quota reset
DevPlace CI / test (push) Failing after 58m59s
DevPlace CI / test (push) Failing after 58m59s
Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.
Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.
Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.
Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.
Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.
Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.
Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.
Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.
Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.
Full suite: 2989 passed, 1 skipped.
This commit is contained in:
@@ -279,9 +279,9 @@ export class GatewayAdmin {
|
||||
form.name.scrollIntoView({ block: "center" });
|
||||
}
|
||||
|
||||
async confirmDelete(message) {
|
||||
async confirmAction(message, confirmLabel = "Delete") {
|
||||
if (window.app && window.app.dialog) {
|
||||
return window.app.dialog.confirm({ message, danger: true, confirmLabel: "Delete" });
|
||||
return window.app.dialog.confirm({ message, danger: true, confirmLabel });
|
||||
}
|
||||
return window.confirm(message);
|
||||
}
|
||||
@@ -294,7 +294,7 @@ export class GatewayAdmin {
|
||||
}
|
||||
|
||||
async deleteProvider(name) {
|
||||
if (!(await this.confirmDelete(`Delete provider "${name}"?`))) return;
|
||||
if (!(await this.confirmAction(`Delete provider "${name}"?`))) return;
|
||||
try {
|
||||
await this.remove(`/admin/gateway/providers/${encodeURIComponent(name)}`);
|
||||
this.notify("Provider deleted", "success");
|
||||
@@ -338,7 +338,7 @@ export class GatewayAdmin {
|
||||
}
|
||||
|
||||
async deleteModel(source) {
|
||||
if (!(await this.confirmDelete(`Delete model route "${source}"?`))) return;
|
||||
if (!(await this.confirmAction(`Delete model route "${source}"?`))) return;
|
||||
try {
|
||||
await this.remove(`/admin/gateway/models/${encodeURIComponent(source)}`);
|
||||
this.notify("Model route deleted", "success");
|
||||
@@ -394,6 +394,7 @@ export class GatewayAdmin {
|
||||
<td>${this.escape(r.label || "")}</td>
|
||||
<td class="gw-actions">
|
||||
<button class="admin-btn admin-btn-sm" data-edit-quota='${this.attr(JSON.stringify(r))}'>Edit</button>
|
||||
<button class="admin-btn admin-btn-sm" data-reset-quota='${this.attr(JSON.stringify(r))}'>Reset spend</button>
|
||||
<button class="admin-btn admin-btn-sm admin-btn-danger" data-del-quota="${this.attr(r.uid)}">Delete</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
@@ -443,13 +444,31 @@ export class GatewayAdmin {
|
||||
|
||||
onQuotaRuleClick(event) {
|
||||
const editRaw = event.target.dataset.editQuota;
|
||||
const resetRaw = event.target.dataset.resetQuota;
|
||||
const delUid = event.target.dataset.delQuota;
|
||||
if (editRaw) this.fillQuotaForm(JSON.parse(editRaw));
|
||||
if (resetRaw) this.resetQuotaSpend(JSON.parse(resetRaw));
|
||||
if (delUid) this.deleteQuotaRule(delUid);
|
||||
}
|
||||
|
||||
async resetQuotaSpend(rule) {
|
||||
const label = this.scopeLabel(rule);
|
||||
if (!(await this.confirmAction(`Reset the counted 24h spend for ${label}? The usage history is kept.`, "Reset"))) return;
|
||||
try {
|
||||
await Http.postJson("/admin/gateway/quota-resets", {
|
||||
owner_kind: rule.owner_kind || "",
|
||||
owner_id: rule.owner_id || "",
|
||||
app_reference: rule.app_reference || "",
|
||||
});
|
||||
this.notify("Quota spend reset", "success");
|
||||
await this.reload();
|
||||
} catch (err) {
|
||||
this.notify(err.message || "Reset failed", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async deleteQuotaRule(uid) {
|
||||
if (!(await this.confirmDelete("Delete this quota rule?"))) return;
|
||||
if (!(await this.confirmAction("Delete this quota rule?"))) return;
|
||||
try {
|
||||
await this.remove(`/admin/gateway/quota-rules/${encodeURIComponent(uid)}`);
|
||||
this.notify("Quota rule deleted", "success");
|
||||
|
||||
Reference in New Issue
Block a user