marathon-live / savedview.py
kalamishere's picture
live matcher
57bda6a verified
Raw
History Blame Contribute Delete
42.6 kB
"""The Tracks list, the resolved-match card, and the batch summary.
Kept out of `render.py` so the report renderer stays one thing. Everything
here borrows `render.CSS` rather than defining a second visual language, so
a saved row and a market card are recognisably the same product.
"""
from __future__ import annotations
import datetime as dt
import livematch
import render
import worldmap
ESC = render.ESC
def _when(iso: str | None) -> str:
""""3 days ago" reads faster than a timestamp on a list someone scans.
The exact date follows in the same line for anyone who wants it."""
if not iso:
return ""
try:
t = dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
except ValueError:
return ESC(iso)
# Everything this app writes carries a zone. Anything hand-written into
# the index, or written by an older build, may not — and a whole list
# that fails to draw because one row is missing a "+00:00" is not a
# trade worth making.
if t.tzinfo is None:
t = t.replace(tzinfo=dt.timezone.utc)
days = (dt.datetime.now(dt.timezone.utc) - t).days
when = ("today" if days <= 0 else "yesterday" if days == 1
else f"{days} days ago")
return f"{when} · {t:%-d %b}"
week_span = render.week_span
# "23 Aug". One author of the day a reader sees, in `render`, so the list,
# the banner, the step bar and the report's own kicker cannot drift apart.
_daystamp = render.day_stamp
def _state(m: dict) -> str:
"""The one word this market is in, for a saved row.
A meta written before "self" was a state carries `band: None` on a
self-match, so the number decides when the word is missing. Everything
else falls to flat, which is the sheet's word for "no ranking to read".
"""
band = m.get("band")
if band in ("place", "derive", "pass", "self"):
return band
if float(m.get("best") or 0.0) >= worldmap.SAME_RECORDING:
return "self"
return "flat"
def _band_class(band: str | None) -> str:
return band if band in ("place", "derive", "pass", "self") else "flat"
def _markets_strip(top: list[dict], flat: bool = False) -> str:
"""The top two or three markets, each in its own verdict colour.
This is what makes the list answer "which track goes where" before
anything is opened. Colouring only the leading market — the first
version — meant a track placing in Kenya and passing everywhere else
looked identical to one placing in all three.
A flat reading gets no colours and a line saying why. The report for
that track opens "No market stands out" and refuses to rank; a row
printing three coloured percentages under a MAKE chip said the opposite
about the same track on the same day (23 Aug audit).
"""
if not top:
return ""
if flat:
names = [ESC(worldmap.market_name(m.get("iso", ""), m.get("name")))
for m in top[:3]]
listed = (names[0] if len(names) == 1
else " and ".join([", ".join(names[:-1]), names[-1]])
if len(names) > 2 else " and ".join(names))
return (f'<p class="den" style="margin:10px 0 0">Reads much the same '
f'in every market measured, so there is no ranking to read. '
f'{listed} came top by a margin too small to act on.</p>')
cells = []
for m in top[:3]:
b = _state(m)
colour = f"var(--{b})"
# A self-match prints no percentage anywhere, here included: the
# number is the record recognising itself, and 100% next to a market
# name reads as the best market on the list.
value = render.CHIP["self"] if b == "self" \
else livematch.pct(m.get("best", 0))
cells.append(
f'<span style="display:inline-flex;align-items:baseline;gap:5px;'
f'white-space:nowrap">'
f'<b style="font-weight:800;font-size:15px;color:{colour};'
f'font-variant-numeric:tabular-nums">{value}</b>'
f'<span style="font-size:13px">'
f'{ESC(worldmap.market_name(m.get("iso", ""), m.get("name")))}'
f'</span></span>')
return ('<div style="display:flex;flex-wrap:wrap;gap:12px;'
'margin:10px 0 0">' + "".join(cells) + "</div>")
def _sort_key(r: dict) -> tuple:
"""Artist, then newest first inside an artist.
A label's list is usually several tracks by one or two acts, so grouping
by artist puts the comparison the reader is actually making — how do this
artist's records sit against each other — next to each other with no
filter to operate. Tracks with no artist sort last rather than first, so
a stray upload does not head the list.
"""
artist = (r.get("artist") or "").strip()
return (artist == "", artist.lower(), _neg_time(r.get("saved_at")))
def _neg_time(iso: str | None) -> str:
"""Descending sort on an ISO timestamp without parsing it: invert each
digit. Newest first inside a group, with no date arithmetic."""
s = iso or ""
return "".join(chr(ord("9") - (ord(c) - ord("0"))) if c.isdigit() else c
for c in s)
def header(rows: list[dict], corpus_week: str) -> str:
"""The Tracks list's opening block: how many tracks, and whether they are
all read against the week the app has loaded.
Carries the stylesheet for the whole tab. The cards below are separate
HTML blocks so each can have its own button, and one copy of the CSS in
the page serves all of them.
"""
out = [render.CSS, '<div class="ml">']
if not rows:
out.append(
'<div class="card flat"><p class="why"><b>Nothing saved yet.'
'</b> Every track you analyse is kept here automatically, with '
'its fingerprint. When next Monday\'s charts land you can '
're-read the whole list against them without finding the files '
'again.</p></div></div>')
return "".join(out)
stale = sum(1 for r in rows if str(r.get("corpus_week") or "")
!= str(corpus_week))
out.append(f'<div class="kicker">Tracks</div>'
f'<h1 class="hdr">{len(rows)} '
f'{"track" if len(rows) == 1 else "tracks"}</h1>')
if stale:
out.append(
f'<p class="sub">{stale} of them were last read against an '
f'earlier week. <b>Re-score all</b> brings the list up to the '
f'latest charts ({ESC(week_span(str(corpus_week)))}).</p>')
else:
out.append(f'<p class="sub">All read against the latest charts '
f'({ESC(week_span(str(corpus_week)))}).</p>')
out.append('<p class="foot" style="margin:12px 0 0">Tap a track to open '
'its full report.</p>')
out.append("</div>")
return "".join(out)
def unreachable() -> str:
"""The Tracks tab when the archive cannot be read.
Not the empty state. "Nothing saved yet" told a team whose forty
analyses were behind a bad minute that they had never saved one — and
the first thing anyone does about that is analyse everything again (23
Aug audit). Nothing here says what went wrong, because the reader cannot
act on that; it says what is true and what to do.
"""
return (render.CSS + '<div class="ml"><div class="card flat">'
'<p class="why"><b>The saved list could not be read just now.'
'</b> This is the list not loading — nothing has been lost, and '
'nothing has been deleted. Try again in a minute.</p>'
'</div></div>')
def card(row: dict, corpus_week: str, artist_heading: str | None = None,
artist_count: int = 1) -> str:
"""One saved track, as the thing you tap.
The picker at the foot of the tab was the only way into a saved report,
and nobody found it — the cards looked like a read-only summary. The card
is now the control: it sits under a transparent button covering the whole
of it, and it says so, with the chevron every list on a phone uses to
mean "this opens".
Everything drawn here comes off the index, so a list of fifty costs one
small download rather than fifty.
"""
out = ['<div class="ml">']
if artist_heading is not None:
out.append(f'<h2 style="margin-top:0"><span>'
f'{ESC(artist_heading) if artist_heading else "Other tracks"}'
f'</span>'
+ (f'<span class="tag">{artist_count} tracks</span>'
if artist_count > 1 else "")
+ '</h2>')
top = row.get("top_markets") or []
lead = top[0] if top else {}
# Flatness is a fact about the whole field, written down at save time —
# `store._flat_field`. It outranks the leading market's own band, because
# a field with no ranking has no leading market. A row saved before the
# key existed carries no `flat` and reads as it did before.
flat = bool(row.get("flat"))
cls = "flat" if flat else _state(lead)
artist = (row.get("artist") or "").strip()
row_stale = str(row.get("corpus_week") or "") != str(corpus_week)
# The title alone under an artist heading; the full label when there is
# no artist to head it.
name = (row.get("title") or row.get("label") or "untitled") if artist \
else (row.get("label") or "untitled")
out.append(f'<div class="card tap {cls}">')
out.append(f'<div class="mkt"><div class="name">{ESC(name)}</div>')
if top:
out.append(render.chip(cls))
out.append('<span class="chev">›</span>')
out.append("</div>")
out.append(_markets_strip(top, flat=flat))
meta_bits = [_when(row.get("saved_at"))]
if row.get("source") == "lookup":
meta_bits.append("30s preview, found by name")
elif row.get("duration_s"):
# "full track, 0 min" for anything under half a minute, and a
# 20-second sketch is a real thing to drop in (23 Aug audit).
secs = float(row["duration_s"])
meta_bits.append("full track, under a minute" if secs < 60
else f'full track, {secs / 60:.0f} min')
week_bit = (f'charts of '
f'{ESC(week_span(str(row.get("corpus_week") or "?")))}')
meta_bits.append(week_bit + (" — earlier than this one" if row_stale
else ""))
if int(row.get("runs", 1)) > 1:
meta_bits.append(f'read {row["runs"]} times')
out.append(f'<p class="den" style="margin:10px 0 0">'
f'{" · ".join(b for b in meta_bits if b)}</p>')
out.append("</div></div>")
return "".join(out)
def cards(rows: list[dict], corpus_week: str) -> list[tuple[dict, str]]:
"""(row, html) in the order the list draws them, with an artist heading
on the first card of each artist's run.
The Tracks tab builds one button per entry from this, so the order the
buttons are created in and the order the cards read in are the same list.
"""
out = []
current = object()
for r in sorted(rows, key=_sort_key):
artist = (r.get("artist") or "").strip()
heading, n = None, 1
if artist != current:
current = artist
heading = artist
n = sum(1 for x in rows
if (x.get("artist") or "").strip() == artist)
out.append((r, card(r, corpus_week, artist_heading=heading,
artist_count=n)))
return out
def saved_list(rows: list[dict], corpus_week: str) -> str:
"""The whole list as one block of HTML — the header and every card.
One renderer, two callers: this is what a test and any non-interactive
read of the list see, while the tab itself lays the same cards out one
per button.
Every card carries the same four things in the same places — name, the
verdict chip, the top three markets with their own colours, and the
provenance line — so two cards read against each other down a phone
screen without a compare mode. That is the cheap 80% of comparison;
what a real compare view would add is noted in design/ONE_APP.md §5.
Everything drawn here comes off the index, so a list of fifty costs one
small download rather than fifty.
"""
out = [header(rows, corpus_week)]
for _row, html in cards(rows, corpus_week):
out.append(html)
return "".join(out)
def match_card(match: dict, preview_url: str | None = None) -> str:
"""What the name lookup resolved, shown BEFORE any verdict.
Deezer's search is fuzzy and its ranking put "Sugarcane (Remix)" above
"Sugarcane" on a search for Sugarcane. A wrong match produces a perfectly
confident analysis of the wrong record, so the reader sees what was found
and can hear it before reading a single number.
"""
src = {"deezer": "Deezer", "itunes": "Apple Music"}.get(
match.get("source", ""), match.get("source", ""))
out = [render.CSS, '<div class="ml"><div class="card flat">',
'<div class="lab">FOUND</div>',
f'<div class="mkt"><div class="name">'
f'{ESC(match.get("artist", ""))} — '
f'{ESC(match.get("title", ""))}</div></div>']
bits = [f'official 30-second preview from {ESC(src)}']
if match.get("album"):
bits.append(ESC(match["album"]))
out.append(f'<p class="den">{" · ".join(bits)}</p>')
if preview_url:
out.append(f'<div class="player"><span class="plab">PREVIEW</span>'
f'<audio controls preload="none" src="{ESC(preview_url)}">'
f'</audio></div>')
alts = match.get("alternatives") or []
if alts:
names = "; ".join(f'{ESC(a.get("artist", ""))} — '
f'{ESC(a.get("title", ""))}' for a in alts)
out.append(f'<p class="why">Other versions with the same name: '
f'{names}. If one of those is the record you meant, type '
f'its full title and search again.</p>')
out.append(
'<p class="why"><b>This reading is like-for-like.</b> Every sound '
'the track is compared against is measured from a 30-second preview '
'as well, so the percentages here are better calibrated than they '
'are for a full-length upload.</p>')
out.append('</div></div>')
return "".join(out)
PREVIEW_NO_SNIPPETS = (
'<div class="note"><div class="lab">NO CLIP SHORTLIST</div>'
'<p class="why">A preview is 30 seconds, which is one window, so there '
'is nothing to choose between. Upload the full track to get the clip '
'shortlist and the section map.</p></div>')
def _names_said(names: list[str]) -> str:
""""Kenya", "Kenya and Ghana", "Kenya, Ghana and Peru"."""
if len(names) <= 1:
return names[0] if names else ""
return ", ".join(names[:-1]) + f" and {names[-1]}"
def _answer_since(was: dict, now: dict, previous: dict, current: dict) -> str:
"""Whether this reading's answer is the one before it.
"Held" and "moved" are the only two words here. A track reading Nigeria
one week and Argentina the next has not risen, fallen or gained momentum:
two readings of one record were taken and they said different things.
That is the whole of the claim, and there is no other one to make from two
readings of a single track.
"""
was_flat, now_flat = bool(previous.get("flat")), bool(current.get("flat"))
if was_flat and now_flat:
return ("the field was flat in the last chart read, and it is flat "
"in this one.")
if was_flat:
return (f"the field was flat in the last chart read; this one ranks "
f"{now['lead']}.")
if now_flat:
return f"the answer was {was['lead']}; this reading's field is flat."
if was["text"] == now["text"]:
return f"the answer held — {now['text']}."
return f"the answer moved — was {was['text']}, now {now['text']}."
def _markets_since(previous: dict, current: dict) -> str:
"""Which markets joined the top of the ranking and which dropped out of
it.
Both readings wrote down the three markets their row prints, and those
three are what is compared — so the sentence says "the top markets" and
not "the ranked list", which would claim a market had left the report
altogether. A reading missing that list says nothing here.
A flat reading on either side says nothing here either. Flatness is a
refusal to rank, and a market cannot enter or leave an order that the
report on the same page says does not exist — the same rule the saved row
and the batch summary already follow.
"""
if previous.get("flat") or current.get("flat"):
return ""
was = list(previous.get("top_markets") or [])
now = list(current.get("top_markets") or [])
if not was or not now:
return ""
def named(m: dict) -> str:
return ESC(worldmap.market_name(m.get("iso", ""), m.get("name")))
old_iso = {str(m.get("iso") or "") for m in was}
new_iso = {str(m.get("iso") or "") for m in now}
entered = [named(m) for m in now if str(m.get("iso") or "") not in old_iso]
left = [named(m) for m in was if str(m.get("iso") or "") not in new_iso]
if entered and left:
return (f"{_names_said(entered)} entered the top markets; "
f"{_names_said(left)} left them.")
if entered:
return f"{_names_said(entered)} entered the top markets."
if left:
return f"{_names_said(left)} left the top markets."
return ""
def _windows_since(previous: dict, current: dict) -> str:
"""How much of the clip shortlist this reading picked for the first time.
Counted by window start, which is what identifies a window. Either
reading missing its windows says nothing — a reading whose file could not
be opened has no shortlist anybody has read, and a track too short to hold
a 30-second window has none to compare.
"""
was = [round(float(w), 2) for w in (previous.get("windows") or [])]
now = [round(float(w), 2) for w in (current.get("windows") or [])]
if not was or not now:
return ""
fresh = [w for w in now if w not in set(was)]
n, total = len(fresh), len(now)
if not n:
return (f"The same {total} snippet window"
f"{' is' if total == 1 else 's are'} picked.")
if n == total:
return ("The one snippet window is new." if total == 1
else f"All {total} snippet windows are new.")
return f"{n} of {total} snippet windows {'is' if n == 1 else 'are'} new."
def since_last(current: dict, previous: dict | None) -> str:
"""What changed between the previous chart read and this one.
One short paragraph of plain HTML with no wrapper around it: the app puts
it in the SAVED READING banner and the share page draws its own note, and
both get the same sentences from here.
"Chart read" is the name the whole page uses — the line over the title
says "chart read 25 Aug" — and it is the thing being compared. Two
readings taken against one evening's charts are one chart read, and
`store.prior` hands in the reading before that instead of the button
press before this.
`current` and `previous` are what `store.summarise` returns —
`top_markets`, `flat`, `windows` — with the kept reading's entry folded
into `previous`, which is where its date comes from. Every clause is
printed only when the facts behind it are on both sides: a previous
reading whose file would not open has no windows and possibly no answer,
and what cannot be read is not written about. No earlier chart read at
all is silence, not a line saying there is nothing to compare.
"""
if not previous:
return ""
was, now = verdict(previous), verdict(current)
said = []
if was and now:
said.append(_answer_since(was, now, previous, current))
said.append(_markets_since(previous, current))
said.append(_windows_since(previous, current))
body = " ".join(s for s in said if s)
if not body:
return ""
day = _daystamp(previous.get("stamped_at"))
head = (f"Since the last chart read (read {ESC(day)})" if day
else "Since the last chart read")
return f'<p class="why"><b>{head}:</b> {body}</p>'
def since_note(line: str) -> str:
"""The same line on a page that has no SAVED READING banner to put it in —
the share page. Nothing at all when there is nothing to say.
No uppercase label over it. The sentence opens with "Since the last
chart read" in bold, and a SINCE THE LAST CHART READ kicker above it
printed the same words twice in two type sizes. The banner in the app
keeps its own label because that one says something else — how many
readings are kept.
"""
if not line:
return ""
return f'<div class="note">{line}</div>'
def reopened_note(row: dict, corpus_week: str, since: str = "") -> str:
"""Sits above a reopened report. Its numbers may be from an older week,
and a reader who assumes otherwise would compare two different worlds.
When the track has been read before, the line says how many earlier
readings are kept — the way into them is below the report, because the
reader came here to read this week's answer first.
And it says where they are. Below the report is five screens down on a
desktop and eight on a phone (measured on staging, 25 Aug), so a count
with no direction is a count of something the reader has no reason to
believe is on the page at all.
`since` is `since_last`'s paragraph, and it goes here because this is the
one block on the page that is already about the track's history. A caller
with no earlier reading to compare against passes nothing and the banner
is what it was.
"""
week = str(row.get("corpus_week") or "?")
if week == str(corpus_week):
# No week named here. This track is read against the charts the app
# has loaded, which is the whole of what the reader needs to know;
# the span in brackets was a second date beside a read-date and read
# as a contradiction of it. Which charts those are is on the
# provenance line at the foot of the report.
body = (f'Saved {_when(row.get("saved_at"))}, read against the '
'charts loaded now.')
else:
body = (f'Saved {_when(row.get("saved_at"))}, read against the '
f'charts of {ESC(week_span(week))}. The charts loaded '
f'now cover {ESC(week_span(str(corpus_week)))}. '
f'<b>Re-run</b> to read it against them.')
n = len(row.get("readings") or [])
lab = "SAVED READING"
if n:
lab += f' · {n} earlier reading{"" if n == 1 else "s"}'
body += (f' {n} earlier reading{"" if n == 1 else "s"} of it '
f'{"is" if n == 1 else "are"} kept, at the foot of this '
f'report.')
return (render.CSS + '<div class="ml"><div class="note">'
f'<div class="lab">{ESC(lab)}</div>'
f'<p class="why">{body}</p>{since}</div></div>')
def verdict(entry: dict) -> dict | None:
"""What one kept reading answered, in the words a row prints.
"Argentina 95% · push" is what tells a reader whether to open a reading,
so it stands on the row itself rather than inside it — beside the days
the group was read, which are what tell one group of readings from the
next.
Returns None when the answer is not known — a reading filed before the
entry carried one, whose file could not be opened. A row with no verdict
says the week and the date and stops there. Nothing here guesses: a flat
field says flat, and everything else is read off the markets that were
written down.
"""
if entry.get("flat"):
return {"band": "flat", "lead": "Field was flat",
"text": "Field was flat · no ranking to read"}
top = entry.get("top_markets") or []
if not top:
return None
m = top[0] or {}
band = _state(m)
name = ESC(worldmap.market_name(m.get("iso", ""), m.get("name")))
if band == "self":
# A record meeting its own chart entry prints no percentage, here as
# everywhere else: the number is the record recognising itself.
return {"band": band, "lead": name,
"text": f"{name} · already charting there"}
lead = f"{name} {livematch.pct(m.get('best', 0))}"
return {"band": band, "lead": lead,
"text": f"{lead} · {render.CHIP[band].lower()}"}
def _verdict_bits(said: dict | None) -> str:
"""The verdict half of a week row: the answer, then its chip."""
if not said:
return '<div class="rverd"></div>'
return (f'<div class="rverd">'
f'<span class="rlead {said["band"]}">{said["lead"]}</span>'
f'<span class="rchip {said["band"]}">'
f'{render.CHIP[said["band"]]}</span></div>')
def _days_span(first: str, last: str) -> str:
""""24–25 Aug" from two day stamps, "24 Aug – 2 Sep" across a month end.
The same shape `render.week_span` gives a chart week, so a span of days
reads as one span wherever it appears rather than as two dates that
happen to be next to each other. One day, or the same day twice, is
itself.
"""
if not first or not last or first == last:
return first or last
a, b = first.rsplit(" ", 1), last.rsplit(" ", 1)
if len(a) == 2 and len(b) == 2 and a[1] == b[1]:
return f"{a[0]}{b[0]} {b[1]}"
return f"{first}{last}"
def _span(readings: list[dict]) -> str:
""""24–25 Aug" for a group read on more than one day, "4 Aug" for a group
read on one. Oldest first: the rows underneath list newest first, and a
date range still has to run forward to read as a range — the mockup's
newest-first span ("25 Aug – 24 Aug") read as broken dates. Kalam,
25 Aug."""
stamped = sorted(str(r.get("stamped_at") or "") for r in readings
if r.get("stamped_at"))
days = [d for d in (_daystamp(s) for s in stamped) if d]
if not days:
return ""
return _days_span(days[0], days[-1])
def _movement(readings: list[dict], short: bool = False) -> str:
"""How many readings this week holds, and whether the answer moved.
Counted from the answers themselves. A week holding a reading whose
answer is not known says how many it holds and nothing more — "the same
answer each time" about readings that were never compared would be the
kind of claim this app does not make.
"""
n = len(readings)
if n == 1:
return "1 reading"
said = [verdict(r) for r in readings]
if any(s is None for s in said):
return f"{n} readings"
changes = sum(1 for a, b in zip(said, said[1:])
if a["text"] != b["text"])
if not changes:
return f"{n} readings · same answer" + ("" if short else " each time")
if changes == 1:
return f"{n} readings · answer changed once"
return f"{n} readings · answer changed {changes} times"
def week_groups(kept: list[dict]) -> list[dict]:
"""The kept readings grouped by the chart week they were read against.
The list grows by week rather than by reading, which is what makes it
hold: a year of Monday re-scores is 52 rows, and a team re-scoring twice
a week is still 52.
`n` on each reading is its place in the order they were made, counting
from the oldest, so reading 1 stays reading 1 when a fifth is added under
it. Weeks come back most-recently-read first, and so do the readings
inside each one.
"""
groups: dict[str, list[dict]] = {}
for n, e in enumerate(kept, 1):
week = str(e.get("week") or "").strip() or "an earlier week"
groups.setdefault(week, []).append(dict(e, n=n))
out = [{"week": w, "readings": list(reversed(rs))}
for w, rs in groups.items()]
out.sort(key=lambda g: -max(r["n"] for r in g["readings"]))
return out
# How many weeks stand open at the foot of a report before the rest fold into
# one row. Three is what fits on a phone screen under the report without the
# list becoming the page.
OPEN_WEEKS = 3
_COUNT_WORDS = {2: "two", 3: "three", 4: "four", 5: "five", 6: "six",
7: "seven", 8: "eight", 9: "nine", 10: "ten"}
def _weeks_said(n: int) -> str:
return f'{_COUNT_WORDS.get(n, n)} week{"" if n == 1 else "s"}'
def _pick(file: str) -> str:
return ESC(str(file or ""))
def _reading_row(r: dict) -> str:
"""One reading, inside its week."""
said = verdict(r)
day = _daystamp(r.get("stamped_at"))
return (f'<button type="button" class="rrow" data-pick="'
f'{_pick(r.get("file"))}">'
f'<span class="rno">Reading {r["n"]}'
+ (f' <span class="rday">· read {ESC(day)}</span>' if day else "")
+ '</span>'
f'<span class="rsaid">{said["text"] if said else ""}</span>'
f'<span class="ropen">Open →</span></button>')
def _group_title(group: dict) -> str:
"""What the row is called: "Readings · 24–25 Aug".
The days these readings were made, not the week they were read against.
"Week 2026-08-17" was the first thing Kalam read on the first live
re-score and the one thing he could not place — the chart week's start
date reads as the past to anyone looking today, while the day a reading
was made is the day they looked. A group whose readings carry no date
says "Readings" and stops there.
"""
span = _span(group["readings"])
return f"Readings · {span}" if span else "Readings"
def _group_note(group: dict, short: bool = False) -> str:
"""The line under the title: how many readings, whether the answer moved,
and quietly which charts they were read against.
The chart week is still the thing that groups these rows, so it is still
written down — one step below the title, where it tells two groups apart
without leading the row.
"""
said = _movement(group["readings"], short=short)
week = week_span(group["week"])
return f"{said} · charts of {ESC(week)}" if week else said
def _week_row(group: dict, short: bool = False) -> str:
"""A whole week as one row, for a week read once and for the weeks folded
away below. It opens that week's latest reading."""
rs = group["readings"]
return (f'<button type="button" class="rrow rwrow" data-pick="'
f'{_pick(rs[0].get("file"))}">'
f'<div class="rwk"><b>{ESC(_group_title(group))}</b>'
f'<span class="rnote">{_group_note(group, short=short)}</span>'
f'</div>'
+ _verdict_bits(verdict(rs[0]))
+ f'<span class="ropen">Open →</span></button>')
def _week_details(group: dict, open_: bool) -> str:
"""A week read more than once: the week's own line, and every reading of
it one click inside."""
rs = group["readings"]
return (f'<details class="rweek"{" open" if open_ else ""}>'
f'<summary>'
f'<div class="rwk"><b>{ESC(_group_title(group))}</b>'
f'<span class="rnote">{_group_note(group)}</span></div>'
+ _verdict_bits(verdict(rs[0]))
+ f'<span class="rchev">▼</span></summary>'
f'<div class="rrows">'
+ "".join(_reading_row(r) for r in rs)
+ '</div></details>')
def readings_list(kept: list[dict]) -> str:
"""Every earlier reading of this track, as a list of chart weeks.
Under the report and under what to do about it: this week's answer is
what the reader came for, and what the charts said before it is the
second question.
The newest weeks stand open; everything older folds into one row that
names how much it holds and the span it covers, so a track read a hundred
times is still a list somebody can look down.
"""
groups = week_groups(kept)
if not groups:
return ""
n = len(kept)
out = [render.CSS, '<div class="ml">',
'<div class="rhead"><div class="lab">EARLIER READINGS</div>',
f'<div class="rkept">{n} kept · '
f'{len(groups)} chart week{"" if len(groups) == 1 else "s"}</div>'
'</div>',
'<p class="den rwhy">Every re-read keeps the answer it replaced, '
'grouped by the chart week it was read against. Opening one '
'changes nothing.</p>',
'<div class="rlist">']
for i, g in enumerate(groups[:OPEN_WEEKS]):
out.append(_week_details(g, open_=(i == 0))
if len(g["readings"]) > 1 else _week_row(g))
older = groups[OPEN_WEEKS:]
if older:
held = sum(len(g["readings"]) for g in older)
days = [d for d in (_daystamp(r.get("stamped_at"))
for g in older for r in g["readings"]) if d]
# Oldest first, the same way a group's own title runs.
span = f", {_days_span(days[-1], days[0])}" if days else ""
out.append(f'<details class="rfold"><summary>'
f'<span class="rfoldlab">{held} more reading'
f'{"" if held == 1 else "s"} · {_weeks_said(len(older))}'
f'{ESC(span)}</span>'
f'<span class="rchev">▼</span></summary>'
f'<div class="rrows">'
+ "".join(_week_row(g, short=True) for g in older)
+ '</div></details>')
out.append('</div></div>')
return "".join(out)
def earlier_reading_note(entry: dict, current_week: str,
n: int | None = None,
total: int | None = None) -> str:
"""Sits above an earlier reading, and says that is what it is.
Nothing on the report below it says which week it was read against — the
page looks exactly like the current one, because it IS a report, drawn
the same way. So this line carries both weeks: the one these numbers were
read against, and the one the track's current reading is from.
When both weeks are the same the two halves printed the same date twice
and the line said nothing at all, so that case says the thing that is
actually true about it: this track was read against those charts more
than once. The date it was read carries the difference either way, and
the number matches the button that was pressed to get here.
"""
week = str(entry.get("week") or "").strip() or "an earlier week"
cur = str(current_week or "").strip()
day = _daystamp(entry.get("stamped_at"))
who = (f"Reading {n} of {total}" if n and total
else f"Reading {n}" if n else "An earlier reading")
when = f"read on {ESC(day)} against" if day else "read against"
if cur and cur == week:
tail = ('The current reading is from the same charts — this '
'track was read against them more than once.')
elif cur and cur > week:
# Only said when it is true. The comparison is between two week
# stamps of the same shape, and anything else falls to the plain
# line below rather than to a claim about which came first.
tail = (f'The current reading is against the charts of '
f'{ESC(week_span(cur))}, a later week than this one.')
else:
tail = (f'The current reading is from the charts of '
f'{ESC(week_span(cur)) if cur else "?"}.')
return (render.CSS + '<div class="ml"><div class="note">'
'<div class="lab">AN EARLIER READING</div>'
f'<p class="why"><b>{ESC(who)}</b> — {when} the charts '
f'of {ESC(week_span(week))}. {tail}</p></div></div>')
def reading_day(entry: dict) -> str:
""""12 Aug" — the day one kept reading was made, or nothing when the
entry carries no date."""
return _daystamp(entry.get("stamped_at"))
def reading_stamp(entry: dict, n: int | None) -> str:
"""The line every card on a kept reading closes with.
Uppercased by the stylesheet. It exists so that a screenshot taken from
the middle of the page cannot pass for this week's answer: the banner is
at the top, and nobody screenshots the top.
"""
day = _daystamp(entry.get("stamped_at"))
bits = [f"reading {n}" if n else "an earlier reading"]
if day:
bits.append(f"read {day.lower()}")
bits.append("not this week's answer")
return " · ".join(bits)
def step_bar(entry: dict, n: int, total: int,
older: str | None, newer: str | None) -> str:
"""Which reading this is, and the step to the one either side of it.
One reading at a time: inside an open reading there is no list, because
the list is the thing the reader just came out of.
"""
week = str(entry.get("week") or "").strip()
day = _daystamp(entry.get("stamped_at"))
who = f"Reading {n} of {total}"
# The day this reading was made, which is what tells one record on this
# track apart from the next: two readings a day apart can be read against
# the same charts. The chart week stands in only for a reading filed
# before the day was written down.
tail = f"read {day}" if day else week_span(week) if week else ""
if tail:
# Dropped on a phone, where the bar is three controls across 390px
# and this line is the one that can be shorter.
who += f'<span class="rbarwk"> · {ESC(tail)}</span>'
def pill(file: str | None, label: str) -> str:
if not file:
return f'<span class="rstep off">{label}</span>'
return (f'<button type="button" class="rstep" '
f'data-pick="{_pick(file)}">{label}</button>')
return (f'<div class="rbar"><span class="rbarwho">{who}</span>'
f'<span class="rsteps">{pill(older, "‹ older")}'
f'{pill(newer, "newer ›")}</span></div>')
def reading_foot(entry: dict) -> str:
"""The line the page closes on. Says what this page is and what it is
not for — a reader who has scrolled a kept reading to the bottom is one
press from a Re-run that is not on this page at all."""
day = _daystamp(entry.get("stamped_at"))
from_when = f" from {ESC(day)}" if day else ""
return (render.CSS + '<div class="ml"><p class="rfoot">You are reading a '
f'record{from_when}. It cannot be re-run or deleted from here, '
f'and nothing on this page has changed by being opened.</p>'
'</div>')
def archive_page(parts: list[str], stamp: str) -> str:
"""A kept reading, on its own paper.
The tone and the stamp are carried by one wrapper so that every block
inside it — the banner, the report, the closing line — is on the same
page rather than three blocks that happen to sit near each other.
"""
said = str(stamp or "").replace('"', "")
return (f'<div class="arch" style=\'--stamp:"{ESC(said)}"\'>'
+ "".join(parts) + '</div>')
def batch_summary(done: list[dict], failed: list[dict]) -> str:
out = [render.CSS, '<div class="ml">',
f'<div class="kicker">Batch</div>'
f'<h1 class="hdr">{len(done)} of {len(done) + len(failed)} '
f'analysed</h1>',
'<p class="sub">Each one is saved. Open any of them in '
'<b>Tracks</b>.</p>']
for d in done:
lead = (d.get("top_markets") or [{}])[0]
flat = bool(d.get("flat"))
cls = "flat" if flat else _state(lead)
where = ESC(worldmap.market_name(lead.get("iso", ""), lead.get("name")))
out.append(f'<div class="card {cls}"><div class="mkt">'
f'<div class="name">{ESC(d.get("label", ""))}</div>')
if lead:
out.append(render.chip(cls))
out.append("</div>")
if lead and flat:
# Same rule as the Saved card: a field with no ranking has no
# closest market, and naming one would invent the finding the
# report refuses to make.
out.append('<p class="why">Reads much the same in every market '
'measured — no ranking to read.</p>')
elif lead and cls == "self":
out.append(f'<p class="why">Already charting in {where} — the '
f'reading there is the record meeting itself.</p>')
elif lead:
out.append(f'<p class="why">Closest to {where} at '
f'{livematch.pct(lead.get("best", 0))}</p>')
out.append("</div>")
for f in failed:
out.append(f'<div class="card flat"><div class="mkt">'
f'<div class="name">{ESC(f.get("label", ""))}</div>'
f'</div><p class="why">Could not be read: '
f'{ESC(str(f.get("error", "")))}</p></div>')
out.append("</div>")
return "".join(out)
def rescore_summary(out: dict) -> str:
n, total = len(out.get("rescored", [])), out.get("total", 0)
body = [render.CSS, '<div class="ml"><div class="card flat">',
f'<div class="lab">RE-SCORED</div>'
f'<p class="why"><b>{n} of {total}</b> saved tracks read '
f'against the charts of '
f'{ESC(week_span(str(out.get("week", ""))))}.</p>']
# A re-score can promote a window nobody had cut, and cuts it. Both lines
# are printed only when the number behind them is real: a sweep where
# every card already had its audio says neither.
cut = int(out.get("clips_cut") or 0)
missed = int(out.get("clips_missed") or 0)
if cut:
body.append(f'<p class="why"><b>{render._plural(cut, "clip")}</b> cut '
f'for windows these readings picked and no clip existed '
f'for.</p>')
if missed:
body.append(f'<p class="den">{render._plural(missed, "clip")} could '
f'not be cut. Those cards say so, and the readings '
f'themselves are complete.</p>')
for f in out.get("failed", []):
body.append(f'<p class="den">{ESC(str(f.get("id", "")))}: '
f'{ESC(str(f.get("error", "")))}</p>')
body.append("</div></div>")
return "".join(body)
def bands_note() -> str:
return (f'{livematch.PLACE_THRESHOLD:.0%} and above is a '
f'{render.CHIP["place"]}, {livematch.DERIVE_THRESHOLD:.0%} to '
f'{livematch.PLACE_THRESHOLD:.0%} is a {render.CHIP["derive"]}.')