Small joys: Codex audit #2 fixes (route resolution, noindex, sense/tone, exclude-current re-pick)
- Admin joy item route moved to /api/admin/joys/{kind}/items/{item_id} so the
/add and /repick verbs resolve to their own routes instead of 422-ing as a
non-int item id (the launch blocker). Frontend mutate URL updated to match.
- Re-pick now excludes the currently-shown item: the endpoint reads today's
daily pool_id and passes it as `avoid`, so "Re-pick today" yields a different
item. Added `avoid` to pick_daily/_candidates across wotd/quote/onthisday.
- WOTD sense selection: the LLM now proposes word + intended part of speech, and
_lookup prefers that sense (fixes "serene" returning the archaic noun).
- On This Day tone prompt tightened to favor genuinely uplifting events and
exclude merely procedural/political-administrative ones.
- Caddy @hidden now also noindexes /word /quote /onthisday /admin (+ .html).
- Regression tests: add/repick resolve (401 not 422), add/feature/block/delete,
re-pick excludes current; WOTD pos-preference + proposal parsing units.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+7
-2
@@ -2358,6 +2358,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
# --- Small-joys admin: manage the WOTD / QOTD / On-This-Day pools ----------------
|
||||
_JOY_TABLES = {"onthisday": "onthisday_pool", "quote": "quote_pool", "word": "wotd_pool"}
|
||||
_JOY_DAILY = {"onthisday": "daily_onthisday", "quote": "daily_quote", "word": "daily_wotd"}
|
||||
_JOY_MODULES = {"onthisday": onthisday, "quote": quote, "word": wotd}
|
||||
_JOY_EDITABLE = { # whitelist of editable columns
|
||||
"onthisday": {"text", "summary", "year"},
|
||||
@@ -2375,7 +2376,7 @@ def create_app() -> FastAPI:
|
||||
rows = conn.execute(f"SELECT * FROM {table} ORDER BY featured DESC, id DESC LIMIT ?", (limit,)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.post("/api/admin/joys/{kind}/{item_id}")
|
||||
@app.post("/api/admin/joys/{kind}/items/{item_id}") # /items/ so 'add'/'repick' don't parse as an id
|
||||
def admin_joys_mutate(kind: str, item_id: int, body: JoyAction, request: Request) -> dict:
|
||||
table = _JOY_TABLES.get(kind)
|
||||
if not table:
|
||||
@@ -2438,7 +2439,11 @@ def create_app() -> FastAPI:
|
||||
raise HTTPException(status_code=404, detail="Unknown joy.")
|
||||
with get_conn() as conn:
|
||||
_require_admin(conn, request)
|
||||
picked = mod.pick_daily(conn, force=True)
|
||||
cur = conn.execute(
|
||||
f"SELECT pool_id FROM {_JOY_DAILY[kind]} WHERE feature_date=?", (local_today(),)
|
||||
).fetchone()
|
||||
avoid = cur["pool_id"] if cur else None # force a DIFFERENT item, not the same one
|
||||
picked = mod.pick_daily(conn, force=True, avoid=avoid)
|
||||
return {"ok": True, "picked": bool(picked)}
|
||||
|
||||
@app.get("/api/replacement", response_model=Article | None)
|
||||
|
||||
+26
-15
@@ -62,10 +62,15 @@ def _llm_keep(client, candidates: list[dict]) -> list[dict]:
|
||||
keep the keyword-passed set (never lose the day to a model hiccup)."""
|
||||
lines = [f"{i}: {c['text']}" for i, c in enumerate(candidates)]
|
||||
user = (
|
||||
"These are 'on this day' history events. Return the indices of the ones with a "
|
||||
"POSITIVE or NEUTRAL, uplifting tone — discoveries, inventions, firsts, achievements, "
|
||||
"peace, the arts, science, exploration, culture, milestones. EXCLUDE anything about "
|
||||
"war, violence, disasters, death, or tragedy.\n\n" + "\n".join(lines) +
|
||||
"These are 'on this day' history events. Return the indices of the ones that are "
|
||||
"GENUINELY UPLIFTING — a reader should feel a small lift of wonder, hope, or delight. "
|
||||
"Keep: discoveries, inventions, scientific breakthroughs, the arts and culture, "
|
||||
"exploration, human achievement, acts of courage or kindness, milestones of progress "
|
||||
"(rights won, things built, records set). EXCLUDE war, violence, disasters, death, or "
|
||||
"tragedy, AND exclude merely procedural or political-administrative events that carry no "
|
||||
"warmth (a coronation or accession, a treaty signing, an election, a law passed, a "
|
||||
"boundary or office change). When unsure whether something is truly uplifting, leave it "
|
||||
"out.\n\n" + "\n".join(lines) +
|
||||
'\n\nReply with JSON only, exactly: {"keep": [<indices>]}'
|
||||
)
|
||||
txt = client.chat_text([{"role": "user", "content": user}])
|
||||
@@ -115,23 +120,29 @@ def harvest(conn: sqlite3.Connection, md: str | None = None, client=None) -> dic
|
||||
return {"md": md, "fetched": len(events), "kept": len(kept), "added": after - before, "pool": after}
|
||||
|
||||
|
||||
def _candidates(conn: sqlite3.Connection, md: str) -> list[int]:
|
||||
def _candidates(conn: sqlite3.Connection, md: str, avoid: int | None = None) -> list[int]:
|
||||
"""The pick pool for a date: if admin has featured any, pick only among those;
|
||||
otherwise the N least-recently-shown."""
|
||||
otherwise the N least-recently-shown. `avoid` drops a specific id (admin re-pick)
|
||||
unless it's the only option."""
|
||||
featured = conn.execute(
|
||||
"SELECT id FROM onthisday_pool WHERE md=? AND blocked=0 AND featured=1 ORDER BY id", (md,)
|
||||
).fetchall()
|
||||
if featured:
|
||||
return [r[0] for r in featured]
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM onthisday_pool WHERE md=? AND blocked=0 "
|
||||
"ORDER BY shown_at IS NOT NULL, shown_at, id LIMIT ?",
|
||||
(md, _NO_REPEAT_POOL),
|
||||
).fetchall()
|
||||
return [r[0] for r in rows]
|
||||
ids = [r[0] for r in featured]
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM onthisday_pool WHERE md=? AND blocked=0 "
|
||||
"ORDER BY shown_at IS NOT NULL, shown_at, id LIMIT ?",
|
||||
(md, _NO_REPEAT_POOL),
|
||||
).fetchall()
|
||||
ids = [r[0] for r in rows]
|
||||
if avoid is not None:
|
||||
ids = [i for i in ids if i != avoid] or ids
|
||||
return ids
|
||||
|
||||
|
||||
def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, force: bool = False) -> dict | None:
|
||||
def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, force: bool = False,
|
||||
avoid: int | None = None) -> dict | None:
|
||||
"""Pick + cache today's fact. Idempotent (skips if today's done unless force).
|
||||
Returns the stored row, or None if the pool has nothing for today's date."""
|
||||
feature_date = feature_date or local_today()
|
||||
@@ -139,7 +150,7 @@ def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, force:
|
||||
existing = conn.execute("SELECT * FROM daily_onthisday WHERE feature_date=?", (feature_date,)).fetchone()
|
||||
if existing and not force:
|
||||
return dict(existing)
|
||||
ids = _candidates(conn, md)
|
||||
ids = _candidates(conn, md, avoid)
|
||||
if not ids:
|
||||
return None
|
||||
pick_id = daily.seeded_order(ids, feature_date)[0]
|
||||
|
||||
+14
-9
@@ -58,25 +58,30 @@ def _explain(client, text: str, author: str | None) -> str | None:
|
||||
return out or None
|
||||
|
||||
|
||||
def _candidates(conn: sqlite3.Connection) -> list[int]:
|
||||
def _candidates(conn: sqlite3.Connection, avoid: int | None = None) -> list[int]:
|
||||
featured = conn.execute(
|
||||
"SELECT id FROM quote_pool WHERE blocked=0 AND featured=1 ORDER BY id"
|
||||
).fetchall()
|
||||
if featured:
|
||||
return [r[0] for r in featured]
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM quote_pool WHERE blocked=0 ORDER BY shown_at IS NOT NULL, shown_at, id LIMIT ?",
|
||||
(_NO_REPEAT_POOL,),
|
||||
).fetchall()
|
||||
return [r[0] for r in rows]
|
||||
ids = [r[0] for r in featured]
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM quote_pool WHERE blocked=0 ORDER BY shown_at IS NOT NULL, shown_at, id LIMIT ?",
|
||||
(_NO_REPEAT_POOL,),
|
||||
).fetchall()
|
||||
ids = [r[0] for r in rows]
|
||||
if avoid is not None:
|
||||
ids = [i for i in ids if i != avoid] or ids
|
||||
return ids
|
||||
|
||||
|
||||
def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, client=None, force: bool = False) -> dict | None:
|
||||
def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, client=None,
|
||||
force: bool = False, avoid: int | None = None) -> dict | None:
|
||||
feature_date = feature_date or local_today()
|
||||
existing = conn.execute("SELECT * FROM daily_quote WHERE feature_date=?", (feature_date,)).fetchone()
|
||||
if existing and not force:
|
||||
return dict(existing)
|
||||
ids = _candidates(conn)
|
||||
ids = _candidates(conn, avoid)
|
||||
if not ids:
|
||||
return None
|
||||
pick_id = daily.seeded_order(ids, feature_date)[0]
|
||||
|
||||
+51
-22
@@ -44,23 +44,33 @@ def _http_bytes(url: str, timeout: int = 30) -> tuple[bytes, str]:
|
||||
return r.read(), (r.headers.get("Content-Type") or "")
|
||||
|
||||
|
||||
def _propose_words(client, n: int) -> list[str]:
|
||||
def _propose_words(client, n: int) -> list[dict]:
|
||||
"""Ask for word + the intended part of speech, so _lookup picks the sense the LLM meant
|
||||
(e.g. 'serene' the adjective, not the archaic noun)."""
|
||||
user = (
|
||||
f"Suggest {n} English vocabulary words for an uplifting 'word of the day' — positive, "
|
||||
"calming, hopeful, or quietly beautiful in meaning (e.g. serene, kindness, dawn, "
|
||||
"resilience, wonder). Real, usable words; vary common and slightly elevated. "
|
||||
'Reply with JSON only: {"words": ["...", "..."]}'
|
||||
"resilience, wonder). Real, usable words; vary common and slightly elevated. For each, "
|
||||
"give the part of speech you intend (the everyday modern sense, not an archaic one). "
|
||||
'Reply with JSON only: {"words": [{"word": "serene", "pos": "adjective"}, ...]}'
|
||||
)
|
||||
txt = client.chat_text([{"role": "user", "content": user}])
|
||||
m = re.search(r"\{.*\}", txt, re.S)
|
||||
if not m:
|
||||
return []
|
||||
words = json.loads(m.group(0)).get("words", [])
|
||||
return [str(w).strip().lower() for w in words if isinstance(w, str) and w.strip()]
|
||||
out = []
|
||||
for w in json.loads(m.group(0)).get("words", []):
|
||||
if isinstance(w, str) and w.strip():
|
||||
out.append({"word": w.strip().lower(), "pos": None})
|
||||
elif isinstance(w, dict) and str(w.get("word", "")).strip():
|
||||
out.append({"word": str(w["word"]).strip().lower(),
|
||||
"pos": (str(w.get("pos")).strip().lower() or None) if w.get("pos") else None})
|
||||
return out
|
||||
|
||||
|
||||
def _lookup(word: str) -> dict | None:
|
||||
"""Validate + enrich a word via the dictionary. Returns None if it's not a real word."""
|
||||
def _lookup(word: str, prefer_pos: str | None = None) -> dict | None:
|
||||
"""Validate + enrich a word via the dictionary. Returns None if it's not a real word.
|
||||
When prefer_pos is given, picks the meaning of that part of speech (the sense the LLM meant)."""
|
||||
try:
|
||||
data = daily.http_json(f"{DICT_BASE}/{urllib.parse.quote(word)}")
|
||||
except Exception: # noqa: BLE001 — unknown word / network → just skip it
|
||||
@@ -71,9 +81,22 @@ def _lookup(word: str) -> dict | None:
|
||||
meanings = entry.get("meanings") or []
|
||||
if not meanings or not (meanings[0].get("definitions") or []):
|
||||
return None
|
||||
definition = (meanings[0]["definitions"][0].get("definition") or "").strip()
|
||||
if not definition:
|
||||
# Prefer the meaning whose part of speech matches the LLM's intent; else the first usable one.
|
||||
chosen = None
|
||||
if prefer_pos:
|
||||
for mn in meanings:
|
||||
if (mn.get("partOfSpeech") or "").strip().lower() == prefer_pos and (mn.get("definitions") or []):
|
||||
if (mn["definitions"][0].get("definition") or "").strip():
|
||||
chosen = mn
|
||||
break
|
||||
if chosen is None:
|
||||
for mn in meanings:
|
||||
if (mn.get("definitions") or []) and (mn["definitions"][0].get("definition") or "").strip():
|
||||
chosen = mn
|
||||
break
|
||||
if chosen is None:
|
||||
return None
|
||||
definition = (chosen["definitions"][0].get("definition") or "").strip()
|
||||
phonetic = entry.get("phonetic")
|
||||
audio_url = None
|
||||
for p in (entry.get("phonetics") or []):
|
||||
@@ -82,13 +105,13 @@ def _lookup(word: str) -> dict | None:
|
||||
if not audio_url and p.get("audio"):
|
||||
audio_url = p["audio"]
|
||||
examples = []
|
||||
for m in meanings:
|
||||
for m in [chosen] + [mn for mn in meanings if mn is not chosen]: # chosen sense's examples first
|
||||
for d in (m.get("definitions") or []):
|
||||
if d.get("example"):
|
||||
examples.append(d["example"].strip())
|
||||
return {
|
||||
"word": (entry.get("word") or word).strip().lower(),
|
||||
"part_of_speech": meanings[0].get("partOfSpeech"),
|
||||
"part_of_speech": chosen.get("partOfSpeech"),
|
||||
"phonetic": phonetic,
|
||||
"audio_url": audio_url,
|
||||
"definition": definition,
|
||||
@@ -136,10 +159,11 @@ def harvest(conn: sqlite3.Connection, client, count: int = _HARVEST_BATCH) -> di
|
||||
except Exception: # noqa: BLE001
|
||||
return {"proposed": 0, "added": 0, "pool": _pool_count(conn)}
|
||||
rows = []
|
||||
for w in words:
|
||||
for item in words:
|
||||
w = item["word"]
|
||||
if not w.isalpha() or conn.execute("SELECT 1 FROM wotd_pool WHERE word=?", (w,)).fetchone():
|
||||
continue
|
||||
info = _lookup(w)
|
||||
info = _lookup(w, item.get("pos"))
|
||||
if not info:
|
||||
continue
|
||||
audio_file = _cache_audio(info["audio_url"], info["word"])
|
||||
@@ -155,23 +179,28 @@ def harvest(conn: sqlite3.Connection, client, count: int = _HARVEST_BATCH) -> di
|
||||
return {"proposed": len(words), "added": after - before, "pool": after}
|
||||
|
||||
|
||||
def _candidates(conn: sqlite3.Connection) -> list[int]:
|
||||
def _candidates(conn: sqlite3.Connection, avoid: int | None = None) -> list[int]:
|
||||
featured = conn.execute("SELECT id FROM wotd_pool WHERE blocked=0 AND featured=1 ORDER BY id").fetchall()
|
||||
if featured:
|
||||
return [r[0] for r in featured]
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM wotd_pool WHERE blocked=0 ORDER BY shown_at IS NOT NULL, shown_at, id LIMIT ?",
|
||||
(_NO_REPEAT_POOL,),
|
||||
).fetchall()
|
||||
return [r[0] for r in rows]
|
||||
ids = [r[0] for r in featured]
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM wotd_pool WHERE blocked=0 ORDER BY shown_at IS NOT NULL, shown_at, id LIMIT ?",
|
||||
(_NO_REPEAT_POOL,),
|
||||
).fetchall()
|
||||
ids = [r[0] for r in rows]
|
||||
if avoid is not None:
|
||||
ids = [i for i in ids if i != avoid] or ids
|
||||
return ids
|
||||
|
||||
|
||||
def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, force: bool = False) -> dict | None:
|
||||
def pick_daily(conn: sqlite3.Connection, feature_date: str | None = None, force: bool = False,
|
||||
avoid: int | None = None) -> dict | None:
|
||||
feature_date = feature_date or local_today()
|
||||
existing = conn.execute("SELECT * FROM daily_wotd WHERE feature_date=?", (feature_date,)).fetchone()
|
||||
if existing and not force:
|
||||
return dict(existing)
|
||||
ids = _candidates(conn)
|
||||
ids = _candidates(conn, avoid)
|
||||
if not ids:
|
||||
return None
|
||||
pick_id = daily.seeded_order(ids, feature_date)[0]
|
||||
|
||||
Reference in New Issue
Block a user