initial commit

This commit is contained in:
Krzysztof
2025-08-01 18:01:55 +02:00
commit 9af1af7f92
73 changed files with 6531 additions and 0 deletions

76
html/app.html Normal file
View File

@@ -0,0 +1,76 @@
{{ define "body" }}
<div class="flex flex-col gap-2 min-h-screen">
{{ template "nav" . }}
<main class="flex-1">
{{ block "content" . }}{{ end }}
</main>
{{ template "footer" . }}
</div>
{{ end }}
{{ define "nav" }}
<div class="max-w-[1600px] w-full mx-auto">
<div class="flex justify-between gap-8 items-center py-2 p-4 pb-0">
<div class="flex-1">
<div class="breadcrumbs text-xs">
<ul>
{{ block "breadcrumbs" . }}{{ end }}
</ul>
</div>
</div>
<div class="shrink-0">
<a class="btn btn-neutral" href="/new" hx-boost preload="mouseover">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5!">
<path
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h2.5A2.25 2.25 0 0 1 9 4.25v2.5A2.25 2.25 0 0 1 6.75 9h-2.5A2.25 2.25 0 0 1 2 6.75v-2.5ZM2 13.25A2.25 2.25 0 0 1 4.25 11h2.5A2.25 2.25 0 0 1 9 13.25v2.5A2.25 2.25 0 0 1 6.75 18h-2.5A2.25 2.25 0 0 1 2 15.75v-2.5ZM11 4.25A2.25 2.25 0 0 1 13.25 2h2.5A2.25 2.25 0 0 1 18 4.25v2.5A2.25 2.25 0 0 1 15.75 9h-2.5A2.25 2.25 0 0 1 11 6.75v-2.5ZM15.25 11.75a.75.75 0 0 0-1.5 0v2h-2a.75.75 0 0 0 0 1.5h2v2a.75.75 0 0 0 1.5 0v-2h2a.75.75 0 0 0 0-1.5h-2v-2Z" />
</svg>
<span class="">
Create
</span>
</a>
</div>
</div>
</div>
{{ end }}
{{ define "footer" }}
<div class="max-w-[1600px] mx-auto w-full p-4 pt-10">
<footer class="text-sm text-base-content p-4 flex flex-col md:flex-row gap-12 md:gap-4">
<nav class="flex flex-col gap-2 flex-1">
<h6 class="footer-title truncate">{{ .User.Name }}</h6>
<a class="link link-hover">Account settings</a>
<a class="link link-hover">Manage users</a>
<a class="link link-hover" href="/logout">Log out</a>
</nav>
<nav class="flex flex-col gap-2 flex-1">
<h6 class="footer-title">UPTIMEMONITOR</h6>
<a class="link link-hover">Documentation</a>
<a class="link link-hover">Pricing</a>
<a class="link link-hover">Terms of use</a>
</nav>
<nav class="flex flex-col gap-2 flex-1">
<h6 class="footer-title">AIR Labs</h6>
<a class="link link-hover">About us</a>
<a class="link link-hover">Other products</a>
<a class="link link-hover flex items-center gap-1" href="https://x.com/airlabspl" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" version="1.1" class="size-[11px]!">
<path
d="M178.57 127.15 290.27 0h-26.46l-97.03 110.38L89.34 0H0l117.13 166.93L0 300.25h26.46l102.4-116.59 81.8 116.59h89.34M36.01 19.54H76.66l187.13 262.13h-40.66" />
</svg>
<div>Account</div>
</a>
</nav>
</footer>
<footer class="footer text-base-content p-4">
<aside class="flex flex-col gap-3">
<div>
{{ template "logo" . }}
</div>
<div>
© 2025 AIR Labs
</div>
</aside>
</footer>
</div>
{{ end }}

57
html/check.html Normal file
View File

@@ -0,0 +1,57 @@
{{ define "check_list" }}
<div class="flex flex-col gap-1" hx-swap="outerHTML" hx-get="/monitors/{{ .Monitor.ID }}/checks" hx-trigger="every 60s">
<div class="flex justify-between items-center w-full gap-1.5 relative">
{{ $checkMaxIndex := sub (len $.Checks) 1 }}
{{ range $i, $v := .Skeletons }}
{{ $currentIndex := sub 59 $i }}
{{ $check := "" }}
{{ if lt $currentIndex (len $.Checks) }}
{{ $check = index $.Checks $currentIndex }}
{{ end }}
<div
class="flex-1 tooltip justify-center items-center {{ if gt $i 30 }} {{ else if gt $i 25 }} hidden md:inline-flex {{ else }} hidden lg:inline-flex {{ end }} ">
<div class="tooltip-content tooltip-bottom flex flex-col gap-1 bg-light">
{{ if $check }}
<div class="stats shadow bg-white">
<div class="stat flex flex-col gap-1 justify-stretch">
<div class="stat-desc">
<div class="badge badge-soft {{ $check.BadgeClass }} w-full">{{ $check.StatusCode }} {{
$check.StatusText }}</div>
</div>
<div class="stat-value text-center text-sm font-medium">
{{ $check.CreatedAt.Format "2006-01-02 15:04" }}, <span class="font-semibold">{{
$check.ResponseTimeMs
}}</span>ms
</div>
</div>
</div>
{{ else }}
{{ end }}
</div>
<div class="relative h-[64px] w-[6px] rounded-full bg-neutral-100 overflow-hidden"
id="{{ if $check }}monitors-{{ $check.MonitorID }}-checks-{{ $check.ID }}{{ else }}placeholder-{{ $i }}{{ end }}">
<div id="{{ if $check }}monitors-{{ $check.MonitorID }}-index-{{ $currentIndex }}{{ end }}"
class="absolute w-full bottom-0 rounded-lg transition-all duration-300 ease-in-out {{ if $check }} {{ $check.HeightClass $.MaxTime }} {{ $check.ColorClass }} {{ else }}bg-neutral-200 {{ end }} ">
</div>
</div>
</div>
{{ end }}
<div class="relative h-[64px] w-[6px] overflow-x-hidden rounded-full bg-neutral-100 ">
<div
class="absolute w-[6px] bottom-0 rounded-lg transition-all duration-300 ease-in-out bg-neutral-200 h-full animate-pulse">
</div>
</div>
{{ if eq (len $.Checks ) 0}}
<div class="absolute inset-0 flex items-center justify-center z-[999999] opacity-80 text-xs animate-pulse">
waiting...
</div>
{{ end }}
</div>
<div class="w-full justify-between text-xs opacity-60 hidden lg:flex">
<div>{{ .StartTime }}</div>
<div>{{ .EndTime }}</div>
</div>
</div>
{{ end }}

62
html/delete.html Normal file
View File

@@ -0,0 +1,62 @@
{{ define "title" }}Delete monitor{{end}}
{{ define "content" }}
<div class="mx-auto max-w-[1600px] w-full">
<div class="md:flex md:items-center md:justify-center p-8 md:p-10">
{{template "delete_form" . }}
</div>
</div>
{{ end }}
{{ define "delete_form" }}
<form hx-delete="/monitors/{{ .Monitor.ID }}" hx-swap="outerHTML" class="flex flex-col gap-4 w-full max-w-md">
<div>
<h1 class="font-semibold text-lg">Delete monitor</h1>
</div>
<div>
<div role="alert" class="alert alert-vertical alert-error alert-soft">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-6! text-black">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
<span>Are you sure? All the data will be lost.</span>
<div>
<a href="/m/{{ .Monitor.Uuid }}" hx-boost preload>
<button class="btn btn-sm btn-soft" type="button">Cancel</button>
</a>
<button class="btn btn-sm btn-neutral " type="submit">Remove this monitor</button>
</div>
</div>
</div>
</form>
{{ end }}
{{ define "breadcrumbs" }}
<li class="">
<a href="/" hx-boost preload>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
Monitors
</a>
</li>
<li class="">
<a href="/m/{{ .Monitor.Uuid }}" hx-boost preload>
<div class="flex items-center gap-2 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>
<span class="truncate">{{ .Monitor.Host }}</span>
</div>
</a>
</li>
<li class="">
<div class="flex items-center gap-2 ">
Delete
</div>
</li>
{{ end }}

181
html/edit.html Normal file
View File

@@ -0,0 +1,181 @@
{{ define "title" }}Edit monitor{{end}}
{{ define "content" }}
<div class="mx-auto max-w-[1600px] w-full">
<div class="md:flex md:items-center md:justify-center p-8 md:p-10">
{{template "edit_form" . }}
</div>
</div>
{{ end }}
{{ define "edit_form" }}
<form hx-patch="/monitors/{{ .Monitor.ID }}" hx-swap="outerHTML" class="flex flex-col gap-4 w-full md:max-w-md">
<div>
<h1 class="font-semibold text-lg">Edit monitor</h1>
</div>
<fieldset class="fieldset">
<div class="join w-full flex">
<div>
<select class="select join-item" name="http_method">
<option {{ if eq .Form.HttpMethod "GET" }}selected{{ end }}>GET</option>
<option {{ if eq .Form.HttpMethod "POST" }}selected{{ end }}>POST</option>
<option {{ if eq .Form.HttpMethod "PATCH" }}selected{{ end }}>PATCH</option>
<option {{ if eq .Form.HttpMethod "PUT" }}selected{{ end }}>PUT</option>
<option {{ if eq .Form.HttpMethod "DELETE" }}selected{{ end }}>DELETE</option>
</select>
</div>
<div class="flex-1 ">
<input name="url" id="edit_form--url"
class="input join-item {{ with .Form.Errors.Url }} input-error {{ end }} w-full" required type="url"
placeholder="https://example.com" value="{{ .Form.Url }}" />
{{ with .Form.Errors.Url }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</div>
</div>
</fieldset>
<div class="card border border-base-200">
<div class="card-body p-4">
<div class="card-actions justify-start peer">
<label class="label">
<input type="checkbox" class="toggle toggle-sm" name="has_custom_headers" {{ if
.Form.HasCustomHeaders }} checked {{ end }} />
Custom Headers
</label>
</div>
<div class="hidden peer-has-checked:block">
<textarea class="textarea bg-neutral text-neutral-content w-full textarea" placeholder="{}"
name="http_headers">{{ .Form.HttpHeaders }}</textarea>
{{ with .Form.Errors.HttpHeaders }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</div>
</div>
</div>
<div class="card border border-base-200">
<div class="card-body p-4">
<div class="card-actions justify-start peer">
<label class="label">
<input type="checkbox" class="toggle toggle-sm" name="has_custom_body" {{ if .Form.HasCustomBody }}
checked {{ end }} />
Custom Body
</label>
</div>
<div class="hidden peer-has-checked:block">
<textarea class="textarea bg-neutral text-neutral-content w-full textarea" placeholder="{}"
name="http_body">{{ .Form.HttpBody }}</textarea>
{{ with .Form.Errors.HttpBody }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</div>
</div>
</div>
<div class="card border border-base-200">
<div class="card-body p-4">
<div class="card-actions justify-start peer">
<label class="label">
<input type="checkbox" class="toggle toggle-sm" name="has_webhook" {{ if .Form.HasWebhook }} checked
{{ end }} />
Webhook
<div class="tooltip">
<div class="tooltip-content tooltip-neutral bg-neutral text-white/90 text-xs">
After each incident the given URL will be notified
</div>
<button class="btn btn-square btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-5!">
<path stroke-linecap="round" stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
</button>
</div>
</label>
</div>
<div class="hidden peer-has-checked:block">
<fieldset class="fieldset">
<legend class="fieldset-legend">
Webhook URL
</legend>
<div class="join w-full flex">
<div>
<select class="select join-item" name="webhook_method">
<option {{ if eq .Form.WebhookMethod "GET" }}selected{{ end }}>GET</option>
<option {{ if eq .Form.WebhookMethod "POST" }}selected{{ end }}>POST</option>
<option {{ if eq .Form.WebhookMethod "PATCH" }}selected{{ end }}>PATCH</option>
<option {{ if eq .Form.WebhookMethod "PUT" }}selected{{ end }}>PUT</option>
<option {{ if eq .Form.WebhookMethod "DELETE" }}selected{{ end }}>DELETE</option>
</select>
</div>
<div class="flex-1">
<input name="webhook_url" id="new_form--webhook_url"
class="input join-item {{ with .Form.Errors.WebhookUrl }} input-error {{ end }} w-full"
type="url" placeholder="https://example.com" value="{{ .Form.WebhookUrl }}" />
{{ with .Form.Errors.WebhookUrl }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</div>
</div>
</fieldset>
</div>
<div class="hidden peer-has-checked:block">
<fieldset class="fieldset">
<legend class="fieldset-legend">
Webhook Headers
</legend>
<textarea class="textarea w-full textarea bg-neutral text-neutral-content" placeholder="{}"
name="webhook_headers">{{ .Form.WebhookHeaders }}</textarea>
{{ with .Form.Errors.WebhookHeaders }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
</div>
<div class="hidden peer-has-checked:block">
<fieldset class="fieldset">
<legend class="fieldset-legend">
Webhook Body
</legend>
<textarea class="textarea w-full textarea bg-neutral text-neutral-content" placeholder="{}"
name="webhook_body">{{ .Form.WebhookBody }}</textarea>
{{ with .Form.Errors.WebhookBody }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
</div>
</div>
</div>
<div>
<button type="submit" class="btn btn-neutral">
Submit
</button>
</div>
</form>
{{ end }}
{{ define "breadcrumbs" }}
<li class="">
<a href="/" hx-boost preload>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
Monitors
</a>
</li>
<li class="">
<a href="/m/{{ .Monitor.Uuid }}" hx-boost preload>
<div class="flex items-center gap-2 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>
<span class="truncate">{{ .Monitor.Host }}</span>
</div>
</a>
</li>
<li class="">
<div class="flex items-center gap-2 ">
Edit
</div>
</li>
{{ end }}

8
html/embed.go Normal file
View File

@@ -0,0 +1,8 @@
package html
import "embed"
var (
//go:embed *.html
FS embed.FS
)

28
html/home.html Normal file
View File

@@ -0,0 +1,28 @@
{{ define "title" }}Home{{ end }}
{{define "content"}}
<div class="flex flex-col-reverse lg:flex-row gap-4 justify-between max-w-[1600px] mx-auto px-4">
<div class="flex-1">
<div hx-get="/monitors" hx-trigger="intersect" hx-swap="outerHTML"></div>
</div>
<div class="min-w-[300px]! flex flex-col gap-4">
<div hx-get="/incidents" hx-trigger="intersect" hx-swap="outerHTML"></div>
{{ template "sponsors" . }}
</div>
</div>
{{ end }}
{{ define "breadcrumbs" }}
<li class="">
<a href="/" hx-boost preload>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="size-4! stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
Monitors
</a>
</li>
{{ end }}

184
html/incident.html Normal file
View File

@@ -0,0 +1,184 @@
{{ define "incident_list" }}
<ul class="list card card-border {{ if .Incidents }} bg-neutral text-neutral-content {{ end }} bg-base-100 w-full w-full rounded-box lg:max-w-sm"
hx-get="/incidents" hx-trigger="every 60s" hx-swap="outerHTML">
{{ if .Incidents }}
<li class="p-4 pb-2 tracking-wide font-semibold">
🚨 Open Incidents
</li>
{{ range .Incidents }}
<a href="/m/{{ .Monitor.Uuid }}/i/{{ .Uuid}}" hx-boost preload>
<li class="list-row w-full max-w-full block" id="incidents-{{ .ID }}">
<div class="flex flex-col gap-1.5">
<div class="flex gap-2 justify-between items-center">
<div aria-label="error" class="status status-error animate-ping shrink-0"></div>
<div class="text-xs font-semibold truncate flex-1 shrink-0">
<div class="flex items-base gap-1 w-full max-w-full">
<span class="flex-none truncate"> {{ .Monitor.Domain }}</span>
<span class="flex-grow leader-dots hidden lg:block"></span>
</div>
</div>
</div>
<div class="flex gap-2 justify-between items-center">
<div class="max-w-full overflow-hidden text-xs opacity-80 text-right">
{{ .CreatedAt.Format "2006-01-02 15:04" }}
</div>
<div class="text-xs">
<div class="badge badge-soft truncate max-w-full inline-block text-xs py-1 {{ .BadgeClass }}">
{{ .StatusCode }}
</div>
</div>
</div>
</div>
</li>
</a>
{{ end }}
{{ else }}
<li class="p-4 pb-0 tracking-wide font-semibold">
Open Incidents
</li>
<li class="p-4 tracking-wide opacity-60 text-xs">
No incidents to report.
</li>
{{ end }}
</ul>
{{ end }}
{{ define "title" }}
{{ .Incident.StatusCode }} {{ .Incident.StatusCodeText }} ({{ .Incident.CreatedAt.Format "2006-01-02 15:04"}})
{{ end }}
{{ define "content" }}
<div class="max-w-[1600px] mx-auto px-4">
<div class="flex justify-between gap-4 flex-col lg:flex-row">
{{ with .Incident }}
<div class="flex-1 flex flex-col gap-4">
<div class="card card-border p-4 flex flex-col gap-4">
<div class="flex flex-col md:flex-row gap-1.5 md:gap-4 md:items-start justify-between text-sm">
<div>
<div
class="badge badge-soft truncate max-w-full inline-block text-xs py-1 {{ .StatusBadgeClass }}">
{{ .StatusBadgeText }}
</div>
</div>
<div class="flex-1 flex flex-col gap-1">
<div>
{{ .StatusCode }} {{ .StatusCodeText }}
</div>
<div class="w-auto max-w-[350px] text-xs leading-[1.2rem] opacity-80">
{{ .CreatedAt.Format "2006-01-02 15:04" }}
{{ if .ResolvedAt }}
(resolved at {{ .ResolvedAt.Format "2006-01-02 15:04" }})
{{ end }}
</div>
</div>
</div>
</div>
<div class="px-4">
<h2 class="font-semibold">Request</h2>
</div>
<div class="mockup-code w-full">
<div class="p-4 max-h-[600px] overflow-y-scroll overflow-x-scroll flex flex-col gap-1">
{{ if and $.Incident.ReqMethod $.Incident.ReqUrl }}
<pre>curl -X{{ $.Incident.ReqMethod }} {{ $.Incident.ReqUrl }} \</pre>
{{ end}}
{{ if $.Incident.ReqHeaders }}
{{ range $key, $value := $.Incident.ReqHeadersMap }}
<pre class="opacity-60 pl-4">-H '{{ $key }}: {{ $value }}' \</pre>
{{ end }}
{{ end}}
{{ if $.Incident.ReqBody }}
<pre class="pl-4">-d '{{ $.Incident.ReqBody }}'</pre>
{{ end}}
</div>
</div>
<div class="px-4">
<h2 class="font-semibold">Response</h2>
</div>
<div class="mockup-browser border border-base-200 w-full bg-base-100 text-neutral-800 text-sm">
<div class="mockup-browser-toolbar">
<div class="input">{{ $.Monitor.HttpMethod }} {{ $.Monitor.Url }}</div>
</div>
<div class="p-4 max-h-[600px] overflow-y-scroll overflow-x-scroll flex flex-col gap-2">
{{ if $.Incident.Headers }}
<pre class="opacity-60">{{ $.Incident.Headers }}</pre>
{{ end}}
{{ if $.Incident.Body }}
<pre>{{ $.Incident.Body }}</pre>
{{ else }}
<pre>//</pre>
{{ end}}
</div>
</div>
</div>
{{ end }}
<div class="lg:w-[237px] flex flex-col gap-4">
<ul class="list bg-base-100 rounded-box border border-base-200 p-2 text-left">
<form class="block list-row justify-stretch px-0 py-0" hx-delete="/incidents/{{ .Incident.ID }}"
hx-confirm="Are you sure?" hx-swap="none">
<button type="submit">
<div class="list-col-grow">
<button class="btn btn-ghost w-full text-left align-start justify-start text-error">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="size-5!">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
Remove incident
</button>
</div>
</button>
</form>
</ul>
</div>
</div>
</div>
{{end}}
{{ define "breadcrumbs" }}
<li class="">
<a href="/" hx-boost preload>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
Monitors
</a>
</li>
<li class="">
<a href="/m/{{ .Monitor.Uuid }}" hx-boost preload>
<div class="flex items-center gap-2 hidden lg:flex">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>
<span class="truncate">{{ .Monitor.Host }}</span>
</div>
<span class="lg:hidden opacity-40">...</span>
</a>
</li>
<li class="">
<div class="flex items-center gap-2 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
<span class="truncate">{{ .Incident.StatusCode }} {{ .Incident.StatusCodeText }}</span>
</div>
</li>
{{ end }}

45
html/layout.html Normal file
View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>{{ block "title" .}}{{ end }} - Uptime Monitor</title>
<link rel="stylesheet" href="/static/css/daisyui.css?v=5.0.46" />
<link rel="stylesheet" href="/static/css/themes.css?v=5.0.46" />
<link rel="stylesheet" href="/static/css/main.css?v=1" />
{{ block "head" .}}{{ end }}
</head>
<body hx-ext="preload" data-theme="emerald" class="min-h-screen font-mono">
{{ block "body" . }}{{ end }}
<script defer src="/static/js/tailwind.min.js?v=5.0.46"></script>
<script defer src="/static/js/htmx.min.js?v=2.0.6"></script>
<script defer src="/static/js/htmx-ext-preload.min.js?v=2.1.0"></script>
<script defer src="/static/js/main.js?v=1"></script>
</body>
</html>
{{ define "logo"}}
<a href="/" hx-boost preload="mouseover" class="flex items-center gap-1.5 text-sm font-medium">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-5!">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941" />
</svg>
<span class="">
UM {{ template "version" . }}
</span>
</a>
{{ end }}
{{ define "version" }}
<span class="text-neutral-400 text-xs font-normal truncate">v0.5.0-alpha</span>
{{ end }}
{{ define "sponsors" }}
<div hx-get="/sponsors" hx-trigger="intersect" hx-swap="outerHTML"></div>
{{ end }}

53
html/login.html Normal file
View File

@@ -0,0 +1,53 @@
{{ define "title" }}Log in{{end}}
{{ define "body" }}
<div class="flex items-center justify-center min-h-screen p-8 pt-16">
<div class="absolute top-2 left-2">
{{ template "logo" . }}
</div>
{{template "login_form" . }}
</div>
{{ end }}
{{ define "login_form" }}
<form hx-post="/login" hx-swap="outerHTML" class="w-full max-w-sm flex flex-col gap-4">
<div>
<h1 class="flex gap-1 items-center text-lg font-semibold">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
style="height: 20px; width: auto;">
<path fill-rule="evenodd"
d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z"
clip-rule="evenodd" />
</svg>
Log in
</h1>
<h5 class="text-sm">
Please fill in your credentials to proceed
</h5>
</div>
<div>
<fieldset class="fieldset">
<legend class="fieldset-legend">Email</legend>
<input class="input w-full {{ with .Form.Errors.Email }} input-error {{ end }}" id="login_form--email"
name="email" type="email" autocomplete="email" value="{{ .Form.Email}}" placeholder="email@example.com"
required />
{{ with .Form.Errors.Email }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Password</legend>
<input class="input w-full {{ with .Form.Errors.Password }} input-error {{ end }}" id="login_form--password"
name="password" type="password" autocomplete="current-password" placeholder="••••••••" required
hx-preserve />
{{ with .Form.Errors.Password }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
</div>
<button type="submit" class="btn btn-neutral">
Submit
</button>
</form>
{{ end }}

245
html/monitor.html Normal file
View File

@@ -0,0 +1,245 @@
{{ define "title" }}
{{ .Monitor.Domain }}
{{end}}
{{ define "monitor_list" }}
<div class="grid gap-4">
{{ range .Monitors }}
<a href="{{ .URI }}" hx-boost preload>
<div class="card card-border p-4 flex flex-col gap-4">
<h2 class="card-title text-sm">
<div class="font-semibold flex gap-1.5 items-center flex-1 ">
{{ if .Secure }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4!">
<path fill-rule="evenodd"
d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z"
clip-rule="evenodd" />
</svg>
{{ else }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="size-4! opacity-50">
<path
d="M18 1.5c2.9 0 5.25 2.35 5.25 5.25v3.75a.75.75 0 0 1-1.5 0V6.75a3.75 3.75 0 1 0-7.5 0v3a3 3 0 0 1 3 3v6.75a3 3 0 0 1-3 3H3.75a3 3 0 0 1-3-3v-6.75a3 3 0 0 1 3-3h9v-3c0-2.9 2.35-5.25 5.25-5.25Z" />
</svg>
{{ end }}
<span class="truncate">{{ .Domain }}</span>
</div>
</h2>
<div hx-get="/monitors/{{ .ID }}/checks" hx-swap="outerHTML" hx-trigger="intersect"
class="flex flex-col gap-1">
<div class="flex justify-between items-center w-full gap-1.5">
{{ range $i, $e := $.Skeletons }}
<div class="h-[64px] rounded-full w-[6px] bg-neutral-100 animate-pulse stagger {{ if gt $i 30 }} {{ else if gt $i 25 }} hidden md:block{{ else }} hidden lg:block{{ end }}"
style="--index: {{ $i }};">
</div>
{{ end }}
</div>
<div class="w-full hidden lg:flex justify-between text-xs opacity-60">
<div>--:--</div>
<div>--:--</div>
</div>
</div>
</div>
</a>
{{ end }}
</div>
{{ end }}
{{ define "content" }}
<div class="max-w-[1600px] mx-auto px-4">
<div class="flex flex-col gap-4 lg:flex-row">
<div class="flex-1 flex flex-col gap-4">
<div class="card card-border p-4 flex flex-col gap-4">
<div class="flex items-center justify-between gap-2">
<div class="font-semibold flex gap-1.5 items-center flex-1 ">
<div class="flex-1 truncate text-sm">
{{ .Monitor.Url }}
</div>
</div>
</div>
<div>
<div hx-get="/monitors/{{ .Monitor.ID }}/checks" hx-swap="outerHTML" hx-trigger="intersect"
class="flex flex-col gap-1">
<div class="flex justify-between items-center w-full gap-1.5">
{{ range $i, $e := .Skeletons }}
<div class="h-[64px] rounded-full w-[6px] bg-neutral-100 animate-pulse stagger {{ if gt $i 30 }} {{ else if gt $i 25 }} hidden md:block{{ else }} hidden lg:block{{ end }}"
style="--index: {{ $i }};">
</div>
{{ end }}
</div>
<div class="w-full idden lg:flex justify-between text-xs opacity-60">
<div>--:--</div>
<div>--:--</div>
</div>
</div>
</div>
</div>
<div class="stats w-full flex flex-col md:flex-row gap-1.5 px-4" hx-get="/monitors/{{ .Monitor.ID }}/stats"
hx-trigger="intersect" hx-swap="outerHTML">
<div class="stat md:place-items-center p-0">
<div class="stat-title">Avg. response time</div>
<div class="stat-value text-lg md:text-3xl ">-</div>
</div>
<div class="stat md:place-items-center p-0">
<div class="stat-title">Checks</div>
<div class="stat-value text-lg md:text-3xl ">-</div>
</div>
<div class="stat md:place-items-center p-0">
<div class="stat-title">Fails</div>
<div class="stat-value text-lg md:text-3xl ">-</div>
</div>
<div class="stat md:place-items-center p-0">
<div class="stat-title">Uptime</div>
<div class="stat-value text-lg md:text-3xl ">-</div>
</div>
</div>
<div>
<div hx-get="/monitors/{{ .Monitor.ID }}/incidents" hx-trigger="intersect" hx-swap="outerHTML"></div>
</div>
</div>
<div class="lg:w-[237px] flex flex-col gap-4">
<ul class="list bg-base-100 rounded-box border border-base-200 p-2 text-left">
<a href="/m/{{ .Monitor.Uuid }}/edit" hx-boost preload>
<div class="list-row justify-stretch px-0 py-0">
<div class="list-col-grow">
<button class="btn btn-ghost w-full text-left align-start justify-start">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="size-5!">
<path stroke-linecap="round" stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
Edit monitor
</button>
</div>
</div>
</a>
<div class="list-row justify-stretch px-0 py-0">
<a href="/m/{{ .Monitor.Uuid }}/delete" hx-boost preload>
<div class="list-col-grow">
<button class="btn btn-ghost w-full text-left align-start justify-start text-error">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="size-5!">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
Remove monitor
</button>
</div>
</a>
</div>
</ul>
</div>
</div>
</div>
{{ end }}
{{ define "monitor_stats" }}
<div class="stats w-full flex flex-col md:flex-row gap-1.5 px-4" hx-get="/monitors/{{ .ID }}/stats"
hx-trigger="every 60s" hx-swap="outerHTML">
<div class="stat md:place-items-center p-0">
<div class="stat-title">Avg. response time</div>
<div class="stat-value text-lg md:text-3xl ">{{ .AvgResponseTime }}ms</div>
</div>
<div class="stat md:place-items-center p-0">
<div class="stat-title">Checks</div>
<div class="stat-value text-lg md:text-3xl ">{{ .ChecksCount }}</div>
</div>
<div class="stat md:place-items-center p-0">
<div class="stat-title">Fails</div>
<div class="stat-value text-lg md:text-3xl ">{{ .FailureCount }}</div>
</div>
<div class="stat md:place-items-center p-0">
<div class="stat-title">Uptime</div>
<div class="stat-value text-lg md:text-3xl ">{{ .Uptime }}%</div>
</div>
</div>
{{ end }}
{{ define "monitor_incident_list" }}
<div class="card card-border bg-base-100 {{ if .Incidents }} bg-neutral text-neutral-content {{ end }} w-full w-full rounded-box"
hx-get="/monitors/{{ .ID }}/incidents" hx-trigger="every 60s" hx-swap="outerHTML">
{{ if .Incidents }}
<div class="p-4 pb-2 tracking-wide font-semibold">
Incidents
</div>
<div class="overflow-x-auto flex flex-col gap-4 md:gap-0 pb-2">
{{ range .Incidents }}
<a href="/m/{{ .Monitor.Uuid }}/i/{{ .Uuid }}" hx-boost preload>
<div class="p-0" id="incidents-{{ .ID }}">
<div class="px-4 py-2">
<div class="">
<div class="flex flex-col md:flex-row gap-1.5 md:gap-4 md:items-start justify-between text-sm">
<div>
<div
class="badge badge-soft truncate max-w-full inline-block text-xs py-1 w-[100px] text-center {{ .StatusBadgeClass }}">
{{ .StatusBadgeText }}
</div>
</div>
<div class="flex-1 flex flex-col gap-1">
<div>
{{ .StatusCode }} {{ .StatusCodeText }}
</div>
<div class="w-auto max-w-[350px] text-xs leading-[1.2rem] opacity-80">
{{ .CreatedAt.Format "2006-01-02 15:04" }}
{{ if .ResolvedAt }}
(resolved at {{ .ResolvedAt.Format "2006-01-02 15:04" }})
{{ end }}
</div>
</div>
<div>
<label for="incident-details-{{ .ID }}" class="btn btn-sm btn-warning">
Details
</label>
</div>
</div>
</div>
</div>
</div>
</a>
{{ end }}
</div>
{{ else }}
<div class="p-4 pb-0 tracking-wide font-semibold">
Incidents
</div>
<div class="p-4 tracking-wide opacity-60 text-xs">
No incidents to report.
</div>
{{ end }}
</div>
{{ end }}
{{ define "breadcrumbs" }}
<li class="">
<a href="/" hx-boost preload>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
Monitors
</a>
</li>
<li class="">
<div class="flex items-center gap-2 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>
<span class="truncate">{{ .Monitor.Host }}</span>
</div>
</li>
{{ end }}

175
html/new.html Normal file
View File

@@ -0,0 +1,175 @@
{{ define "title" }}Create monitor{{end}}
{{ define "content" }}
<div class="mx-auto max-w-[1600px] w-full">
<div class="md:flex md:items-center md:justify-center p-8 md:p-10">
{{template "new_form" . }}
</div>
</div>
{{ end }}
{{ define "breadcrumbs" }}
<li class="">
<a href="/" hx-boost preload>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="size-4! stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
Monitors
</a>
</li>
<li class="">
<div class="flex items-center gap-2 ">
New
</div>
</li>
{{ end }}
{{ define "new_form" }}
<form hx-post="/monitors" hx-swap="outerHTML" class="flex flex-col gap-4 w-full md:max-w-md">
<div>
<h1 class="font-semibold text-lg">Create monitor</h1>
<h5 class="text-sm">
The url you provide will be monitored every minute.
</h5>
</div>
<fieldset class="fieldset">
<legend class="fieldset-legend">
Website URL
</legend>
<div class="join w-full flex">
<div>
<select class="select join-item" name="http_method">
<option selected>GET</option>
<option>POST</option>
<option>PATCH</option>
<option>PUT</option>
<option>DELETE</option>
</select>
</div>
<div class="flex-1">
<input name="url" id="new_form--url"
class="input join-item {{ with .Form.Errors.Url }} input-error {{ end }} w-full" autofocus required
type="url" placeholder="https://example.com" value="{{ .Form.Url }}" />
{{ with .Form.Errors.Url }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</div>
</div>
</fieldset>
<div class="card border border-base-200">
<div class="card-body p-4">
<div class="card-actions justify-start peer">
<label class="label">
<input type="checkbox" class="toggle toggle-sm" name="has_custom_headers" {{ if
.Form.HasCustomHeaders }} checked {{ end }} />
Custom Headers
</label>
</div>
<div class="hidden peer-has-checked:block">
<textarea class="textarea w-full textarea bg-neutral text-neutral-content" placeholder="{}"
name="http_headers">{{ .Form.HttpHeaders }}</textarea>
{{ with .Form.Errors.HttpHeaders }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</div>
</div>
</div>
<div class="card border border-base-200">
<div class="card-body p-4">
<div class="card-actions justify-start peer">
<label class="label">
<input type="checkbox" class="toggle toggle-sm" name="has_custom_body" {{ if .Form.HasCustomBody }}
checked {{ end }} />
Custom Body
</label>
</div>
<div class="hidden peer-has-checked:block">
<textarea class="textarea w-full textarea bg-neutral text-neutral-content" placeholder="{}"
name="http_body">{{ .Form.HttpBody }}</textarea>
{{ with .Form.Errors.HttpBody }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</div>
</div>
</div>
<div class="card border border-base-200">
<div class="card-body p-4">
<div class="card-actions justify-start peer">
<label class="label">
<input type="checkbox" class="toggle toggle-sm" name="has_webhook" {{ if .Form.HasWebhook }} checked
{{ end }} />
Webhook
<div class="tooltip">
<div class="tooltip-content tooltip-neutral bg-neutral text-white/90 text-xs">
After each incident the given URL will be notified
</div>
<button class="btn btn-square btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-5!">
<path stroke-linecap="round" stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
</button>
</div>
</label>
</div>
<div class="hidden peer-has-checked:block">
<fieldset class="fieldset">
<legend class="fieldset-legend">
Webhook URL
</legend>
<div class="join w-full flex">
<div>
<select class="select join-item" name="webhook_method">
<option>GET</option>
<option selected>POST</option>
<option>PATCH</option>
<option>PUT</option>
<option>DELETE</option>
</select>
</div>
<div class="flex-1">
<input name="webhook_url" id="new_form--webhook_url"
class="input join-item {{ with .Form.Errors.WebhookUrl }} input-error {{ end }} w-full"
type="url" placeholder="https://example.com" value="{{ .Form.WebhookUrl }}" />
{{ with .Form.Errors.WebhookUrl }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</div>
</div>
</fieldset>
</div>
<div class="hidden peer-has-checked:block">
<fieldset class="fieldset">
<legend class="fieldset-legend">
Webhook Headers
</legend>
<textarea class="textarea w-full textarea bg-neutral text-neutral-content" placeholder="{}"
name="webhook_headers">{{ .Form.WebhookHeaders }}</textarea>
{{ with .Form.Errors.WebhookHeaders }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
</div>
<div class="hidden peer-has-checked:block">
<fieldset class="fieldset">
<legend class="fieldset-legend">
Webhook Body
</legend>
<textarea class="textarea w-full textarea bg-neutral text-neutral-content" rows="4" placeholder="{}"
name="webhook_body">{{ .Form.WebhookBody }}</textarea>
{{ with .Form.Errors.WebhookBody }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
</div>
</div>
</div>
<div>
<button type="submit" class="btn btn-neutral">
Submit
</button>
</div>
</form>
{{ end }}

60
html/setup.html Normal file
View File

@@ -0,0 +1,60 @@
{{ define "title" }}Setup{{end}}
{{ define "body" }}
<div class="flex items-center justify-center min-h-screen p-8 pt-16">
<div class="absolute top-2 left-2">
{{ template "logo" . }}
</div>
{{template "setup_form" . }}
</div>
{{ end }}
{{ define "setup_form" }}
<form hx-post="/setup" hx-swap="outerHTML" class="w-full max-w-sm flex flex-col gap-4">
<div>
<h1 class="flex gap-1 items-center text-lg font-semibold">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
style="height: 20px; width: auto;">
<path fill-rule="evenodd"
d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 0 0-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 0 0-2.282.819l-.922 1.597a1.875 1.875 0 0 0 .432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 0 0 0 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 0 0-.432 2.385l.922 1.597a1.875 1.875 0 0 0 2.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 0 0 2.28-.819l.923-1.597a1.875 1.875 0 0 0-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 0 0 0-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 0 0-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 0 0-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 0 0-1.85-1.567h-1.843ZM12 15.75a3.75 3.75 0 1 0 0-7.5 3.75 3.75 0 0 0 0 7.5Z"
clip-rule="evenodd" />
</svg>
Setup
</h1>
<h5 class="text-sm">
Create your admin account
</h5>
</div>
<div>
<fieldset class="fieldset">
<legend class="fieldset-legend">Name</legend>
<input class="input w-full {{ with .Form.Errors.Name }} input-error {{ end }}" id="setup_form--name"
name="name" required autofocus autocomplete="name" value="{{ .Form.Name}}" placeholder="John Doe" />
{{ with .Form.Errors.Name }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">E-mail</legend>
<input class="input w-full {{ with .Form.Errors.Email }} input-error {{ end }}" id="setup_form--email"
name="email" type="email" autocomplete="email" value="{{ .Form.Email}}" placeholder="email@example.com"
required />
{{ with .Form.Errors.Email }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Password</legend>
<input class="input w-full {{ with .Form.Errors.Password }} input-error {{ end }}" id="setup_form--password"
name="password" type="password" required autocomplete="new-password" placeholder="••••••••" />
{{ with .Form.Errors.Password }}
<div class="label text-red-500">{{ . }}</div>
{{ end }}
</fieldset>
</div>
<button type="submit" class="btn btn-neutral">
Submit
</button>
</form>
{{ end }}

15
html/sponsor.html Normal file
View File

@@ -0,0 +1,15 @@
<div class="card border border-base-200 p-4 flex flex-col gap-2">
<div class="text-center text-xs font-semibold text-neutral-400 uppercase tracking-widest">
Sponsors
</div>
<div class="flex lg:flex-col gap-6 overflow-x-auto">
{{ range .Sponsors }}
<a href="{{ .Url }}" target="_blank" rel="noopener noreferrer">
<div class="text-center flex flex-col shrink-0">
<img src="{{ .Image }}" alt="{{ .Name }}" class="max-h-[30px] w-auto" />
<div class="text-sm truncate">{{ .Name }}</div>
</div>
</a>
{{ end }}
</div>
</div>