Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.
People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.
Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.
Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.
Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
436 lines
27 KiB
HTML
436 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<!-- Deployment identity: the configured deployment-name (default
|
|
"Member Console") renders identically here and on the member
|
|
mastheads, so the console tells one story about who it is. The
|
|
default is itself brand-neutral, which keeps a stranger's first
|
|
screenshot of this general-purpose OSS project deployment-neutral
|
|
too. -->
|
|
<title>Operator overview - {{ deploymentName }}</title>
|
|
<meta name="description" content="Operator console for membership, entitlement and billing administration">
|
|
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="ia-position" content="{{ .IAPosition }}">
|
|
|
|
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
|
|
<link href="/static/bootstrap.css" rel="stylesheet">
|
|
<script defer src="/static/bootstrap.bundle.js"></script>
|
|
<script defer src="/static/htmx.min.js"></script>
|
|
<script defer src="/static/error-handler.js"></script>
|
|
<script defer src="/static/success-toast.js"></script>
|
|
<script defer src="/static/grant-toggle.js"></script>
|
|
<script defer src="/static/grant-valid-until-tz.js"></script>
|
|
<script defer src="/static/confirm-action-modal.js"></script>
|
|
<script defer src="/static/sortable.min.js"></script>
|
|
<script defer src="/static/sortable-reorder.js"></script>
|
|
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
|
<link rel="manifest" href="/static/site.webmanifest">
|
|
|
|
<link href="/static/app.css" rel="stylesheet">
|
|
</head>
|
|
|
|
<body class="d-flex flex-column vh-100"
|
|
hx-boost="true"
|
|
hx-headers='{"X-CSRF-Token": "{{ .CSRFToken }}"}'>
|
|
<!-- Top navbar — cross-app navigation; visible on all breakpoints so the
|
|
sidebar can be repurposed for operator IA. -->
|
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="/">{{ deploymentName }}</a>
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="navbarNav">
|
|
<ul class="navbar-nav ms-auto">
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/">Dashboard</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/products">Products</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link active" href="/operator" aria-current="page">Operator</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="{{ .KeycloakAccountURL }}" target="_blank" hx-boost="false" title="Manage your account and sign-in. Opens in a new tab.">Identity and <span class="text-nowrap">Access<svg class="external-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></span></a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link text-danger" href="/logout" hx-boost="false">Logout</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="d-flex flex-column flex-lg-row flex-grow-1">
|
|
<!-- Operator IA sidebar — one flat list of eight top-level task
|
|
entries, nothing else (maintainer 2026-08-23: no children, no
|
|
groups; the sidebar names the sections, the sections navigate
|
|
themselves; People joined as the eighth entry with the
|
|
directory, ux-operator-scale). Order: Overview, People,
|
|
Organizations, Grants, Billing, Products, Domains,
|
|
Integrations. Second-level surfaces are reached in-page:
|
|
person detail from the People directory's rows, Org Types from
|
|
the Organizations page header, the four billing views from the
|
|
Billing wrapper's tab row, Entitlement Sets and Plan Ladders
|
|
from the Products page header, and provider surfaces from the
|
|
Integrations home's provider cards. Their IA-position
|
|
declarations still carry the Catalog conceptual group
|
|
(docs/operator-ia.md). A second-level page marks its section's
|
|
entry active. Every entry points at a real
|
|
/operator/<capability> route under hx-boost. -->
|
|
<aside class="app-sidebar operator-ia-sidebar bg-light d-none d-lg-flex flex-column flex-shrink-0 h-lg-100 border-end">
|
|
<ul class="nav flex-column p-3">
|
|
<li class="nav-item"><a class="nav-link {{ if eq .ActiveCapability "" }}active{{ end }}" href="/operator">Overview</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ if eq .ActiveCapability "persons" }}active{{ end }}" href="/operator/persons">People</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ if or (eq .ActiveCapability "organizations") (eq .ActiveCapability "org-types") }}active{{ end }}" href="/operator/organizations">Organizations</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ if eq .ActiveCapability "grants" }}active{{ end }}" href="/operator/grants">Grants</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ if or (eq .ActiveCapability "billing-accounts") (eq .ActiveCapability "billing-subscriptions") (eq .ActiveCapability "billing-invoices") (eq .ActiveCapability "billing-payments") }}active{{ end }}" href="/operator/billing/accounts">Billing</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ if or (eq .ActiveCapability "products") (eq .ActiveCapability "entitlement-sets") (eq .ActiveCapability "plan-ladders") }}active{{ end }}" href="/operator/products">Products</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ if eq .ActiveCapability "domains" }}active{{ end }}" href="/operator/domains">Domains</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ if eq .ActiveCapability "integrations" }}active{{ end }}" href="/operator/integrations">Integrations</a></li>
|
|
</ul>
|
|
</aside>
|
|
|
|
<!-- Main content — single swap target for hx-boost navigations. The
|
|
toast container and confirm modal are siblings (outside this region)
|
|
so they survive boosts per docs/design-system.md §3. The inner
|
|
container-xxl caps content width at ~1320px so wide screens don't
|
|
stretch text to the viewport edge. -->
|
|
<main id="operator-main" class="flex-fill p-4">
|
|
<div class="container-xxl px-0">
|
|
<!-- #operator-body is the HTMX swap target for every mutation
|
|
form in the operator panel. Forms hx-post to /partials/...
|
|
endpoints that respond with the bare page partial; HTMX
|
|
swaps that fragment into #operator-body, preserving the
|
|
#operator-main + container-xxl wrappers. See
|
|
docs/operator-ux-conventions.md §2 (HTMX request shape). -->
|
|
<div id="operator-body">
|
|
{{ if .BodyTemplate }}
|
|
<!-- MPA page handler set a body partial; dispatch via the
|
|
renderBody template func (html/template's built-in template
|
|
action requires a string literal, not a dynamic name). -->
|
|
{{ renderBody .BodyTemplate .BodyData }}
|
|
{{ else }}
|
|
<!-- ============================================================
|
|
Curated landing surface — the operator's entry point.
|
|
Answers "what is this deployment doing right now?"
|
|
attention-first: search, then setup progress (while
|
|
required), then the counts an operator checks daily, then
|
|
system status, then the event timeline (maintainer
|
|
2026-08-23: numbers outrank the system cards).
|
|
Attention-shaped content (an unconfigured integration, a
|
|
stuck delivery queue) is never the last region on the page.
|
|
|
|
Heading contract: one H1 for the page, an H2 per region,
|
|
H3 for panels nested inside a region. No level is skipped.
|
|
(The older operator pages run H1→H6; that is not a pattern
|
|
to copy here.)
|
|
|
|
Per operator-composite-expansion D4, "Recent activity" is
|
|
one unified timeline (grants + transitions + invoices +
|
|
payments interleaved by timestamp) rather than two
|
|
domain-partitioned sections. Override the default 20-row
|
|
limit with ?limit=N (clamped to [1,100]). Per task 7.4,
|
|
each entry links to its own subject's operator surface
|
|
where one exists.
|
|
============================================================ -->
|
|
<header class="mb-4">
|
|
<h1 class="h2 mb-1">Operator overview</h1>
|
|
<p class="text-muted mb-0">
|
|
Membership, delivery, catalog and integration state for this deployment.
|
|
</p>
|
|
</header>
|
|
|
|
<!-- Region 1 — Lookup. Single typed input resolves to person
|
|
detail (email-exact or name-substring), org composite
|
|
(slug-exact), a disambiguation list (multi-person), or a
|
|
no-match notice. Uses HTMX (hx-post) so the request goes
|
|
through the same X-CSRF-Token header path every other
|
|
operator form uses (native form POST hit a brittle
|
|
Origin-check path in gorilla CSRF). Unique matches return
|
|
HX-Redirect; multi/no-match return the result partial
|
|
swapped into #lookup-result.
|
|
|
|
The heading is visually hidden: the input's own placeholder
|
|
and button already label the region on screen, and a visible
|
|
"Find" heading above a search box is redundant. Screen
|
|
readers still get the landmark. -->
|
|
<section class="mb-5" aria-labelledby="overview-lookup-heading">
|
|
<h2 class="visually-hidden" id="overview-lookup-heading">Find a person or organization</h2>
|
|
<form autocomplete="off" hx-post="/operator/lookup" hx-target="#lookup-result" hx-swap="innerHTML">
|
|
<div class="input-group input-group-lg">
|
|
<input autocomplete="off" type="search" name="term" class="form-control form-control-lg"
|
|
aria-labelledby="overview-lookup-heading"
|
|
placeholder="Find a person or organization (email, name, or org slug)"
|
|
value="{{ .LookupTerm }}" autofocus>
|
|
<button type="submit" class="btn btn-primary px-4">Look up</button>
|
|
</div>
|
|
</form>
|
|
<div id="lookup-result">
|
|
{{ template "operator_lookup_result.html" . }}
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Setup checklist region (ux-first-run). Renders ahead of the
|
|
metric tiles ONLY while a REQUIRED step is incomplete, and
|
|
stops once every required step is complete even if
|
|
conditional steps remain — RequiredIncomplete is the single
|
|
predicate that decides both this and /operator/setup's own
|
|
content, so the two can never disagree. -->
|
|
{{ if .Setup.RequiredIncomplete }}
|
|
{{ template "operator_setup.html" .Setup }}
|
|
{{ end }}
|
|
|
|
<!-- Region 2 — At a glance. One tile per capability; each tile
|
|
is a link into that capability's section, so this row doubles
|
|
as the primary navigation for operators who arrived here
|
|
without a specific destination in mind. A tile whose count
|
|
could not be loaded renders an em dash, never a zero. -->
|
|
<section class="mb-5" aria-labelledby="overview-glance-heading">
|
|
<h2 class="h6 text-uppercase text-body-secondary mb-3" id="overview-glance-heading">At a glance</h2>
|
|
{{ if .Overview.Stats }}
|
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-xl-3 g-3">
|
|
{{ range .Overview.Stats }}
|
|
<div class="col">
|
|
{{ if .Href }}
|
|
<a class="card h-100 text-decoration-none text-body" href="{{ .Href }}">
|
|
{{ else }}
|
|
<div class="card h-100">
|
|
{{ end }}
|
|
<div class="card-body">
|
|
<p class="text-uppercase text-body-secondary small fw-semibold mb-1">{{ .Label }}</p>
|
|
<p class="display-6 fw-semibold lh-1 mb-2">
|
|
{{ if .Available }}{{ .Value }}{{ else }}<span class="text-body-secondary" title="This count could not be loaded">—</span>{{ end }}
|
|
</p>
|
|
<p class="small text-body-secondary mb-0">
|
|
{{ if .Available }}{{ .Caption }}{{ else }}Count unavailable{{ end }}
|
|
</p>
|
|
</div>
|
|
{{ if .Href }}
|
|
</a>
|
|
{{ else }}
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
{{ else }}
|
|
<div class="card">
|
|
<div class="card-body text-center py-4">
|
|
<p class="text-body-secondary mb-0">Counts are unavailable.</p>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
</section>
|
|
|
|
<!-- Region 3 — System status. Registry-driven: every registered
|
|
integration is listed with its operational status, and the
|
|
outbox panel reports whether queued integration work is
|
|
draining. Renders after the metric tiles (maintainer
|
|
2026-08-23: the counts are what an operator checks first)
|
|
but ahead of the activity feed, so attention-shaped content
|
|
is still never the last region on the page. -->
|
|
<section class="mb-5" aria-labelledby="overview-system-heading">
|
|
<h2 class="h6 text-uppercase text-body-secondary mb-3" id="overview-system-heading">System</h2>
|
|
<div class="row row-cols-1 row-cols-md-2 g-3">
|
|
<div class="col">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h3 class="h6 mb-3">Integrations</h3>
|
|
{{ if .Overview.Providers }}
|
|
<ul class="list-unstyled mb-0">
|
|
{{ range .Overview.Providers }}
|
|
<li class="d-flex justify-content-between align-items-center gap-2 py-2 border-bottom">
|
|
<span class="text-truncate">
|
|
{{ if .SurfacePath }}<a href="{{ .SurfacePath }}">{{ .DisplayName }}</a>
|
|
{{ else }}{{ .DisplayName }}{{ end }}
|
|
<span class="d-block small text-body-secondary">{{ .Kind }}</span>
|
|
</span>
|
|
<span class="d-flex flex-column align-items-end gap-1 flex-shrink-0">
|
|
<span class="badge {{ if .Healthy }}text-bg-success{{ else }}text-bg-warning{{ end }}"
|
|
title="Registry status: an operator-managed record, not a live health check">{{ .Status }}</span>
|
|
{{ if not .Configured }}
|
|
<span class="badge text-bg-warning" {{ if .MissingKeysText }}title="Missing: {{ .MissingKeysText }}"{{ end }}>Not configured</span>
|
|
{{ end }}
|
|
</span>
|
|
</li>
|
|
{{ end }}
|
|
</ul>
|
|
{{ else }}
|
|
<p class="small text-body-secondary mb-0">No integrations registered.</p>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h3 class="h6 mb-3">Delivery queue</h3>
|
|
<p class="small text-body-secondary mb-3">
|
|
Tracks Stripe's queued outbound work only; other integrations dispatch directly and are not reflected here, so an empty or draining queue says nothing about their state.
|
|
</p>
|
|
{{ if .Overview.Queue.Available }}
|
|
<!-- Term before description, matching the
|
|
label-then-value rhythm of the stat tiles
|
|
below and keeping the list well-formed. -->
|
|
<dl class="row row-cols-3 g-0 mb-0 text-center">
|
|
<div class="col">
|
|
<dt class="small fw-normal text-body-secondary">Pending</dt>
|
|
<dd class="fs-4 fw-semibold mb-0">{{ .Overview.Queue.Pending }}</dd>
|
|
</div>
|
|
<div class="col">
|
|
<dt class="small fw-normal text-body-secondary">Retrying</dt>
|
|
<dd class="fs-4 fw-semibold mb-0">{{ .Overview.Queue.Retrying }}</dd>
|
|
</div>
|
|
<div class="col">
|
|
<dt class="small fw-normal text-body-secondary">Dead-letter</dt>
|
|
<dd class="fs-4 fw-semibold mb-0 {{ if .Overview.Queue.NeedsAttention }}text-danger{{ end }}">{{ .Overview.Queue.DeadLetter }}</dd>
|
|
</div>
|
|
</dl>
|
|
{{ if .Overview.Queue.NeedsAttention }}
|
|
<p class="small text-danger mb-0 mt-3">
|
|
Dead-lettered work has exhausted its retries and needs an operator.
|
|
</p>
|
|
{{ else }}
|
|
<p class="small text-body-secondary mb-0 mt-3">
|
|
Queued integration work is draining normally.
|
|
</p>
|
|
{{ end }}
|
|
{{ else }}
|
|
<p class="small text-body-secondary mb-0">Queue health is unavailable.</p>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Region 4 — Recent activity. Each entry's Organization column
|
|
always names and links the org it belongs to when one is
|
|
known; the Summary text additionally links to the entry's
|
|
own subject surface (task 7.4 / chrome-conventions D8: "feed
|
|
entries link to their subjects") — the org detail page for a
|
|
grant or transition, the billing view for an invoice or
|
|
payment. An entry with no addressable surface (.Href empty)
|
|
renders its summary as plain text, never a dead link. -->
|
|
<section aria-labelledby="overview-activity-heading">
|
|
<h2 class="h6 text-uppercase text-body-secondary mb-3" id="overview-activity-heading">Recent activity</h2>
|
|
<div class="card">
|
|
{{ if eq (len .Activity) 0 }}
|
|
<div class="card-body text-center py-5">
|
|
<p class="mb-1">No activity recorded yet.</p>
|
|
<p class="small text-body-secondary mb-0">
|
|
Grants, plan transitions, invoices and payments appear here as they happen.
|
|
</p>
|
|
</div>
|
|
{{ else }}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead>
|
|
<tr class="text-uppercase small text-body-secondary">
|
|
<th scope="col">When</th>
|
|
<th scope="col">Event</th>
|
|
<th scope="col">Organization</th>
|
|
<th scope="col">Actor</th>
|
|
<th scope="col">Summary</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .Activity }}
|
|
<tr>
|
|
<td><small class="text-nowrap text-body-secondary">{{ .Timestamp }}</small></td>
|
|
<td>
|
|
{{ if eq .EventType "grant_issued" }}<span class="badge text-bg-primary">grant</span>
|
|
{{ else if eq .EventType "transition" }}<span class="badge text-bg-info">transition</span>
|
|
{{ else if eq .EventType "invoice_created" }}<span class="badge text-bg-secondary">invoice</span>
|
|
{{ else if eq .EventType "payment_received" }}<span class="badge text-bg-success">payment</span>
|
|
{{ else }}<span class="badge text-bg-light">{{ .EventType }}</span>{{ end }}
|
|
</td>
|
|
<td>
|
|
{{ if .OrgID }}<a href="/operator/organizations/{{ .OrgID }}">{{ if .OrgName }}{{ .OrgName }}{{ else }}<code>{{ .OrgID }}</code>{{ end }}</a>{{ end }}
|
|
</td>
|
|
<td>
|
|
{{ if .PersonID }}<a href="/operator/persons/{{ .PersonID }}">{{ if .PersonName }}{{ .PersonName }}{{ else }}<code>{{ .PersonID }}</code>{{ end }}</a>
|
|
{{ else }}<small class="text-body-secondary">—</small>{{ end }}
|
|
</td>
|
|
<td><small>{{ if .Href }}<a href="{{ .Href }}">{{ .Summary }}</a>{{ else }}{{ .Summary }}{{ end }}</small></td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</section>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<!-- Toast Container — sibling of <main> so toasts survive hx-boost
|
|
navigations and partial swaps (see docs/design-system.md §3). -->
|
|
<div class="toast-container">
|
|
<div id="errorToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive"
|
|
aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body" id="errorToastBody">
|
|
An error occurred. Please try again.
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
<div id="successToast" class="toast align-items-center text-bg-success border-0" role="status"
|
|
aria-live="polite" aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body" id="successToastBody">
|
|
Done.
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shared confirmation modal (driven by data-action-* attrs on triggers).
|
|
Sibling of <main> so the modal markup survives boosted navigations. -->
|
|
<div class="modal fade" id="confirmActionModal" tabindex="-1" aria-labelledby="confirmActionModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title h5" id="confirmActionModalLabel">Confirm</h2>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form autocomplete="off" id="confirmActionForm"
|
|
hx-swap="innerHTML"
|
|
hx-disabled-elt="find button[type=submit]"
|
|
hx-indicator="find .htmx-indicator">
|
|
<div class="modal-body">
|
|
<p id="confirmActionBody" class="mb-0"></p>
|
|
<div id="confirmActionHiddenFields"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-danger" id="confirmActionSubmit">
|
|
<span class="htmx-indicator spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
|
<span id="confirmActionLabel">Confirm</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
|
|
</html>
|