Files
cgalo5758 94b7c364de Characterize the organization page: a snapshot of its data and render, a table of its failed reads, and its reads per row
No production file changes. The page's output is unchanged because nothing
the page runs was edited; the commit adds tests and the builders they use.

TestOrgEnrollmentGolden compares, for nine states (no organization, no
pool, the default plan alone, a rich organization with one plan on two
ladders and a subscription on a third, a grant with a note, a flagged
subscription, usage, an open overdue invoice and a rule change, a removed
tier, a second pool, a suspended subscription, twelve tier changes paged, and
the rich one searched), the data the loader returns and the
operator_enrollment.html render with files under testdata/org_enrollment.
The data leaves out the Issue grant form, the grants ledger's form and the
grants rows, which the render holds. The files were read against the state
table before they were committed.

TestOrgEnrollmentReadFailures fails each of 23 reads in turn and pins the
section it feeds and, where it is words on the page, the render. The
operator read-count test gains the row "organization page" at 9 reads per
row, as predicted.

world gains Member, Suspend, Pool and ConferOn, and each says which state it
writes that the console does not: Pool and ConferOn, a second
pool; Member, a member who is not an organization's owner; Suspend, a
suspended subscription whose row keeps its status.

Shown to change no output: 66 breaks of the page's code, each applied on its
own, are all caught by the new tests; the tests of ./internal/server and
./internal/testkit/... pass, with no test or subtest of the parent missing or
changed in result, and 35 added in ./internal/server (the golden's 10, the
failure table's 24 and the read-count row); the golden passes three runs in a
row on a reset database.

Unprobed, and why:
- The two advisory failure rows keep their fallback in the nothing-held
  state; no one-line edit flags a product without the rows the failed read
  did not return, so a break there cannot be written.
- The order of the rungs of one act (Hosting before Chat) is the position
  query's order among equal keys, which it does not specify; accepted.
- The members cap: no state holds more than 16 members.
- Values copied unchanged from a row (organization, pool, set and resource
  names, keys, statuses, emails, roles, notes, limit lines): a copy can only
  be dropped, and the golden shows a drop.
- The ledger's rows, its History tab and its search beyond one break: the
  grants goldens pin them; this page's render only holds them.
- The pager's own markup (the Showing line, Previous and Next): the pager
  test of the embedded lists and the list-scale tests pin it.
- The pool type badge, which is a copied value the template renders.
2026-10-09 19:35:24 -05:00

433 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="d-flex align-items-start justify-content-between gap-3 mb-3 app-page-header">
<div class="app-page-header-text">
<nav aria-label="breadcrumb"><ol class="breadcrumb small mb-1">
<li class="breadcrumb-item"><a href="/operator">Operator</a></li><li class="breadcrumb-item"><a href="/operator/organizations">Organizations</a></li>
<li class="breadcrumb-item active" aria-current="page">many-changes</li>
</ol></nav>
<h1 class="h2 mb-0">many-changes</h1>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-3">Owner</dt>
<dd class="col-sm-9">
<a href="/operator/persons/<person:Ann>">Ann</a>
</dd>
</dl>
</div>
</div>
<div class="d-flex align-items-center justify-content-between gap-3 mt-4 mb-2 app-section-header">
<h2 class="h5 mb-0">Billing</h2>
</div>
<div class="card mb-3">
<div class="card-body">
<div class="text-center py-4">
<p class="text-muted mb-2">No billing account exists for this organization yet.</p>
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-between gap-3 mt-4 mb-2 app-section-header">
<h2 class="h5 mb-0">Pools <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Pools" data-bs-content="A pool holds what an organization&#39;s plan and grants deliver; its sites and services draw from it." aria-label="Help: Pools"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button></h2>
</div>
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h3 class="h6 mb-1">
Default <span class="badge text-bg-success">Active</span>
</h3>
<p class="text-muted mb-0">No plan is delivering into this pool</p>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-between gap-3 mt-4 mb-2 app-section-header">
<h2 class="h5 mb-0">Grants</h2>
</div>
<div id="plan-grants-panel">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center gap-2">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item"><a class="nav-link active" href="/operator/organizations/<org:many-changes>?tc_page=2" hx-get="/operator/organizations/<org:many-changes>?tc_page=2" hx-target="#plan-grants-panel" hx-select="#plan-grants-panel" hx-select-oob="#tier-changes-panel,#entitlement-changes-panel" hx-swap="outerHTML" hx-push-url="true">Active</a></li>
<li class="nav-item"><a class="nav-link " href="/operator/organizations/<org:many-changes>?tab=history&amp;tc_page=2" hx-get="/operator/organizations/<org:many-changes>?tab=history&amp;tc_page=2" hx-target="#plan-grants-panel" hx-select="#plan-grants-panel" hx-select-oob="#tier-changes-panel,#entitlement-changes-panel" hx-swap="outerHTML" hx-push-url="true">History</a></li>
</ul>
<div class="flex-shrink-0">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="collapse" data-bs-target="#issueGrantPanel" aria-expanded="false" aria-controls="issueGrantPanel"><svg class="app-opener-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true" focusable="false"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>Issue grant</button>
</div>
</div>
<div class="card-body">
<div class="collapse" id="issueGrantPanel">
<div class="card app-form-box mb-3"><div class="card-body">
<form id="form-operator.enrollment.grant.issue" data-form="operator.enrollment.grant.issue" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/organizations/<org:many-changes>/grant/create" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
<div id="form-operator.enrollment.grant.issue-error" class="app-form-error" role="alert"></div>
<div class="app-form-message mb-2">A tier product moves the pool to that tier, up or down.</div>
<div class="app-form-fields">
<div id="form-operator.enrollment.grant.issue-product_id" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.enrollment.grant.issue-product_id-control">Product</label>
<select class="form-select" id="form-operator.enrollment.grant.issue-product_id-control" name="product_id" required aria-describedby="form-operator.enrollment.grant.issue-product_id-warning" hx-get="/partials/operator/products/rule-check" hx-swap="innerHTML" hx-target="#form-operator\.enrollment\.grant\.issue-product_id-warning" hx-trigger="change">
<option value="" disabled selected>Choose a product</option>
<option value="<product:Basic>">Basic</option>
<option value="<product:Forum>">Forum</option>
<option value="<product:Forum2>">Forum2</option>
<option value="<product:Large>">Large</option>
<option value="<product:Lite>">Lite</option>
<option value="<product:Lite2>">Lite2</option>
<option value="<product:Mid>">Mid</option>
<option value="<product:Plus>">Plus</option>
<option value="<product:Pro>">Pro</option>
<option value="<product:Small>">Small</option>
<option value="<product:Small2>">Small2</option>
<option value="<product:Starter>">Starter</option>
<option value="<product:Starter2>">Starter2</option>
<option value="<product:Top>">Top</option>
</select>
<div id="form-operator.enrollment.grant.issue-product_id-warning"></div>
</div><div id="form-operator.enrollment.grant.issue-quantity" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.enrollment.grant.issue-quantity-control">Quantity <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Quantity" data-bs-content="Seats, for per-seat products." aria-label="Help: Quantity"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="number" class="form-control" id="form-operator.enrollment.grant.issue-quantity-control" name="quantity" value="" placeholder="1" min="1" max="1000000">
</div><div id="form-operator.enrollment.grant.issue-valid_until" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.enrollment.grant.issue-valid_until-control">Valid until <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Valid until" data-bs-content="Leave blank for a grant with no end date." aria-label="Help: Valid until"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="datetime-local" class="form-control" id="form-operator.enrollment.grant.issue-valid_until-control" name="valid_until" value="" >
</div><div id="form-operator.enrollment.grant.issue-reason" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.enrollment.grant.issue-reason-control">Reason</label>
<select class="form-select" id="form-operator.enrollment.grant.issue-reason-control" name="reason" required>
<option value="" disabled selected>Choose a reason</option>
<option value="manual">manual</option>
<option value="evaluation">evaluation</option>
<option value="promotional">promotional</option>
<option value="complimentary">complimentary</option>
<option value="sponsored">sponsored</option>
<option value="board_decision">board_decision</option>
<option value="legacy">legacy</option>
</select>
</div><div id="form-operator.enrollment.grant.issue-description" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.enrollment.grant.issue-description-control">Note <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Note" data-bs-content="Why this grant is issued. Shown with the grant in its history." aria-label="Help: Note"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="text" class="form-control" id="form-operator.enrollment.grant.issue-description-control" name="description" value="" maxlength="500" autocomplete="off">
</div><div id="form-operator.enrollment.grant.issue-resumes_after_supersession" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.enrollment.grant.issue-resumes_after_supersession-control">Resumes</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Resumes" data-bs-content="If a plan replaces this grant, the grant delivers again when that plan ends." aria-label="Help: Resumes"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.enrollment.grant.issue-resumes_after_supersession-control" name="resumes_after_supersession" value="true" ></div>
</div>
</div>
<div class="d-flex flex-wrap gap-2 app-form-commit">
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Issue grant</button>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="collapse" data-bs-target="#issueGrantPanel">Cancel</button>
</div>
</form>
</div></div>
</div>
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<form id="form-operator.list.controls" data-form="operator.list.controls" data-form-kind="search" class="app-form app-form-bar" method="GET" action="/operator/organizations/<org:many-changes>" role="search" hx-get="/operator/organizations/<org:many-changes>" hx-target="#plan-grants-panel" hx-swap="outerHTML" hx-push-url="true" hx-select="#plan-grants-panel" hx-select-oob="#tier-changes-panel,#entitlement-changes-panel">
<div id="form-operator.list.controls-error" class="app-form-error" role="alert"></div>
<label class="form-label visually-hidden" for="form-operator.list.controls-q-control">Search this list</label>
<input type="hidden" name="tc_page" value="2" >
<div class="input-group">
<span class="input-group-text"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.44.44.98.98l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.98-.98zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/></svg></span>
<input type="text" class="form-control" id="form-operator.list.controls-q-control" name="q" value="" placeholder="Product or reason" autocomplete="off">
<button type="submit" class="btn btn-outline-secondary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Search</button>
</div>
</form>
</div>
<div class="text-center py-4">
<p class="text-muted mb-2">No grants are delivering right now.</p>
<p class="text-muted small mb-2">The History tab lists every grant ever issued.</p>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-between gap-3 mt-4 mb-2 app-section-header">
<h2 class="h5 mb-0">Members</h2>
</div>
<div class="card mb-3">
<div class="card-body">
<div class="text-center py-4">
<p class="text-muted mb-2">No members.</p>
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-between gap-3 mt-4 mb-2 app-section-header">
<h2 class="h5 mb-0">Entitlement changes</h2>
</div>
<div id="entitlement-changes-panel">
<div class="card mb-3">
<div class="card-body">
<div class="text-center py-4">
<p class="text-muted mb-2">No entitlement changes recorded yet.</p>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-between gap-3 mt-4 mb-2 app-section-header">
<h2 class="h5 mb-0">Tier changes <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Tier changes" data-bs-content="Every tier change on this organization&#39;s plan ladders, newest first. Tier names reflect the ladder&#39;s current shape." aria-label="Help: Tier changes"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button></h2>
</div>
<div id="tier-changes-panel">
<div class="card mb-3">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-sm table-record">
<thead>
<tr>
<th>Ladder</th>
<th>Change</th>
<th>From</th>
<th>To</th>
<th>By</th>
<th>Reason</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Cycle</th>
<td><span class="badge text-bg-light border">Ended</span></td>
<td>Lite2</td>
<td>—</td>
<td>
Ann
</td>
<td class="text-break" title="operator_revocation">Revoked by an operator</td>
<td class="text-nowrap"><small><span class="d-block"><today></span><span class="d-block text-muted"><hh:mm></span></small></td>
</tr>
<tr>
<th scope="row">Cycle</th>
<td><span class="badge text-bg-light border">Started</span></td>
<td>—</td>
<td>Lite2</td>
<td>
Ann
</td>
<td class="text-break" title="test fixture">Complimentary</td>
<td class="text-nowrap"><small><span class="d-block"><today></span><span class="d-block text-muted"><hh:mm></span></small></td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mt-2">
<p class="small text-body-secondary mb-0">Showing 11–12 of 12</p>
<p class="small text-body-secondary mb-0">Per page:
<strong>10</strong>
<a href="/operator/organizations/<org:many-changes>?tc_per=25" hx-get="/operator/organizations/<org:many-changes>?tc_per=25" hx-target="#tier-changes-panel" hx-select="#tier-changes-panel" hx-select-oob="#plan-grants-panel,#entitlement-changes-panel" hx-swap="outerHTML" hx-push-url="true">25</a>
<a href="/operator/organizations/<org:many-changes>?tc_per=50" hx-get="/operator/organizations/<org:many-changes>?tc_per=50" hx-target="#tier-changes-panel" hx-select="#tier-changes-panel" hx-select-oob="#plan-grants-panel,#entitlement-changes-panel" hx-swap="outerHTML" hx-push-url="true">50</a>
</p>
<nav aria-label="List pages">
<ul class="pagination pagination-sm mb-0">
<li class="page-item "><a class="page-link" href="/operator/organizations/<org:many-changes>" hx-get="/operator/organizations/<org:many-changes>" hx-target="#tier-changes-panel" hx-select="#tier-changes-panel" hx-select-oob="#plan-grants-panel,#entitlement-changes-panel" hx-swap="outerHTML" hx-push-url="true">Previous</a></li>
<li class="page-item disabled"><span class="page-link">Page 2 of 2</span></li>
<li class="page-item disabled"><a class="page-link" href="/operator/organizations/<org:many-changes>?tc_page=3" aria-disabled="true" tabindex="-1">Next</a></li>
</ul>
</nav>
</div>
</div>
</div>
</div>