feat: MVP phase 1 complete

This commit is contained in:
Blake Ridgway
2026-03-25 02:41:17 -05:00
parent 81ae5c6c7b
commit bfa03e6fbf
32 changed files with 3503 additions and 39 deletions

View File

@@ -0,0 +1,76 @@
{{define "content"}}
{{with .Data}}
<div class="page-header">
<p class="page-header__label"><a href="/admin" class="link">admin</a> / clients</p>
<h1 class="page-header__title">{{.Client.DisplayName}}</h1>
<p class="text-dim td-mono">@{{.Client.Username}}</p>
</div>
<section class="section">
<div class="section__header">
<h2 class="section__title">Service Monitors</h2>
</div>
<p class="muted" style="margin-bottom:1rem">
Monitor names must match exactly what's configured in arcline-uptime.
</p>
<form method="POST" action="/admin/clients/{{.Client.ID}}/monitors/add" class="inline-form">
<input class="field__input" type="text" name="monitor_name"
placeholder="monitor name (from arcline-uptime)" required>
<input class="field__input" type="text" name="label"
placeholder="display label (optional)">
<button type="submit" class="btn btn--primary btn--sm">+ add monitor</button>
</form>
{{if .Monitors}}
<table class="table" style="margin-top:1rem">
<thead><tr><th>monitor name</th><th>label</th><th></th></tr></thead>
<tbody>
{{range .Monitors}}
<tr>
<td class="td-mono">{{.MonitorName}}</td>
<td class="text-dim">{{if .Label}}{{.Label}}{{else}}&mdash;{{end}}</td>
<td>
<form method="POST" action="/admin/clients/{{$.Data.Client.ID}}/monitors/delete" style="display:inline">
<input type="hidden" name="monitor_id" value="{{.ID}}">
<button type="submit" class="btn-link btn-link--danger">remove</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p class="muted">No monitors assigned.</p>
{{end}}
</section>
<section class="section">
<h2 class="section__title">Domains</h2>
{{if .Domains}}
<table class="table">
<thead><tr><th>domain</th><th>expires</th><th>days</th><th>status</th></tr></thead>
<tbody>
{{range .Domains}}
<tr>
<td class="td-mono">{{.Domain}}</td>
<td class="text-dim">{{if .IsValid}}{{formatDate .ExpiresAt}}{{else}}&mdash;{{end}}</td>
<td class="td-mono">{{if .IsValid}}{{.DaysRemaining}}d{{else}}&mdash;{{end}}</td>
<td>
{{if .IsValid}}
{{if gt .DaysRemaining 30}}<span class="badge badge--ok">OK</span>
{{else if ge .DaysRemaining 14}}<span class="badge badge--warn">EXPIRING</span>
{{else}}<span class="badge badge--err">CRITICAL</span>{{end}}
{{else if .CheckError}}<span class="badge badge--err">ERROR</span>
{{else}}<span class="badge badge--dim">PENDING</span>{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p class="muted">No domains tracked for this client.</p>
{{end}}
</section>
{{end}}
{{end}}

View File

@@ -0,0 +1,97 @@
{{define "content"}}
<div class="page-header">
<p class="page-header__label">admin</p>
<h1 class="page-header__title">Admin Overview</h1>
</div>
{{with .Data}}
<section class="section">
<div class="section__header">
<h2 class="section__title">Clients</h2>
</div>
<div class="term-window term-window--narrow">
<div class="term-header">
<div class="term-controls"><button class="term-btn term-btn--close"></button><button class="term-btn"></button><button class="term-btn"></button></div>
<span class="term-title">new-client</span>
</div>
<div class="term-body">
<form method="POST" action="/admin/clients/new" class="admin-form">
<div class="form-row">
<div class="field">
<label class="field__label" for="username">username</label>
<input class="field__input" type="text" id="username" name="username" required>
</div>
<div class="field">
<label class="field__label" for="display_name">display name</label>
<input class="field__input" type="text" id="display_name" name="display_name" required>
</div>
<div class="field">
<label class="field__label" for="email">email</label>
<input class="field__input" type="email" id="email" name="email">
</div>
<div class="field">
<label class="field__label" for="password">password</label>
<input class="field__input" type="password" id="password" name="password" minlength="8" required>
</div>
<div class="field field--check">
<label class="checkbox-label">
<input type="checkbox" name="is_admin" value="1"> admin
</label>
</div>
</div>
<button type="submit" class="btn btn--primary btn--sm">create client</button>
</form>
</div>
</div>
{{if .Clients}}
<table class="table">
<thead><tr><th>username</th><th>display name</th><th>role</th><th>joined</th><th></th></tr></thead>
<tbody>
{{range .Clients}}
<tr>
<td class="td-mono">{{.Username}}</td>
<td><a href="/admin/clients/{{.ID}}" class="link">{{.DisplayName}}</a></td>
<td>{{if .IsAdmin}}<span class="badge badge--admin">admin</span>{{else}}<span class="badge badge--dim">client</span>{{end}}</td>
<td class="text-dim">{{formatDate .CreatedAt}}</td>
<td>
<form method="POST" action="/admin/clients/{{.ID}}/delete" style="display:inline">
<button type="submit" class="btn-link btn-link--danger"
onclick="return confirm('Delete {{.DisplayName}}? This cannot be undone.')">delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p class="muted">No clients yet.</p>
{{end}}
</section>
<section class="section">
<h2 class="section__title">All Tickets</h2>
{{if .Tickets}}
<table class="table">
<thead><tr><th>#</th><th>subject</th><th>client</th><th>status</th><th>updated</th></tr></thead>
<tbody>
{{range .Tickets}}
<tr>
<td class="text-dim td-mono">#{{.ID}}</td>
<td><a href="/tickets/{{.ID}}" class="link">{{.Subject}}</a></td>
<td class="text-dim">{{.ClientName}}</td>
<td><span class="badge badge--{{.Status}}">{{.Status}}</span></td>
<td class="text-dim">{{ago .UpdatedAt}}</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p class="muted">No tickets.</p>
{{end}}
</section>
{{end}}
{{end}}