mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +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
@@ -1,8 +1,68 @@
|
||||
<!-- Achievements plugin settings panel. PR1 ships an informational stub; the
|
||||
Privacy opt-in toggle + "Remove me from the wall" button land in PR2. -->
|
||||
<div class="text-sm text-gray-300 space-y-2">
|
||||
<p>Your <strong>Achievements</strong> (skill milestones) and <strong>Feats of Power</strong>
|
||||
(rare activity trophies) live on your <em>Profile</em> page. Everything here is
|
||||
local and private.</p>
|
||||
<p class="text-gray-400">Sharing Feats on the public wall is opt-in and arrives in a later update.</p>
|
||||
<!-- Achievements plugin — Privacy panel (mounts under the Settings "System" tab
|
||||
via settings.category). Owns the wall opt-in toggle (bound to the core
|
||||
`achievements_enabled` setting) + the self-serve "Remove me from the wall"
|
||||
action. Default OFF — nothing publishes until the user opts in. -->
|
||||
<div class="text-sm text-gray-300 space-y-4" data-ach-privacy>
|
||||
<div>
|
||||
<p>Your <strong>Achievements</strong> and <strong>Feats of Power</strong> live on your
|
||||
<em>Profile</em> page and are local & private by default.</p>
|
||||
</div>
|
||||
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input type="checkbox" id="setting-achievements-enabled"
|
||||
class="mt-1 h-4 w-4 rounded border-gray-600 bg-gray-800 text-sky-500 focus:ring-sky-500">
|
||||
<span>
|
||||
<span class="font-medium text-gray-200">Share my Feats of Power on the public wall</span>
|
||||
<span class="block text-gray-400">Publishes only your display name and the rare
|
||||
<strong>Feats</strong> you earn (activity milestones) — never songs, skills, or scores.
|
||||
You can turn this off and remove yourself at any time.</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<button type="button" id="ach-remove-me"
|
||||
class="px-3 py-1.5 rounded-md text-sm border border-red-500/40 text-red-300 hover:bg-red-500/10 transition">
|
||||
Remove me from the wall
|
||||
</button>
|
||||
<span id="ach-remove-status" class="ml-2 text-xs text-gray-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var root = document.currentScript && document.currentScript.previousElementSibling;
|
||||
// The panel re-injects on each Settings entry; bind once per element.
|
||||
var toggle = document.getElementById('setting-achievements-enabled');
|
||||
if (!toggle || toggle.dataset.wired === '1') return;
|
||||
toggle.dataset.wired = '1';
|
||||
|
||||
// Hydrate from the authoritative server setting.
|
||||
fetch('/api/settings').then(function (r) { return r.ok ? r.json() : {}; }).then(function (d) {
|
||||
toggle.checked = d && d.achievements_enabled === true;
|
||||
}).catch(function () { /* offline — leave unchecked */ });
|
||||
|
||||
toggle.addEventListener('change', function () {
|
||||
var on = !!toggle.checked;
|
||||
try { localStorage.setItem('achievementsEnabled', on ? '1' : '0'); } catch (_) {}
|
||||
fetch('/api/settings', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ achievements_enabled: on }),
|
||||
}).catch(function () { /* best-effort; revert on failure */ });
|
||||
});
|
||||
|
||||
var removeBtn = document.getElementById('ach-remove-me');
|
||||
var status = document.getElementById('ach-remove-status');
|
||||
if (removeBtn) {
|
||||
removeBtn.addEventListener('click', function () {
|
||||
removeBtn.disabled = true;
|
||||
if (status) status.textContent = 'Removing…';
|
||||
fetch('/api/plugins/achievements/remove-me', { method: 'POST' })
|
||||
.then(function (r) {
|
||||
if (status) status.textContent = r.ok ? 'Removed. Your Feats stay on your Profile.' : 'Could not reach the server — try again.';
|
||||
})
|
||||
.catch(function () { if (status) status.textContent = 'Offline — queued; it will sync when you reconnect.'; })
|
||||
.finally(function () { removeBtn.disabled = false; });
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user