diff options
| author | Chia <Chia@93.nz> | 2026-08-06 09:29:41 +1200 |
|---|---|---|
| committer | Chia <Chia@93.nz> | 2026-08-06 09:32:46 +1200 |
| commit | 41e322c53d7b4b796eb377d0df9c29ecd10ba431 (patch) | |
| tree | c730526150e55e39b822d5197e4a20318ecaa449 /internal/adminui/assets/index.html | |
| parent | eadb2ffe85c43cf6fc741c9823cd28eedb4a844c (diff) | |
feat: complete commercial control plane, billing, auth, and model catalog
- add PostgreSQL control-plane persistence with Redis-degraded hot reload
- implement prepaid balance, usage ledger, Stripe top-up and reconciliation
- add registration, email verification, password reset, invitations and RBAC
- support TOTP, Passkey MFA, device sessions, quotas and rate limits
- add tenant billing profiles, audit logs and operational readiness checks
- build authenticated admin console, Quickstart, Playground and usage analytics
- add public model catalog with pricing, filtering and cost estimation
- support OpenAI Responses providers and provider health failover
- validate real upstream usage reporting and balance settlement
Diffstat (limited to 'internal/adminui/assets/index.html')
| -rw-r--r-- | internal/adminui/assets/index.html | 139 |
1 files changed, 132 insertions, 7 deletions
diff --git a/internal/adminui/assets/index.html b/internal/adminui/assets/index.html index 04b4e87..8f31e1c 100644 --- a/internal/adminui/assets/index.html +++ b/internal/adminui/assets/index.html @@ -75,7 +75,9 @@ </header> <main class="shell"> <nav class="tabs" aria-label="Admin sections"> - <button class="tab active" data-section="overview">Overview</button> + <button class="tab active" data-section="quickstart">Quickstart</button> + <button class="tab" data-section="overview">Overview</button> + <button class="tab" data-section="catalog">Model catalog</button> <button class="tab" data-section="usage" data-permission="usage.read">Usage</button> <button class="tab" data-section="billing" data-permission="billing.read" id="billing-tab">Billing</button> <button class="tab" data-section="projects" data-permission="projects.read">Projects</button> @@ -86,10 +88,77 @@ <button class="tab" data-section="audit" data-permission="audit.read">Audit</button> <button class="tab" data-section="tenants" data-permission="tenants.read">Tenants</button> <button class="tab" data-section="providers" data-permission="platform.read">Providers</button> - <button class="tab" data-section="models" data-permission="platform.read">Models & routes</button> + <button class="tab" data-section="models" data-permission="platform.read">Routing</button> </nav> - <section id="overview" class="section active"> + <section id="quickstart" class="section active"> + <div class="section-heading"><div><span class="eyebrow">DEVELOPER WORKSPACE</span><h1>Start building</h1></div><div class="form-actions"><span class="currency-label" id="quickstart-balance"></span><button class="button secondary" type="button" id="quickstart-topup" data-goto="billing" data-permission="billing.topup">Add funds</button></div></div> + <div class="onboarding panel" id="onboarding"></div> + <div class="quick-access-grid"> + <form class="panel starter-key-panel" id="starter-key-form" data-permission="keys.write"> + <div class="section-heading"><div><span class="eyebrow">API ACCESS</span><h2>Create a starter key</h2></div><span class="badge" id="starter-model-label"></span></div> + <div class="starter-key-fields"><label>Project<select id="starter-project" required></select></label><label>Key name<input id="starter-key-name" maxlength="120" value="Quickstart key" required></label><button class="button primary" id="starter-key-submit" type="submit">Create & use key</button></div> + <p class="muted">The key is limited to the selected Quickstart model and placed in the Playground for this page only.</p> + </form> + <section class="panel endpoint-panel"> + <div class="section-heading"><div><span class="eyebrow">CONNECTION</span><h2>API endpoints</h2></div><button class="button subtle" type="button" id="copy-endpoint-env">Copy environment</button></div> + <div class="endpoint-list" id="endpoint-list"></div> + <pre class="endpoint-env"><code id="endpoint-env"></code></pre> + </section> + </div> + <div class="quickstart-grid"> + <div class="panel quickstart-code"> + <div class="section-heading"><div><span class="eyebrow">FIRST REQUEST</span><h2>Copy a working example</h2></div><button class="button subtle" type="button" id="copy-quickstart">Copy</button></div> + <div class="form-grid compact-form"><label>Model<select id="quickstart-model"></select></label><label>Protocol<select id="quickstart-protocol"></select></label><label>Provider<select id="quickstart-provider"></select></label><label>Language<select id="quickstart-language"><option value="curl">cURL</option><option value="python">Python</option><option value="node">Node.js</option></select></label></div> + <pre id="quickstart-code-block"><code></code></pre> + <p class="muted">Use an active API key in <code>AIGW_API_KEY</code>. The example never stores your key in the browser.</p> + </div> + <div class="panel quickstart-next"><span class="eyebrow">WORKSPACE SIGNALS</span><h2>What to do next</h2><div id="quickstart-signals" class="signal-list"></div></div> + </div> + <form class="panel playground" id="playground-form"> + <div class="section-heading"><div><span class="eyebrow">LIVE API</span><h2>API playground</h2></div><span class="currency-label" id="playground-endpoint"></span></div> + <div class="playground-fields"> + <label class="playground-key">API key<input id="playground-key" type="password" autocomplete="off" spellcheck="false" required placeholder="sk-aigw-..."></label> + <label>Model<select id="playground-model" required></select></label> + <label>Protocol<select id="playground-protocol" required></select></label> + <label>Provider<select id="playground-provider"></select></label> + <label class="playground-prompt">Prompt<textarea id="playground-prompt" required rows="4">Reply with exactly: AIGW ready</textarea></label> + <div class="playground-actions"><label>Max output<input id="playground-max-output" type="number" min="1" max="4096" value="128" required></label><button class="button primary" id="playground-send" type="submit">Send request</button><button class="button secondary hidden" id="playground-stop" type="button">Stop</button></div> + </div> + <div class="playground-result hidden" id="playground-result" aria-live="polite"> + <div class="playground-meta"><strong class="badge" id="playground-status"></strong><span id="playground-request-id"></span><span id="playground-duration"></span><span id="playground-tokens"></span></div> + <pre><code id="playground-response"></code></pre> + <div class="playground-diagnostic hidden" id="playground-diagnostic"><div><strong id="playground-diagnostic-title"></strong><p id="playground-diagnostic-message"></p></div><button class="button secondary hidden" id="playground-diagnostic-action" type="button"></button></div> + </div> + </form> + <div class="section-heading ledger-heading"><div><span class="eyebrow">DEFAULTS & ALERTS</span><h2>Workspace preferences</h2></div></div> + <div class="preferences-grid"> + <form class="panel form-grid compact-form" id="developer-preferences-form" data-permission="preferences.read"> + <h2>API defaults</h2> + <label>Default model<select id="preference-default-model" name="default_model"></select></label> + <label>Fallback model<select id="preference-fallback-model" name="fallback_model"></select></label> + <p class="muted form-note">These defaults drive workspace examples. Your API request can still select any model available to this tenant.</p> + <button class="button primary" type="submit" data-permission="developer.preferences.write">Save API defaults</button> + </form> + <form class="panel form-grid compact-form" id="billing-preferences-form" data-permission="billing.read"> + <h2>Balance alert</h2> + <label class="toggle-row"><input id="low-balance-enabled" name="low_balance_enabled" type="checkbox"><span>Email billing members when available balance is low</span></label> + <label>Alert below<input id="low-balance-threshold" name="low_balance_threshold" inputmode="decimal" required></label> + <p class="muted form-note" id="balance-alert-status"></p> + <button class="button primary" type="submit" data-permission="billing.preferences.write">Save balance alert</button> + </form> + </div> + <div class="section-heading ledger-heading"><div><span class="eyebrow">RECENT ACTIVITY</span><h2>Latest requests</h2></div><button class="button subtle" type="button" data-goto="usage">View all usage</button></div> + <div class="panel table-wrap"><table><thead><tr><th>Time</th><th>Model</th><th>Status</th><th>Tokens</th><th>Charged</th><th>Latency</th></tr></thead><tbody id="quickstart-usage-body"></tbody></table></div> + </section> + + <section id="catalog" class="section"> + <div class="section-heading"><div><span class="eyebrow">DISCOVER</span><h1>Model catalog</h1></div><span class="currency-label" id="catalog-count"></span></div> + <div class="panel catalog-toolbar"><label>Search models<input id="catalog-search" type="search" placeholder="Search by model, developer, capability"></label><label>Protocol<select id="catalog-protocol"><option value="">All protocols</option><option value="chat_completions">OpenAI Chat Completions</option><option value="responses">OpenAI Responses</option><option value="messages">Anthropic Messages</option></select></label><label>Input<select id="catalog-input"><option value="">Any input</option><option value="text">Text</option><option value="image">Image</option><option value="audio">Audio</option><option value="video">Video</option></select></label><label>Developer<select id="catalog-owner"><option value="">All developers</option></select></label><label>Sort<select id="catalog-sort"><option value="newest">Newest</option><option value="name">Name</option><option value="input_price">Lowest input price</option><option value="output_price">Lowest output price</option><option value="context">Largest context</option></select></label></div> + <div id="catalog-grid" class="catalog-grid"></div> + </section> + + <section id="overview" class="section"> <div class="section-heading"><div><span class="eyebrow">OPERATIONS</span><h1>Account overview</h1></div><button class="button secondary" id="reload" data-permission="platform.write">Reload snapshot</button></div> <div class="metric-grid" id="metrics"></div> <div class="section-heading ledger-heading"><div><span class="eyebrow">CURRENT PERIOD</span><h2>Project usage</h2></div></div> @@ -98,9 +167,29 @@ <section id="usage" class="section"> <div class="section-heading"><div><span class="eyebrow">METERING</span><h1>Usage ledger</h1></div></div> + <form class="panel usage-filters" id="usage-filter-form"> + <label>From<input id="usage-from" name="from" type="date"></label> + <label>To<input id="usage-to" name="to" type="date"></label> + <label>Project<select id="usage-project" name="project_id"><option value="">All projects</option></select></label> + <label>API key<select id="usage-key" name="key_id"><option value="">All API keys</option></select></label> + <label>Model<select id="usage-model" name="model"><option value="">All models</option></select></label> + <label>Provider<select id="usage-provider" name="provider"><option value="">All providers</option></select></label> + <label>Protocol<select id="usage-protocol" name="protocol"><option value="">All protocols</option><option value="openai">OpenAI Chat Completions</option><option value="openai_responses">OpenAI Responses</option><option value="anthropic">Anthropic Messages</option></select></label> + <label>Transport<select id="usage-stream" name="stream"><option value="">Streaming and non-streaming</option><option value="true">Streaming</option><option value="false">Non-streaming</option></select></label> + <label>Status<select id="usage-status" name="status"><option value="">All statuses</option><option value="success">Successful</option><option value="error">Errors</option></select></label> + <label>Error type<input id="usage-error-type" name="error_type" placeholder="provider_error"></label> + <label class="usage-request-filter">Request ID<input id="usage-request-id" name="request_id" placeholder="req_..."></label> + <div class="form-actions"><button class="button primary" type="submit">Apply filters</button><button class="button subtle" id="usage-filter-reset" type="button">Reset</button></div> + </form> + <div class="metric-grid usage-metrics" id="usage-metrics"></div> + <div class="panel usage-chart" id="usage-chart"></div> + <div class="analytics-grid"> + <div class="panel table-wrap analytics-panel"><div class="section-heading ledger-heading"><div><span class="eyebrow">COST RANKING</span><h2>Model cost</h2></div><span class="currency-label">Current range vs previous</span></div><table class="analytics-table"><thead><tr><th>Model</th><th>Requests</th><th>Success</th><th>Tokens</th><th>Charged</th><th>Change</th><th>P95</th><th>Missing usage</th></tr></thead><tbody id="usage-model-analytics-body"></tbody></table></div> + <div class="panel table-wrap analytics-panel"><div class="section-heading ledger-heading"><div><span class="eyebrow">ROUTE HEALTH</span><h2>Provider performance</h2></div><span class="currency-label">Cache hit and latency</span></div><table class="analytics-table"><thead><tr><th>Provider</th><th>Requests</th><th>Success</th><th>Models</th><th>Cache hit</th><th>Charged</th><th>Change</th><th>P95</th></tr></thead><tbody id="usage-provider-analytics-body"></tbody></table></div> + </div> <div class="panel table-wrap"><table><thead><tr><th>Period</th><th>Project</th><th>Requests</th><th>Success</th><th>Input</th><th>Output</th><th>Cost</th></tr></thead><tbody id="usage-summary-body"></tbody></table></div> <div class="section-heading ledger-heading"><div><span class="eyebrow">REQUESTS</span><h2>Recent events</h2></div></div> - <div class="panel table-wrap"><table><thead><tr><th>Time</th><th>Request</th><th>Model</th><th>Status</th><th>Tokens</th><th>Cost</th><th>Latency</th></tr></thead><tbody id="usage-events-body"></tbody></table></div> + <div class="panel table-wrap"><table><thead><tr><th>Time</th><th>Request</th><th>Project / key</th><th>Model / provider</th><th>Status</th><th>Tokens</th><th>Charged</th><th>Latency</th><th></th></tr></thead><tbody id="usage-events-body"></tbody></table></div> </section> <section id="tenants" class="section"> @@ -117,14 +206,24 @@ <section id="keys" class="section"> <div class="section-heading"><div><span class="eyebrow">ACCESS</span><h1>API keys</h1></div></div> - <form class="panel form-grid" id="key-form" data-permission="keys.write"><label>Tenant<select name="tenant_id" id="key-tenant" required></select></label><label>Project<select name="project_id" id="key-project" required></select></label><label>Name<input name="name" required placeholder="CLI production key"></label><label>Scopes<input name="scopes" value="inference" placeholder="inference"></label><button class="button primary" type="submit">Create key</button></form> + <form class="panel form-grid key-form" id="key-form" data-permission="keys.write"> + <label>Tenant<select name="tenant_id" id="key-tenant" required></select></label> + <label>Project<select name="project_id" id="key-project" required></select></label> + <label>Name<input name="name" required maxlength="120" placeholder="CLI production key"></label> + <label>Scopes<input name="scopes" value="inference" placeholder="inference"></label> + <label>Tags<input name="tags" placeholder="production, backend"></label> + <label>Monthly spend cap<input name="monthly_spend" inputmode="decimal" value="0" placeholder="0 = unlimited"></label> + <label>Expires at<input name="expires_at" type="datetime-local"></label> + <label class="key-model-picker">Allowed models<select name="allowed_models" id="key-models" multiple size="5" aria-describedby="key-model-help"></select><small id="key-model-help">No selection allows every model visible to this workspace.</small></label> + <button class="button primary" type="submit">Create key</button> + </form> <div class="panel warning"><strong>Key visibility</strong><span>The secret is shown only once after creation.</span></div> - <div class="panel table-wrap"><table><thead><tr><th>Name</th><th>Prefix</th><th>Project</th><th>Scopes</th><th>Status</th><th></th></tr></thead><tbody id="keys-body"></tbody></table></div> + <div class="panel table-wrap"><table class="keys-table"><thead><tr><th>Name</th><th>Prefix</th><th>Project</th><th>Restrictions</th><th>Month usage</th><th>Monthly cap</th><th>Activity</th><th></th></tr></thead><tbody id="keys-body"></tbody></table></div> </section> <section id="providers" class="section"> <div class="section-heading"><div><span class="eyebrow">UPSTREAMS</span><h1>Providers</h1></div></div> - <form class="panel form-grid" id="provider-form" data-permission="platform.write"><input class="visually-hidden" name="username" autocomplete="username" value="aigw-provider" aria-hidden="true" tabindex="-1"><label>Name<input name="name" autocomplete="off" required placeholder="openai-primary"></label><label>Protocol<select name="protocol"><option value="openai">OpenAI</option><option value="anthropic">Anthropic</option></select></label><label>Base URL<input name="base_url" type="url" autocomplete="off" required placeholder="https://api.example.com/v1"></label><label>API key<input name="api_key" type="password" required autocomplete="new-password" placeholder="Stored encrypted"></label><button class="button primary" type="submit">Add provider</button></form> + <form class="panel form-grid" id="provider-form" data-permission="platform.write"><input class="visually-hidden" name="username" autocomplete="username" value="aigw-provider" aria-hidden="true" tabindex="-1"><label>Name<input name="name" autocomplete="off" required placeholder="OpenAI primary"></label><label>Public slug<input name="slug" autocomplete="off" required pattern="[a-z0-9][a-z0-9-]{1,62}[a-z0-9]" maxlength="64" placeholder="openai-primary"></label><label>Protocol<select name="protocol"><option value="openai">OpenAI</option><option value="anthropic">Anthropic</option></select></label><label>Wire API<select name="wire_api"><option value="chat_completions">Chat Completions</option><option value="responses">Responses</option></select></label><label>Base URL<input name="base_url" type="url" autocomplete="off" required placeholder="https://api.example.com/v1"></label><label>API key<input name="api_key" type="password" required autocomplete="new-password" placeholder="Stored encrypted"></label><button class="button primary" type="submit">Add provider</button></form> <div class="panel table-wrap"><table><thead><tr><th>Name</th><th>Protocol</th><th>Base URL</th><th>Routes</th><th>Status</th><th></th></tr></thead><tbody id="providers-body"></tbody></table></div> </section> @@ -140,6 +239,30 @@ <form class="panel form-grid compact-form" id="topup-form" data-permission="billing.topup"><label>Tenant<select name="tenant_id" id="topup-tenant" required></select></label><label>Amount<input name="amount" inputmode="decimal" min="0" required placeholder="25.00"></label><button class="button primary" type="submit">Open Stripe Checkout</button></form> <form class="panel form-grid compact-form" id="adjustment-form" data-permission="billing.adjust"><label>Tenant<select name="tenant_id" id="adjustment-tenant" required></select></label><label>Signed amount<input name="amount" inputmode="decimal" required placeholder="10.00 or -5.00"></label><label>Reference<input name="description" maxlength="240" placeholder="Support credit"></label><button class="button secondary" type="submit">Post adjustment</button></form> </div> + <div class="panel billing-profile-panel" id="billing-profile-panel" data-permission="billing.read"> + <div class="section-heading"><div><span class="eyebrow">INVOICE DETAILS</span><h2>Billing profile</h2></div><span class="badge" id="billing-profile-status">Not configured</span></div> + <form class="form-grid" id="billing-profile-form"> + <label>Legal or billing name<input name="legal_name" maxlength="150" autocomplete="organization" required></label> + <label>Billing email<input name="billing_email" type="email" maxlength="254" autocomplete="email" required></label> + <label class="billing-address-wide">Address line 1<input name="address_line1" maxlength="200" autocomplete="address-line1" required></label> + <label class="billing-address-wide">Address line 2<input name="address_line2" maxlength="200" autocomplete="address-line2"></label> + <label>City<input name="city" maxlength="100" autocomplete="address-level2" required></label> + <label>State or region<input name="region" maxlength="100" autocomplete="address-level1"></label> + <label>Postal code<input name="postal_code" maxlength="32" autocomplete="postal-code" required></label> + <label>Country code<input name="country" minlength="2" maxlength="2" pattern="[A-Za-z]{2}" autocomplete="country" autocapitalize="characters" placeholder="NZ" required></label> + <div class="form-actions billing-profile-actions"><small class="error-label" id="billing-profile-error"></small><button class="button primary" type="submit" data-permission="billing.topup">Save invoice details</button></div> + </form> + </div> + <div class="panel auto-topup-panel" id="auto-topup-panel" data-permission="billing.read"> + <div class="section-heading"><div><span class="eyebrow">BALANCE PROTECTION</span><h2>Automatic top-up</h2></div><span class="badge" id="auto-topup-status">Not configured</span></div> + <form class="form-grid" id="auto-topup-form"> + <label class="toggle-row"><input id="auto-topup-enabled" name="enabled" type="checkbox"><span>Automatically add funds when available balance reaches the threshold</span></label> + <label>Balance threshold<input id="auto-topup-threshold" name="threshold" inputmode="decimal" required></label> + <label>Top-up amount<input id="auto-topup-amount" name="amount" inputmode="decimal" required></label> + <div><span class="muted">Payment method</span><strong id="auto-topup-payment-method">Not saved</strong><small class="error-label" id="auto-topup-error"></small></div> + <div class="form-actions auto-topup-actions"><button class="button secondary" id="auto-topup-payment-setup" type="button">Save payment method</button><button class="button primary" type="submit">Save automatic top-up</button></div> + </form> + </div> <div class="panel table-wrap"><table><thead><tr><th>Tenant</th><th>Balance</th><th>Reserved</th><th>Available</th><th>Updated</th></tr></thead><tbody id="billing-accounts-body"></tbody></table></div> <div class="section-heading ledger-heading"><div><span class="eyebrow">AUDIT</span><h2>Recent ledger entries</h2></div></div> <div class="panel table-wrap"><table><thead><tr><th>Time</th><th>Tenant</th><th>Kind</th><th>Amount</th><th>Balance after</th><th>Reference</th></tr></thead><tbody id="billing-ledger-body"></tbody></table></div> @@ -180,6 +303,8 @@ </main> </div> <div id="toast" class="toast" role="status"></div> + <dialog id="model-dialog"><div class="dialog-content model-dialog-content"><div class="section-heading"><div><span class="eyebrow">MODEL DETAIL</span><h2 id="model-dialog-title">Model</h2></div><button class="icon-button" id="close-model-dialog" aria-label="Close">×</button></div><div class="model-dialog-id"><code id="model-dialog-id"></code><button class="text-button" id="copy-model-id" type="button">Copy ID</button></div><div class="model-detail-grid" id="model-detail-grid"></div><section class="model-provider-health" id="model-provider-health"><div class="section-heading"><div><h3>Provider health</h3><small>Measured from requests handled by this gateway instance.</small></div></div><div class="table-wrap"><table class="provider-health-table"><thead><tr><th>Provider</th><th>State</th><th>Recent availability</th><th>Header latency</th><th>Samples</th></tr></thead><tbody id="model-provider-health-body"></tbody></table></div></section><form class="model-estimator" id="model-estimate-form"><h3>Cost estimate</h3><div class="form-grid compact-form"><label>Input tokens<input id="estimate-input" type="number" min="0" step="1" value="1000" required></label><label>Output tokens<input id="estimate-output" type="number" min="0" step="1" value="500" required></label><label>Cache read tokens<input id="estimate-cache-read" type="number" min="0" step="1" value="0" required></label><label>Cache write tokens<input id="estimate-cache-write" type="number" min="0" step="1" value="0" required></label><output id="model-estimate" class="model-estimate" aria-live="polite"></output></div></form><div class="form-actions model-dialog-actions"><button class="button secondary" id="model-dialog-playground" type="button">Use in Playground</button><button class="button subtle" id="model-dialog-close" type="button">Close</button></div></div></dialog> + <dialog id="usage-dialog"><div class="dialog-content request-dialog-content"><div class="section-heading"><div><span class="eyebrow">API REQUEST</span><h2 id="usage-dialog-title">Request details</h2></div><button class="icon-button" id="close-usage-dialog" aria-label="Close">×</button></div><div class="model-dialog-id"><code id="usage-dialog-id"></code><button class="text-button" id="copy-request-id" type="button">Copy request ID</button></div><div class="request-detail-grid" id="usage-detail-grid"></div><pre class="request-diagnostic"><code id="usage-diagnostic"></code></pre><div class="form-actions model-dialog-actions"><button class="button secondary" id="copy-request-diagnostic" type="button">Copy diagnostic</button><button class="button subtle" id="usage-dialog-close" type="button">Close</button></div></div></dialog> <dialog id="secret-dialog"><div class="dialog-content"><div class="section-heading"><div><span class="eyebrow">ONE-TIME SECRET</span><h2 id="secret-title">Credential created</h2></div><button class="icon-button" id="close-dialog" aria-label="Close">×</button></div><p>Copy this credential now. It will not be shown again.</p><code id="created-secret"></code><button class="button primary" id="copy-secret">Copy credential</button></div></dialog> <script src="./app.js" defer></script> </body> |
