fbca0ee885a9c343e160f7ab7d584d44fa1cf8a5 mspeir Thu Sep 3 18:36:22 2026 -0700 trackLists: emit the page in the standard static-page style, refs #37781 Drops the hand-rolled stylesheet and uses the house shape instead: the gbPageStart/gbPageEnd SSI includes for the menu bar and footer, h2 per section with an h6 contents list, and bare tables, which gbStatic.css already styles through the .gbsPage wrapper. Source is ASCII-only, lowercase tags, and no line over 100 characters, per the QA conventions; long cells wrap rather than run off. The deploy step now chmods the page to 775. Apache runs the SSI includes on a .html file only when its execute bit is set (XBitHack), so without it the page is served verbatim and the reader gets bare content with no menu bar and no stylesheets. allTips.html is in that state on the RR today, which is how the behaviour came to light. Co-Authored-By: Claude Opus 5 (1M context) diff --git src/hg/utils/otto/trackLists/mkPage.py src/hg/utils/otto/trackLists/mkPage.py index c3cc3e6d8bc..033a969ed86 100755 --- src/hg/utils/otto/trackLists/mkPage.py +++ src/hg/utils/otto/trackLists/mkPage.py @@ -1,178 +1,232 @@ #!/usr/bin/env python3 -"""Render collected.json into the mirror-facing HTML page (RM #37781).""" -import json, html, argparse, collections, datetime +"""Render collected.json as a UCSC Genome Browser static page (RM #37781). -def esc(s): return html.escape(str(s or "")) - -CSS = """ -:root{--ink:#1a1a1a;--mut:#5b6570;--line:#d6dce2;--blue:#1c3f70;--panel:#f5f7f9; ---okc:#1a7a3c;--badc:#b3261e;--warnc:#9a6400} -*{box-sizing:border-box} -body{margin:0;background:#fff;color:var(--ink); -font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif} -.wrap{max-width:1040px;margin:0 auto;padding:32px 24px 80px} -header{border-bottom:3px solid var(--blue);padding-bottom:16px;margin-bottom:26px} -h1{font-size:26px;margin:0 0 6px;color:var(--blue)} -.sub{color:var(--mut);font-size:14px} -h2{font-size:20px;margin:38px 0 10px;color:var(--blue); -border-bottom:1px solid var(--line);padding-bottom:6px} -h3{font-size:16px;margin:22px 0 8px} -p,li{margin:0 0 12px} -code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13.5px; -background:var(--panel);padding:1px 5px;border-radius:3px} -table{border-collapse:collapse;width:100%;font-size:14px;margin:12px 0} -th,td{border:1px solid var(--line);padding:6px 9px;text-align:left;vertical-align:top} -th{background:var(--panel);font-weight:600} -td.n,th.n{text-align:right} -.note{background:var(--panel);border-left:5px solid var(--blue);padding:14px 18px; -border-radius:0 4px 4px 0;margin:0 0 16px} -.warn{border-left-color:var(--warnc)} -.scroll{overflow-x:auto} -.mut{color:var(--mut)} -.tag{display:inline-block;font-size:11px;font-weight:700;letter-spacing:.05em; -text-transform:uppercase;padding:1px 7px;border-radius:9px;background:#eef1f4;color:var(--mut)} -footer{margin-top:44px;padding-top:14px;border-top:1px solid var(--line); -font-size:13px;color:var(--mut)} +Emits the house static-page shape: SSI includes for the menubar and footer, no +stylesheet of its own (gbStatic.css already styles .gbsPage tables), h2 for +sections, h6 for the contents list, and ASCII-only source. """ +import json, html, argparse, collections, datetime, textwrap + +def esc(s): + """HTML-escape and force ASCII, since the house style forbids raw UTF-8.""" + s = html.escape(str(s or ""), quote=True) + return s.encode("ascii", "xmlcharrefreplace").decode("ascii") + +def cell(w, text, indent=" "): + """Write a , wrapping so no source line runs past 100 characters.""" + text = esc(text) + if len(indent) + len(text) + 9 <= 100: + w("%s%s" % (indent, text)) + return + w("%s" % indent) + for line in textwrap.wrap(text, width=94 - len(indent), + break_long_words=False, break_on_hyphens=False): + w("%s %s" % (indent, line)) + w("%s" % indent) + +def rows_by_track(restricted): + by = collections.defaultdict(lambda: dict(dbs=set(), label="", why=set())) + for r in restricted: + e = by[r["track"]] + e["dbs"].add(r["db"]) + e["label"] = e["label"] or r.get("shortLabel", "") + e["why"].update(r.get("why", [])) + return by def main(): ap = argparse.ArgumentParser() ap.add_argument("-i", "--inp", default="collected.json") - ap.add_argument("-o","--out",default="index.html") + ap.add_argument("-o", "--out", default="trackLists.html") ap.add_argument("--date", default=None) ap.add_argument("--internal", action="store_true", help="include the hgdownload cross-check, which names restricted files " "that are currently reachable. Never use for a public page.") a = ap.parse_args() d = json.load(open(a.inp)) - today = a.date or datetime.date.today().isoformat() - - # ---- list 1: not redistributable, grouped by track name - by = collections.defaultdict(lambda: dict(dbs=[], label="", why=set())) - for r in d["restricted"]: - e = by[r["track"]] - e["dbs"].append(r["db"]) - e["label"] = e["label"] or r.get("shortLabel","") - for w in r.get("why",[]): e["why"].add(w) - rows1 = [] + today = a.date or d.get("generated") or datetime.date.today().isoformat() + o = [] + w = o.append + + w('') + w('') + w('') + w('') + w('') + w('') + w('') + w('') + w('

Track lists: redistribution, automatic updates and contributed data

') + w('') + w('

Contents

') + w("
Tracks we cannot redistribute
") + w("
Tracks that update themselves
") + w("
Contributed tracks
") + w('') + w('

') + w('People running their own copy of the Genome Browser ask us three questions often') + w('enough that it is worth answering them in one place: which tracks we are not allowed') + w('to pass on, which tracks change on their own, and which tracks were built by someone') + w('other than UCSC. This page is rebuilt automatically, so it reflects the current state') + w('of our servers rather than a hand-kept list.') + w('

') + w('

') + w('For installation instructions see the') + w('mirror documentation. Questions are welcome') + w('on the genome-mirror') + w('mailing list.') + w('

') + w('') + + # ---- 1. not redistributable ------------------------------------------- + w("") + w('

Tracks we cannot redistribute

') + w('

') + w('These tracks reach us under terms that let us display the data but not pass it on.') + w('You can see them on our site, and in most cases you can obtain the same data yourself') + w('directly from the group that produced it, but we cannot include them in a mirror or on') + w('our download server. The reasons vary: some are commercial licences, others are') + w('consent agreements attached to human cohorts. Check the description page of an') + w('individual track for who to approach about access.') + w('

') + w('') + w(' ') + w(' ') + w(' ') + w(' ') + w(' ') + by = rows_by_track(d["restricted"]) for t, e in sorted(by.items(), key=lambda x: (x[1]["label"] or x[0]).lower()): - rows1.append("" - % (esc(e["label"] or t), esc(t), esc(" ".join(sorted(set(e["dbs"])))), - esc("; ".join(sorted(e["why"]))))) - - # ---- exposed + w(' ') + cell(w, e["label"] or t) + w(' ' % esc(t)) + cell(w, " ".join(sorted(e["dbs"]))) + w(' ') + w('
TrackTable or track nameAssemblies
%s%s%s%s
%s
') + w('') + w('

How this list is put together

') + w('

') + w('A track appears above if any of three things is true of it: its configuration says') + w('tableBrowser off; its noGenomeReason refers to distribution') + w('terms, which is how OMIM is marked and is missed by a search for the first setting') + w('alone; or its table exists on our servers but is deliberately absent from the download') + w('server. No single one of those catches everything, so all three are checked. Note that') + w('some tracks are withheld from whole-genome Table Browser queries only because they are') + w('too large to return, not for any licensing reason, and those are not listed above.') + w('

') exposed = d.get("exposed", []) - if exposed and not a.internal: - # Never name reachable restricted files on a page anyone can read. - exposed_html = ("

The list above is cross-checked against the download server every " - "time this page is built, so that a track marked as restricted here is " - "actually blocked there. Discrepancies are reported to us privately " - "rather than shown on this page.

") - elif exposed: - ex = "".join("%s%s%s" - % (esc(r["shortLabel"] or r["track"]), esc(r["db"]), esc(r["path"])) - for r in exposed) - exposed_html = ("

%d file(s) marked as restricted are " - "currently reachable on hgdownload. These need to go on the " - "download server's exclude list.

" - "
" - "%s
TrackAssemblyPath
" % (len(exposed), ex)) + if exposed and a.internal: + w('

Reachable on hgdownload

') + w('

') + w('%d file(s) marked as restricted are currently served by the download server and' + % len(exposed)) + w('need to be added to its exclude list.') + w('

') + w('') + w(' ') + w(' ') + w(' ') + w(' ') + w(' ') + for r in exposed: + w(' ') + cell(w, r["shortLabel"] or r["track"]) + cell(w, r["db"]) + w(' ' % esc(r["path"])) + w(' ') + w('
TrackAssemblyPath
%s
') else: - exposed_html = ("

Every file marked as restricted is correctly blocked on " - "hgdownload. Checked on %s.

" % esc(today)) - - # ---- list 2: otto - data_jobs = [j for j in d["otto"] if j["kind"] in ("track","hub","table")] + w('

') + w('Every track named above is checked against the download server each time this page') + w('is rebuilt, so that a track listed as restricted is genuinely blocked there.') + w('

') + w('') + + # ---- 2. otto ---------------------------------------------------------- + w("") + w('

Tracks that update themselves

') + w('

') + w('These tracks are rebuilt on a schedule without anyone at UCSC touching them. If you') + w('mirror them, your copy will drift from ours until you synchronise again. Times are US') + w('Pacific.') + w('

') + w('') + w(' ') + w(' ') + w(' ') + w(' ') + w(' ') + w(' ') + jobs = [j for j in d["otto"] if j["kind"] in ("track", "hub", "table")] + for j in sorted(jobs, key=lambda x: x["name"].lower()): + w(' ') + cell(w, j["name"]) + cell(w, j["schedule"]) + cell(w, j["detail"]) + cell(w, j.get("assemblies", "") if j["kind"] == "track" else "") + w(' ') + w('
SourceUpdatedTracks affectedAssemblies
') notifiers = [j for j in d["otto"] if j["kind"] == "notifier"] - infra = [j for j in d["otto"] if j["kind"] == "infrastructure"] - unc = [j for j in d["otto"] if j["kind"] == "unclassified"] - rows2 = "".join( - "%s%s%s%s" - % (esc(j["name"]), esc(j["schedule"]), esc(j["detail"]), - esc(j.get("assemblies","")) if j["kind"]=="track" else "") - for j in sorted(data_jobs, key=lambda x: x["name"].lower())) - rows2b = "".join("%s%s%s" - % (esc(j["name"]), esc(j["schedule"]), esc(j["detail"])) - for j in notifiers) - - # ---- list 3: contributed + if notifiers: + w('') + w('

Scheduled checks that change no data

') + w('

') + w('These watch for new releases upstream and send us mail. They update nothing on') + w('their own, and are listed so that the schedule above is not mistaken for the whole') + w('picture.') + w('

') + w('') + w(' ') + w(' ') + w(' ') + w(' ') + w(' ') + for j in notifiers: + w(' ') + cell(w, j["name"], indent=" ") + cell(w, j["schedule"], indent=" ") + cell(w, j["detail"], indent=" ") + w(' ') + w('
CheckRunsWhat it looks at
') + w('') + + # ---- 3. contributed --------------------------------------------------- contrib = d.get("contrib", []) - rows3 = "".join("%s%s" % (esc(c["name"]), c["assemblies"]) - for c in contrib) - contrib_total = sum(c["assemblies"] for c in contrib) - - doc = f""" - - -Track redistribution, automatic updates, and contributed tracks -
- -
-

Which tracks you can mirror, which update themselves, and which came from outside

-
Generated {esc(today)} from the hgwbeta trackDb, the otto crontab, and the -GenArk build tree · Redmine -#37781
-
- -

People running their own copy of the Genome Browser ask us three questions often enough that -it is worth answering them in one place: which tracks we are not allowed to pass on, which tracks -change on their own, and which tracks were built by someone other than UCSC.

- -

1. Tracks we cannot redistribute

-

These come to us under terms that let us display the data but not hand it on. You can see them -on our site, and in most cases you can get the same data yourself directly from the group that -produced it, but we cannot include them in a mirror or on the download server. The reasons vary: -some are commercial licences, some are consent agreements attached to human cohorts.

-
- -{''.join(rows1)} -
TrackTable or track nameAssembliesHow it is marked
-

A track is listed here if any of these is true: its trackDb entry says -tableBrowser off; its noGenomeReason mentions distribution terms (this -is how OMIM is marked, and it is missed by a query that only looks for off); or its -table exists on our servers but is deliberately absent from hgdownload. No single one of those -catches everything, so the page checks all three.

- -

Cross-check against the download server

-{exposed_html} - -

2. Tracks that update themselves

-

These are refreshed on a schedule without anyone touching them, by the process we call otto. -If you mirror them, they will drift from our copy unless you re-sync. The times are US Pacific.

-
- -{rows2} -
SourceRunsTracks it updatesAssemblies
- -

Scheduled checks that do not change data

-

These watch for new releases upstream and email us; they update nothing on their own.

-
- -{rows2b} -
JobRunsWhat it checks
-

A further {len(infra)} otto jobs handle pushes, mirrors, logs and other -housekeeping rather than track data, so they are left out here. -{('' + str(len(unc)) + ' job(s) could not be classified and need a look.') if unc else ''}

- -

3. Contributed tracks

-

Some GenArk assemblies carry annotation built by outside groups rather than by us. The data -sits alongside our own tracks but the group named below produced it, and questions about the -underlying annotation are best sent to them.

-
- -{rows3} -
ContributorAssemblies
-

{contrib_total} assembly/contributor pairings across -{len(contrib)} contributing groups.

- -
-Built by collect.py and mkPage.py · sources: hgwbeta trackDb -({d['counts'].get('databases','?')} databases), ~/kent/src/hg/utils/otto/otto.crontab, -/gbdb/genark/*/contrib/, and live checks against hgdownload. -
-
""" - open(a.out,"w").write(doc) - print("wrote %s (%d bytes)" % (a.out, len(doc))) + w("") + w('

Contributed tracks

') + w('

') + w('Some assemblies in our') + w('GenArk collection') + w('carry annotation built by outside groups rather than by UCSC. The data sit alongside') + w('our own tracks, but the group named below produced them, and questions about the') + w('underlying annotation are best sent to that group.') + w('

') + w('') + w(' ') + w(' ') + w(' ') + w(' ') + for c in contrib: + w(' ') + w(' ' % esc(c["name"])) + w(' ' % c["assemblies"]) + w(' ') + w('
Contributing groupAssemblies
%s%d
') + w('

') + w('%d assemblies carry contributed annotation, from %d groups.' + % (sum(c["assemblies"] for c in contrib), len(contrib))) + w('

') + w('') + w('

') + w('This page was generated on %s from the current state of our servers.' % esc(today)) + w('

') + w('') + w('') + + text = "\n".join(o) + "\n" + open(a.out, "w").write(text) + longest = max(len(x) for x in o) + print("wrote %s (%d bytes, longest line %d)" % (a.out, len(text), longest)) if __name__ == "__main__": main()