openloops.dashboard#
One page you can look at instead of reading three command outputs.
render_dashboard() takes what openloops.owed(), openloops.blocked()
and openloops.ls() returned and renders a single self-contained HTML document —
no stylesheet, no script, no font and no request to anywhere. That is not decoration:
the page is meant to be published, and a published page runs no gh, shells out to
nothing and reaches no network.
So the page is a snapshot, and it says so in its largest type. This module inverts
the one rule openloops.digest holds to — a digest deliberately stamps no
generation time so that regenerating it is byte-stable — because here the generation
time is the whole claim. made_at is a required-in-practice argument rather than a
hidden now(), which is also what lets a test compare bytes.
The four registers are ordered the way a person needs them, and the fourth is the one the package exists for:
Needs you now — obligations still open, each printing the predicate that decided it, unabbreviated, so a reader can disagree with the verdict rather than absorb it.
Free to proceed — blocker edges that have all closed, led by how many days the work has been free while nothing anywhere said so.
In flight — what the sessions left open, newest last turn first.
Unknown — every
?, with why. It carries the envelope-level failures too: anowedthat could not list is not “nothing owed”, and rendering it as0would make this page worse than no page. When the count really is zero the section says which checks earned that, because a clean board with no provenance is the same lie told quietly.
Everything this module prints goes through Sanitizer, which is
openloops.egress.scrub() plus HTML escaping plus a scheme allowlist on every link.
The one carve-out is the shared kit below: register() and rail() **escape
nothing**. Every argument they take is markup, interpolated as given and some of it into
an unquoted attribute, because that is what a markup builder is; a caller that passes
anything it did not write itself puts it through Sanitizer first. This module
passes literals. A page that carries a
repository name and an issue title is fine; one that carries a home path or a token is
the failure openloops.egress exists to prevent, and this renderer scrubs its input
rather than trusting it. A credential-shaped field is withheld and counted, never
silently dropped — the count is printed in the footer.
CSS, Sanitizer, register() and rail() are public for
sibling renderers — a tool that shows a different half of the same picture and wants to
look like this page — so that the look, the egress rule and the markup the stylesheet
dresses stay in one place instead of being copied and drifting. They are the shared kit:
the stylesheet here is what makes register--needs a colour and rail a column, so
a package that writes those class names by hand is one rename away from a broken page.
>>> html = render_dashboard({}, {}, [], made_at='2026-01-01T00:00:00Z')
>>> '<title>' in html and 'snapshot' in html
True
Module Attributes
What the page is called — in the tab, in a gallery, and in its own masthead. |
|
How many session rows are printed. |
|
Where the age gauge tops out. |
Functions
|
The four figures across the top of the page. |
|
The fixed left column of every row: what state it is in, and for how long. |
|
One band: a heading, the count in the largest figure on the page, and its rule. |
|
The three envelopes as one self-contained HTML page. |
|
How many OBLIGATIONS could not be checked against the world. |
|
How many things read |
Classes
|
Scrub, then escape. |
- openloops.dashboard.DFLT_MAX_SESSIONS = 40#
How many session rows are printed. The register states the true total either way; a page that showed all 150-odd would bury the two registers above it.
- openloops.dashboard.DFLT_TITLE = 'Open Loops Board'#
What the page is called — in the tab, in a gallery, and in its own masthead.
- openloops.dashboard.GAUGE_FULL_DAYS = 90#
Where the age gauge tops out. Ninety days is the point past which the length of the bar stops being informative and the number beside it is doing all the work.
- class openloops.dashboard.Sanitizer(aliases=None)[source]#
Bases:
objectScrub, then escape. The single path from an envelope to the document.
Two failures are possible and they are treated differently, exactly as
openloops.egressprescribes. A home path is rewritten — it is an identifier and the tail is the part a reader needs. A credential is withheld and counted: the field is replaced by a visible notice naming the pattern class, never the text, andwithheldis printed in the footer so the run reports it rather than quietly losing a field.>>> s = Sanitizer() >>> s.text('a < b') 'a < b' >>> s.text('token=' + 'ghp_' + 'A' * 36) '[withheld: credential-shaped text (github_token)]' >>> s.withheld ['github_token']
- openloops.dashboard.headline_counts(owed, blocked, sessions)[source]#
The four figures across the top of the page.
Noneis?, never a zero.Shared by the masthead and by
openloops.tools.dashboard(), so the number a caller reads back is the number the page printed.>>> headline_counts({'listed': True, 'counts': {'open': 2}, 'rows': []}, ... {'listed': False}, [])['free_to_proceed'] is None True
- openloops.dashboard.rail(chip, tone, age, unit='d', *, extra='')[source]#
The fixed left column of every row: what state it is in, and for how long.
Shared kit (see the module docstring):
CSSstylesrail,chip,chip--<tone>andage, so a sibling renderer that builds this by hand breaks the next time a class name here changes.ageis the figure beside the unit — a number, orNonefor the?that means nobody knows. It is not required to be a day count: pass a string and aunitof your own for a page whose durations run from seconds to days.extrais further markup placed between the state chip and the age — a second chip a sibling page needs and this one has no equivalent of.Nothing here is escaped,
extraleast of all: see the module docstring.- Return type:
>>> rail("owed", "needs", 3) '<div class="rail"><span class="chip chip--needs">owed</span><span class="age"><b>3</b><i>d</i></span></div>'
- openloops.dashboard.register(*, ident, name, figure, tone, rule, body, folds=False, start_open=False, extra='')[source]#
One band: a heading, the count in the largest figure on the page, and its rule.
Shared kit (see the module docstring):
CSSstylesregister,register--<tone>andregister-head, laying the head out asauto 1fr— the figure in the first column, the heading and the rule in the second.foldsrenders the band as a<details>a person can close,start_openopening it anyway; both default off, which is the plain<section>this page has always rendered. A<summary>may hold phrasing content and a heading only, so the folding head carries the figure and the rule as<span>``s rather than ``<p>``s. :data:`CSS` places those three flat children into the same ``auto 1frgrid a plain section’s head uses (figure down column one across both rows, heading and rule down column two) and restores the disclosure affordance adisplay:gridsummary would otherwise cost it (#13).start_openis ignored whenfoldsis false — there is no disclosure to open — because a caller decidesfoldsfrom whether it has rows and passes both.A band with nothing in it is not worth folding — there is nothing to hide — so that decision belongs to the caller, which knows whether its body is rows.
extrais markup that heads the band’s body. A<summary>may not hold interactive content and what a caller puts here is usually a control, so in the<details>it goes inside the body; in the<section>it stays in the head, under the rule. That is above the head’s hairline in one form and below it in the other, which is how the caller that needs it already reads.Nothing here is escaped: see the module docstring.
identandtonereach an unquoted attribute, so they are the two that must be literals or already safe.- Return type:
>>> register(ident="x", name="Needs you", figure="2", tone="needs", rule="now", body="") '<section class="register register--needs" id="x"><div class="register-head"><p class="figure">2</p><div><h2>Needs you</h2><p class="rule">now</p></div></div></section>' >>> register(ident="x", name="Quiet", figure="9", tone="done", rule="why", body="", ... folds=True, start_open=True) '<details class="register register--done" id="x" open><summary class="register-head"><span class="figure">9</span><h2>Quiet</h2><span class="rule">why</span></summary></details>'
- openloops.dashboard.render_dashboard(owed, blocked, sessions, *, made_at=None, source='', title='Open Loops Board', max_sessions=40, standalone=True, aliases=None)[source]#
The three envelopes as one self-contained HTML page. No network, no script.
owedandblockedare the envelopesopenloops.owed()andopenloops.blocked()return;sessionsis the listopenloops.ls()returns. All three may be empty mappings, and an envelope whoselistedisFalserenders as?throughout rather than as zero.made_atis the moment the snapshot was taken and is printed in the largest type on the page. It defaults to now, but a caller that wants a byte-stable page passes it — this is the one module in openloops that stamps a generation time, and it does so because the whole claim of the page is when.standalonewraps the output in a document scaffold for a file you open yourself. PassFalsefor a host that supplies its own<head>— a published artifact does — and the same page comes back as title, styles and content only.max_sessionsbounds the in-flight register; the true total is stated either way.- Return type:
>>> page = render_dashboard( ... {'listed': False, 'error': 'gh: not logged in', 'counts': {}}, ... {'listed': True, 'counts': {'unblocked': 0, 'total': 0}, 'rows': []}, ... [], made_at='2026-01-01T00:00:00Z') >>> 'gh: not logged in' in page and 'could not' in page True
- openloops.dashboard.unchecked_count(owed, blocked)[source]#
How many OBLIGATIONS could not be checked against the world.
Noneif unknowable.Deliberately narrower than
unknown_count(), and the two must not be merged in the masthead. A?on an obligation means the world could not be reached — a timeout, a missinggh, an untrusted owner. A low-confidence session digest means something else entirely: the digest half checks nothing against the world by design, so it has nothing it was unable to check. Rolling forty of the second into the first reports a crisis of forty when the real number is one, and in the alarming direction.>>> unchecked_count({'listed': True, 'rows': [{'state': 'unknown'}]}, ... {'listed': True, 'rows': []}) 1 >>> unchecked_count({'listed': False}, {'listed': True, 'rows': []}) is None True
- openloops.dashboard.unknown_count(owed, blocked, sessions)[source]#
How many things read
?, orNonewhen even that cannot be counted.A register that never listed could be hiding any number of unknown rows, so the headline figure for unknowns is itself unknown. Reporting
2there — the two failures we happen to know about — is the exact shape of the lie this page is against, and it is a lie a reader has no way to spot.It counts the rows that read
?, not the entries in the list below: one entry can stand for forty sessions, and reporting1for those would be the tidier number rather than the true one.>>> unknown_count({'listed': True, 'rows': []}, {'listed': True, 'rows': []}, [{}]) 0 >>> lows = [{'confidence': 'low'}] * 5 >>> unknown_count({'listed': True, 'rows': []}, {'listed': True, 'rows': []}, lows) 5 >>> unknown_count({'listed': False}, {'listed': True, 'rows': []}, [{}]) is None True