mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
feat(achievements): opt-in, privacy controls & data-min gate (epic PR2) (#591)
Sharing earned Feats on the (forthcoming) public wall is strictly opt-in,
default OFF, with a binding data-minimization contract.
- Onboarding (static/v3/profile.js): a new opt-in step (now a 5-step wizard)
after song-directory / before paths — publishes only display name + earned
Feats, never songs/skills/scores; off by default.
- Settings (plugins/achievements/settings.html, System tab via
settings.category): the same toggle + a "Remove me from the wall" button
(POST remove-me — wipes local synced state offline + enqueues removal).
- Core (server.py): achievements_enabled (bool, default false) in
_default_settings + /api/settings validation + _RESETTABLE_SETTINGS_KEYS;
mirrored to localStorage in app.js loadSettings().
- Data-minimization gate: engine.build_wall_payload is the single explicit-dict
serializer; key-set is EXACTLY {display_name, player_hash, achievement_id,
unlocked_at}, achievement_id always a Feat id. Enqueue is gated on
opted-in AND profile identity (reused player_hash); competency never
enqueues (integration law).
Verified natively: settings round-trip + validation + remove-me; opted-in
activity enqueues exactly one 4-field Feat payload; Playwright confirms the
5-step wizard + opt-in card (default unchecked), zero console errors.
29 plugin tests + new settings tests pass.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
05dd3d227a
commit
287c23a532
@@ -135,6 +135,26 @@ def consecutive_run_length(dates):
|
||||
return best
|
||||
|
||||
|
||||
# ── Data-minimization contract (binding, code-enforced) ──────────────────────
|
||||
# The wall payload key-set is frozen here and asserted by a unit test. The
|
||||
# serializer below is the ONLY way outbound data is built — never dict(row) or
|
||||
# **model — so a stray field cannot leak. Adding a key makes the test go red.
|
||||
WALL_PAYLOAD_KEYS = ("display_name", "player_hash", "achievement_id", "unlocked_at")
|
||||
|
||||
|
||||
def build_wall_payload(display_name, player_hash, achievement_id, unlocked_at):
|
||||
"""Build the EXACT four-field wall payload. ``achievement_id`` must always be
|
||||
a Feat id (the caller only ever invokes this for Feat unlocks — competency
|
||||
never syncs). Explicit literal dict on purpose; do not refactor into a
|
||||
row/model splat."""
|
||||
return {
|
||||
"display_name": display_name,
|
||||
"player_hash": player_hash,
|
||||
"achievement_id": achievement_id,
|
||||
"unlocked_at": unlocked_at,
|
||||
}
|
||||
|
||||
|
||||
def diff_unlocks(prev_tiers, new_tiers):
|
||||
"""Feat ids whose tier advanced (incl. first unlock).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user