mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 19:59:35 +00:00
feat(achievements): wall sync drain worker + review fixes (epic PR3) (#592)
* feat(achievements): wall sync drain worker (epic PR3, client side) Background dead-letter worker that POSTs queued Feat unlocks/removals to the hosted feedback-achievements wall. Idle unless FEEDBACK_ACHIEVEMENTS_WALL_URL is set; uses requests + the client-token header (mirrors lyrics_transcribe). Dead-letter, never drop (pure engine.drain_decision): network err / 429 / 5xx -> keep pending (retry) other 4xx -> dead_letter (diagnosable, replayable) 2xx -> delete on server ack remove-me enqueues a wall removal keyed by the reused player_hash. Verified by an end-to-end staging round-trip (earn a Feat -> drains onto the wall with name + short hash -> remove-me -> wall empties) with no IP in tables or access logs. 42 plugin tests pass (test_sync.py adds the decision table + ack/retry/dead-letter retention + four-field on-the-wire payload). The hosted service lives in the new feedback-achievements repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(achievements): address local review findings (epic) Bugs caught in the pre-merge review loop: - secret_witching Feat was DEAD: post_activity wrote witching_nights_run to the DB before snapshotting prev_tiers, so diff_unlocks never saw the fresh unlock. Fold the run into the activity delta instead (same asymmetry chart_encore uses) so the 7th-night unlock is detected. +regression tests. - chart_encore broke across restarts: per-chart counter keyed on abs(hash(str)), which Python salts per-process (PYTHONHASHSEED). Use a stable sha1 digest so the same chart accumulates across sessions. +regression test. - Bounded the per-activity counter read: _read_counters no longer pulls the unbounded chart_plays:* rows (they're bumped/read individually). - screen.js: gate note:hit/miss on an active-song flag so tuner/calibration note events can't inflate Feats or flush a phantom chart:null session. - screen.js: P-III — prefix the plugin localStorage key (achievements:profile-cat). - screen.js: extract the duplicated local-ISO-date helper. 45 plugin tests pass (3 new). Wall-side review fixes are in the feedback-achievements repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(achievements): default the drain worker to the hosted wall Point FEEDBACK_ACHIEVEMENTS_WALL_URL's default at the live got-feedback wall (https://feedback-achievements.onrender.com) so the drain worker targets it out of the box; still env-overridable for self-hosting/staging. Nothing publishes unless the user opted in AND has a profile identity, so a default URL alone sends nothing. Tests disable the default (autouse fixture) so no test ever POSTs to production; drain logic is covered via _drain_once() with an injected poster. 45 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- 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
287c23a532
commit
d2569cc2a8
@@ -33,7 +33,7 @@
|
||||
var registered = {}; // id -> def (contributed + expanded baseline defs)
|
||||
var earned = {}; // id -> { tier, cls, category }
|
||||
var baseline = null; // /catalog baseline blob
|
||||
var CAT_KEY = 'v3-profile-ach-cat';
|
||||
var CAT_KEY = 'achievements:profile-cat'; // P-III: plugin localStorage keys prefixed with plugin id
|
||||
var INSTRUMENTS = ['guitar', 'bass', 'drums', 'keys'];
|
||||
|
||||
function progState() {
|
||||
@@ -172,11 +172,17 @@
|
||||
postUnlock(def, tier).then(function () { refreshEarned().then(scheduleRender); });
|
||||
}
|
||||
|
||||
// Local calendar date 'YYYY-MM-DD' (one source of truth for both the
|
||||
// steady-hands day ledger and the witching-night date below).
|
||||
function localISODate(d) {
|
||||
d = d || new Date();
|
||||
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
||||
}
|
||||
|
||||
// growth-streak + challenger record on real competency events (date ledger /
|
||||
// distinct challenge sets) — kept as bookkeeping over EVENTS, never activity.
|
||||
function recordGrowthDay() {
|
||||
var date = new Date();
|
||||
var iso = date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0');
|
||||
var iso = localISODate();
|
||||
fetchJSON('/report-criterion', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ criterion_id: 'steady_hands_days', token: iso }),
|
||||
@@ -190,17 +196,21 @@
|
||||
|
||||
// ── Activity (Feats): in-memory session counters, flushed on song:ended ───
|
||||
var session = { notesTotal: 0 }; // cumulative across this sitting
|
||||
var song = { hits: 0, streak: 0, maxStreak: 0, chart: null };
|
||||
function resetSong(chart) { song = { hits: 0, streak: 0, maxStreak: 0, chart: chart || null }; }
|
||||
// `active` gates note counting to an actual song in progress — without it,
|
||||
// note:hit/miss from the tuner or input-calibration would inflate Feats from
|
||||
// non-song input and flush a phantom streak with chart:null.
|
||||
var song = { hits: 0, streak: 0, maxStreak: 0, chart: null, active: false };
|
||||
function resetSong(chart) { song = { hits: 0, streak: 0, maxStreak: 0, chart: chart || null, active: true }; }
|
||||
|
||||
function flushActivity(seconds) {
|
||||
if (!song.active) return; // no active song → nothing to flush (ignore stray events)
|
||||
song.active = false;
|
||||
// No notedetect → song.hits stays 0; notes-based Feats simply don't move
|
||||
// (graceful degradation). song_done / seconds / chart still flow so the
|
||||
// notedetect-free Feats (Road Warrior, Time Served, Encore) progress.
|
||||
var hour = new Date().getHours();
|
||||
var isNight = hour >= 2 && hour < 5;
|
||||
var d = new Date();
|
||||
var iso = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
||||
var iso = localISODate();
|
||||
var body = {
|
||||
notes: song.hits,
|
||||
session_notes: session.notesTotal,
|
||||
@@ -364,13 +374,17 @@
|
||||
resetSong(e && e.detail && e.detail.filename);
|
||||
});
|
||||
bus.on && bus.on('note:hit', function () {
|
||||
if (!song.active) return; // ignore tuner/calibration note events
|
||||
song.hits++; song.streak++; session.notesTotal++;
|
||||
if (song.streak > song.maxStreak) song.maxStreak = song.streak;
|
||||
});
|
||||
bus.on && bus.on('note:miss', function () { song.streak = 0; });
|
||||
bus.on && bus.on('note:miss', function () { if (song.active) song.streak = 0; });
|
||||
bus.on && bus.on('song:ended', function (e) {
|
||||
flushActivity(e && e.detail && (e.detail.time || e.detail.audioT));
|
||||
});
|
||||
// Song stopped/abandoned without a natural end → mark inactive so stray
|
||||
// note events after it don't accrue against a phantom (chart:null) song.
|
||||
bus.on && bus.on('song:stop', function () { song.active = false; });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
Reference in New Issue
Block a user