Files
BountyBoard/web/templates/admin.html
T
etalon 458ba6a7ee feat: admin area with customers, users, settings, audit log, service status (phase 5)
- customer CRUD wizard backend: per-system credential shapes validated via
  connector construction, AES-256-GCM at rest, write-only over the API
- ticketing connectors (jira/azure_devops/youtrack/demo) with test-connection
- user management: roles, disable (revokes sessions), force password reset,
  delete; self-demotion/disable/delete guards
- admin-editable runtime settings ({_id:app}) incl. atomizer URL override
- audit log written on every privileged mutation + filterable list API
- service status panel: mongo/atomizer/work-performer health + latency with
  late-bound breaker, sync and jobs status providers
- tabbed admin UI (customers wizard dialog, users table, settings, status, audit)
- compose: mongo nofile ulimit 64000 (1024 default crashed WiredTiger)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:57:10 +02:00

152 lines
6.4 KiB
HTML

{{define "content"}}
<h1>Administration</h1>
<div class="error-box" id="error" role="alert"></div>
<nav class="tabs" role="tablist" aria-label="Admin sections">
<button class="btn" role="tab" data-tab="customers" aria-selected="true">Customers</button>
<button class="btn" role="tab" data-tab="users">Users</button>
<button class="btn" role="tab" data-tab="settings">Settings</button>
<button class="btn" role="tab" data-tab="status">Service status</button>
<button class="btn" role="tab" data-tab="audit">Audit log</button>
</nav>
<section id="tab-customers" class="card" role="tabpanel">
<div class="spread">
<h2>Customers</h2>
<button class="btn primary" id="customer-new">+ New customer</button>
</div>
<div id="customer-list" class="stack"></div>
<dialog id="customer-dialog">
<form id="customer-form" method="dialog" class="stack" style="min-width:480px">
<h2 id="customer-dialog-title">New customer</h2>
<div class="field">
<label for="c-name">Name</label>
<input type="text" id="c-name" required>
</div>
<div class="row">
<div class="field">
<label for="c-type">Ticketing system</label>
<select id="c-type">
<option value="jira">Jira Cloud</option>
<option value="azure_devops">Azure DevOps</option>
<option value="youtrack">YouTrack</option>
<option value="demo">Demo (offline)</option>
</select>
</div>
<div class="field">
<label for="c-project">Project key</label>
<input type="text" id="c-project" placeholder="ACME">
</div>
</div>
<div class="field" id="f-baseurl">
<label for="c-baseurl">Base URL</label>
<input type="text" id="c-baseurl" placeholder="https://acme.atlassian.net">
</div>
<fieldset id="creds-jira" class="creds">
<legend>Jira credentials</legend>
<div class="field"><label for="c-jira-email">Email</label><input type="text" id="c-jira-email" autocomplete="off"></div>
<div class="field"><label for="c-jira-token">API token</label><input type="password" id="c-jira-token" autocomplete="off"></div>
</fieldset>
<fieldset id="creds-azure_devops" class="creds" hidden>
<legend>Azure DevOps credentials</legend>
<div class="field"><label for="c-ado-org">Organization</label><input type="text" id="c-ado-org"></div>
<div class="field"><label for="c-ado-project">Project</label><input type="text" id="c-ado-project"></div>
<div class="field"><label for="c-ado-pat">Personal access token</label><input type="password" id="c-ado-pat" autocomplete="off"></div>
</fieldset>
<fieldset id="creds-youtrack" class="creds" hidden>
<legend>YouTrack credentials</legend>
<div class="field"><label for="c-yt-token">Permanent token</label><input type="password" id="c-yt-token" autocomplete="off"></div>
</fieldset>
<fieldset id="creds-demo" class="creds" hidden>
<legend>Demo</legend>
<p class="hint">No credentials required — tickets are fabricated locally.</p>
</fieldset>
<div class="row">
<div class="field">
<label for="c-poll">Poll interval (seconds)</label>
<input type="number" id="c-poll" min="10" value="60">
</div>
<div class="field">
<label for="c-budget">Default budget</label>
<input type="number" id="c-budget" min="0" step="0.01" value="1000">
</div>
</div>
<div class="field">
<label for="c-consultants">Assigned consultants</label>
<select id="c-consultants" multiple size="4"></select>
<p class="hint">Hold Ctrl/Cmd to select multiple. At least one is needed for syncing.</p>
</div>
<div class="spread">
<span id="c-test-result" class="hint" aria-live="polite"></span>
<span>
<button class="btn" type="button" id="c-test">Test connection</button>
<button class="btn" type="button" id="c-cancel">Cancel</button>
<button class="btn primary" type="submit" id="c-save">Save</button>
</span>
</div>
</form>
</dialog>
</section>
<section id="tab-users" class="card" role="tabpanel" hidden>
<div class="spread">
<h2>Users</h2>
<input type="text" id="user-search" placeholder="Search name or email…" aria-label="Search users" style="max-width:280px">
</div>
<table class="list" id="user-table">
<thead><tr><th>Name</th><th>Email</th><th>Admin</th><th>Consultant</th><th>Developer</th><th>Status</th><th></th></tr></thead>
<tbody></tbody>
</table>
</section>
<section id="tab-settings" class="card" role="tabpanel" hidden>
<h2>Application settings</h2>
<form id="settings-form" class="stack" style="max-width:480px">
<div class="field">
<label for="s-branding">Branding name</label>
<input type="text" id="s-branding">
</div>
<div class="field">
<label for="s-atomizer">Atomizer base URL override</label>
<input type="text" id="s-atomizer" placeholder="leave empty to use the environment default">
</div>
<div class="field">
<label for="s-poll">Default poll interval (seconds)</label>
<input type="number" id="s-poll" min="10">
</div>
<div class="field">
<label for="s-budget">Default customer budget</label>
<input type="number" id="s-budget" min="0" step="0.01">
</div>
<div class="field">
<label><input type="checkbox" id="s-hideclaims"> Hide competing claim requests from developers</label>
</div>
<button class="btn primary" type="submit">Save settings</button>
</form>
</section>
<section id="tab-status" class="card" role="tabpanel" hidden>
<div class="spread">
<h2>Service status</h2>
<button class="btn small" id="status-refresh">Refresh</button>
</div>
<div id="status-body" class="stack"></div>
</section>
<section id="tab-audit" class="card" role="tabpanel" hidden>
<div class="spread">
<h2>Audit log</h2>
<span>
<input type="text" id="audit-entity" placeholder="Filter by entity id" aria-label="Filter by entity id">
<button class="btn small" id="audit-apply">Apply</button>
</span>
</div>
<table class="list" id="audit-table">
<thead><tr><th>When</th><th>Actor</th><th>Action</th><th>Entity</th><th>Details</th></tr></thead>
<tbody></tbody>
</table>
<button class="btn mt" id="audit-more">Load more</button>
</section>
{{end}}