T11.2: capture usage events (CR-019)
POST /api/usage/ping writes one page_open UsageEvent per authenticated page load, identity from the session (get_current_user), never from the client. Wired from exactly one place - auth-guard.js's proceed(), after wp-auth-ready - so this can't drift into six separate per-page copies. okta_callback now also writes one login event per sign-in. Verified locally: unauthenticated ping -> 401; a real fake-Okta sign-in writes exactly one login row and one page_open row, no duplicates.
This commit is contained in:
@@ -125,12 +125,40 @@
|
|||||||
// Nothing replaces it. Every signed-in page mounts the drawer, so there is no page
|
// Nothing replaces it. Every signed-in page mounts the drawer, so there is no page
|
||||||
// left that would need a floating fallback pill.
|
// left that would need a floating fallback pill.
|
||||||
|
|
||||||
|
// ── CR-019: usage ping ───────────────────────────────────────────────────
|
||||||
|
// One page_open event per authenticated load, sent from exactly ONE place
|
||||||
|
// (here) rather than from each page's own script - the shared-chrome lesson
|
||||||
|
// S4 and the token-drift lesson S5 both taught this codebase the hard way.
|
||||||
|
// Fire-and-forget: never blocks reveal(), never retries, never surfaces an
|
||||||
|
// error to the person using the app - a missed usage ping is not something
|
||||||
|
// anyone here should notice happening.
|
||||||
|
var TOOL_BY_PAGE = {
|
||||||
|
'index.html': 'launcher',
|
||||||
|
'work-package-suite.html': 'wizard',
|
||||||
|
'wp-creation-index.html': 'creator',
|
||||||
|
'field.html': 'field_view',
|
||||||
|
'admin.html': 'admin',
|
||||||
|
'users.html': 'directory'
|
||||||
|
};
|
||||||
|
function pingUsage() {
|
||||||
|
var page = (location.pathname.split('/').pop() || 'index.html');
|
||||||
|
var tool = TOOL_BY_PAGE[page] || page.replace(/\.html$/, '');
|
||||||
|
try {
|
||||||
|
fetch('/api/usage/ping', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ tool: tool })
|
||||||
|
}).catch(function () {});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
function proceed(user) {
|
function proceed(user) {
|
||||||
clearTimeout(safety);
|
clearTimeout(safety);
|
||||||
window.WP_USER = user;
|
window.WP_USER = user;
|
||||||
reveal();
|
reveal();
|
||||||
if (window.WP_USER) {
|
if (window.WP_USER) {
|
||||||
window.wpFlags(); // start the feature-flag fetch; pages await it as needed
|
window.wpFlags(); // start the feature-flag fetch; pages await it as needed
|
||||||
|
pingUsage();
|
||||||
try { document.dispatchEvent(new CustomEvent('wp-auth-ready', { detail: window.WP_USER })); } catch (e) {}
|
try { document.dispatchEvent(new CustomEvent('wp-auth-ready', { detail: window.WP_USER })); } catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -756,6 +756,11 @@ async def okta_callback(request: Request, db: Session = Depends(get_db)):
|
|||||||
user.failed_attempts = 0
|
user.failed_attempts = 0
|
||||||
user.locked_until = None
|
user.locked_until = None
|
||||||
user.last_login_at = models.utcnow()
|
user.last_login_at = models.utcnow()
|
||||||
|
# CR-019: one login event per Okta sign-in, written here rather than
|
||||||
|
# inferred from session creation elsewhere, so there is exactly one
|
||||||
|
# source of truth for "did this person sign in" - not one per page load
|
||||||
|
# afterward (T11.2 covers that separately, as page_open events).
|
||||||
|
db.add(models.UsageEvent(id=gen_id("uev"), username=user.username, tool="", event="login"))
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
|
|
||||||
@@ -3374,6 +3379,29 @@ def create_feedback(body: CommentIn, user: models.User = Depends(auth.get_curren
|
|||||||
return _save_comment(body, db, user)
|
return _save_comment(body, db, user)
|
||||||
|
|
||||||
|
|
||||||
|
# ── CR-019: usage/activity metrics ──────────────────────────────────────────
|
||||||
|
class UsageIn(BaseModel):
|
||||||
|
tool: str = ""
|
||||||
|
project_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/usage/ping")
|
||||||
|
def usage_ping(body: UsageIn, user: models.User = Depends(auth.get_current_user), db: Session = Depends(get_db)):
|
||||||
|
"""One usage_events row per authenticated page load. Called exactly once,
|
||||||
|
from auth-guard.js after wp-auth-ready fires (T11.2) - never duplicated
|
||||||
|
per page's own script, the same lesson S4's per-page nav already taught
|
||||||
|
this codebase. `user` comes from the session via get_current_user, never
|
||||||
|
from anything the client claims - identity here is a server-enforced
|
||||||
|
fact, matching every other write in this file, not a client-reported one."""
|
||||||
|
tool = (body.tool or "").strip()[:40]
|
||||||
|
db.add(models.UsageEvent(
|
||||||
|
id=gen_id("uev"), username=user.username, project_id=body.project_id,
|
||||||
|
tool=tool, event="page_open",
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/comments")
|
@app.get("/api/comments")
|
||||||
def list_comments(
|
def list_comments(
|
||||||
source: Optional[str] = Query(None),
|
source: Optional[str] = Query(None),
|
||||||
|
|||||||
Reference in New Issue
Block a user