Step 4 (partial): drop dead pick_points table + game-system DB methods

Migration 020 drops cauldron_pick_points (game system stripped from
/plan 2026-04-30 — all award_pick_points calls were already removed
from server.py). The DB methods household_scoreboard, household_streak,
and award_pick_points are deleted now too — they were unused dead code
since the strip. delete_plan_slots no longer DELETEs from the dropped
table (kept it minimal: just wipe the meal_plan_slots).

DEFERRED from Step 4: dropping cauldron_foods. The 229 Sonnet-curated
density rows + 2462 USDA seed rows are still useful raw material for
a fuzzy backfill into cauldron_food_metadata (only 128 of Mealie's
2895 foods got densities matched on the first exact-name pass). Until
we run a fuzzy backfill, holding onto cauldron_foods as a cold archive
is cheaper than losing the data. Will revisit once the metadata
catalog is more complete.

Recovery from 020: revert this migration, restore the CREATE TABLE
position-014 entry from db.py history. The points data was never
meaningfully populated (jobs 1+3 award attempts were folded into the
strip), so loss is essentially zero.
This commit is contained in:
Kayos 2026-04-30 12:02:58 -07:00
parent 69e05b1f92
commit 94c07ab156

View file

@ -346,6 +346,15 @@ MIGRATIONS = [
FOREIGN KEY (job_id) REFERENCES cauldron_consolidate_jobs(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
# 020 — Drop the game-system pick-points table. Stripped from /plan
# 2026-04-30; tables and methods kept around as dead code for a few
# days to let it bake. Now that the simpler base flow is proven,
# the ledger comes out. Recovery path: revert this migration's number
# and re-add the table from db.py history; the points data was never
# widely populated anyway.
"""
DROP TABLE IF EXISTS cauldron_pick_points
""",
]
@ -569,94 +578,7 @@ class DB:
)
return cur.rowcount
def household_scoreboard(self, household_id: int) -> list[dict]:
"""Per-user lock counts + pick-points + most recent lock time.
Three numbers per row:
wins user-locked weeks (excludes auto-locks)
weeks_locked alias for wins, preserved for older callers
points sum of cauldron_pick_points for this user/household
Sort: points desc, then wins desc, then last_win desc points are
the headline metric in v0.3 (every pick lands matters daily).
We compute lock counts and points as separate scalar subqueries so
the JOIN doesn't blow up on the cartesian (members × plans × points).
"""
with self.conn() as c, c.cursor() as cur:
cur.execute(
"""
SELECT
u.authentik_sub AS sub,
u.email AS email,
u.display_name AS display_name,
COALESCE((
SELECT COUNT(*) FROM cauldron_meal_plans p
WHERE p.locked_by_sub = m.authentik_sub
AND p.household_id = m.household_id
AND p.locked_reason = 'user'
), 0) AS wins,
(
SELECT MAX(p.locked_at) FROM cauldron_meal_plans p
WHERE p.locked_by_sub = m.authentik_sub
AND p.household_id = m.household_id
AND p.locked_reason = 'user'
) AS last_win,
COALESCE((
SELECT SUM(pp.points) FROM cauldron_pick_points pp
WHERE pp.household_id = m.household_id
AND pp.authentik_sub = m.authentik_sub
), 0) AS points
FROM cauldron_household_members m
LEFT JOIN cauldron_users u
ON u.authentik_sub = m.authentik_sub
WHERE m.household_id = %s
ORDER BY points DESC, wins DESC, last_win DESC
""",
(household_id,),
)
out = []
for r in cur.fetchall():
d = dict(r)
d["points"] = int(d.get("points") or 0)
d["wins"] = int(d.get("wins") or 0)
d["weeks_locked"] = d["wins"]
out.append(d)
return out
def household_streak(self, household_id: int) -> dict | None:
"""Compute current win streak: walk back from most recent locked week,
counting consecutive weeks won by the same user. Returns
{sub, display_name, count} or None if no locks."""
with self.conn() as c, c.cursor() as cur:
cur.execute(
"""
SELECT p.week_start, p.locked_by_sub, u.display_name, u.email
FROM cauldron_meal_plans p
LEFT JOIN cauldron_users u ON u.authentik_sub = p.locked_by_sub
WHERE p.household_id = %s
AND p.locked_at IS NOT NULL
AND p.locked_reason = 'user'
ORDER BY p.week_start DESC
""",
(household_id,),
)
rows = cur.fetchall()
if not rows:
return None
leader = rows[0]["locked_by_sub"]
count = 0
for r in rows:
if r["locked_by_sub"] != leader:
break
count += 1
return {
"sub": leader,
"display_name": rows[0]["display_name"] or rows[0]["email"],
"count": count,
}
# --- plan slots + pick points (v0.3 A4) --------------------------------
# --- plan slots (v0.3 A4) ----------------------------------------------
# Day order is stable Mon..Sun. Used everywhere we need to render slots
# in calendar order.
@ -728,13 +650,10 @@ class DB:
return inserted
def delete_plan_slots(self, plan_id: int) -> int:
"""Wipe slots for a plan (used by re-roll). Also nukes the matching
pick_points so the ledger doesn't double-count on regenerate."""
"""Wipe slots for a plan (used by re-roll)."""
with self.conn() as c, c.cursor() as cur:
cur.execute("DELETE FROM cauldron_meal_plan_slots WHERE plan_id=%s", (plan_id,))
slots_removed = cur.rowcount
cur.execute("DELETE FROM cauldron_pick_points WHERE plan_id=%s", (plan_id,))
return slots_removed
return cur.rowcount
def mark_plan_generated(self, plan_id: int, sub: str) -> dict:
"""Set generated_by_sub + generated_at IF not already set. Returns
@ -766,27 +685,6 @@ class DB:
(plan_id,),
)
def award_pick_points(
self,
household_id: int,
plan_id: int,
sub: str,
points: int,
reason: str = "pick_used",
) -> int:
"""Insert one ledger row. Returns the new row id. Reason must be one
of the ENUM values; we don't validate here — DB will reject bad ones."""
with self.conn() as c, c.cursor() as cur:
cur.execute(
"""
INSERT INTO cauldron_pick_points
(household_id, plan_id, authentik_sub, points, reason)
VALUES (%s, %s, %s, %s, %s)
""",
(household_id, plan_id, sub, int(points), reason),
)
return cur.lastrowid
def enrich_plan_with_slots(self, plan: dict) -> dict:
"""In-place: add `slots` key to a plan dict. Returns the same dict
for chaining. Empty list if there are no slots yet."""