wp-packages/internal/http/templates/status.html
Ben Word 154cb2f376
Migrate templates from html/template to Jet v6
Replace Go's html/template with CloudyKit/jet/v6 for server-rendered
templates. Jet provides first-class parameterized blocks, template
inheritance via extends/yield, and C-like expressions — eliminating
the dict helper workarounds that html/template required for reusable
components.

Add components.html with four reusable Jet blocks:
- shell(command, variant, class) — copyable command boxes (replaces
  ~16 duplicated instances across 6 templates)
- notice(variant, class) with yield content — alert boxes with
  variant-based styling and content slots
- input(icon, name, ...) — form inputs with icon and keyboard hint
- pagination(pager, class) — pagination controls driven by
  pre-computed buildPagination() data

Key changes:
- templates.go: embed.FS loader for Jet, function registration via
  AddGlobal, buildPagination() helper, render() using VarMap
- handlers.go: simplified render calls (no more template combinations)
- router.go: *jet.Set replaces *templateSet
- All 13 templates converted to Jet syntax (extends/block/yield)
- Tailwind scanning unaffected (templates remain .html files)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:05:25 -05:00

182 lines
12 KiB
HTML

{{extends "layout.html"}}
{{block title()}}Status — WP Packages{{end}}
{{block meta_seo()}}
<meta name="description" content="WP Packages build and status check activity.">
{{if AppURL}}<link rel="canonical" href="{{AppURL}}/status">{{end}}
{{end}}
{{block json_ld()}}{{end}}
{{block og_meta()}}
<meta property="og:title" content="Status — WP Packages">
<meta property="og:description" content="WP Packages build and status check activity.">
{{if AppURL}}<meta property="og:url" content="{{AppURL}}/status">{{end}}
<meta property="og:type" content="website">
{{end}}
{{block body()}}
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-10">
<h1 class="text-4xl font-bold tracking-tight text-gray-900 mb-6">Status</h1>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-10">
<div class="rounded-lg border border-gray-200 px-5 py-4">
<p class="text-xs font-medium text-gray-400 uppercase tracking-wide">Packages</p>
<p class="text-2xl font-bold mt-1">{{formatNumberComma(Stats.TotalPackages)}}</p>
<p class="text-xs text-gray-500 mt-0.5">{{formatNumberComma(Stats.ActivePlugins)}} plugins · {{formatNumberComma(Stats.ActiveThemes)}} themes</p>
</div>
<div class="rounded-lg border border-gray-200 px-5 py-4">
<p class="text-xs font-medium text-gray-400 uppercase tracking-wide">Composer Installs</p>
<p class="text-2xl font-bold mt-1">{{formatNumberComma(Stats.TotalInstalls)}}</p>
<p class="text-xs text-gray-500 mt-0.5">{{formatNumberComma(Stats.Installs30d)}} in last 30d{{if Stats.StatsUpdatedAt}} · updated <span title="{{Stats.StatsUpdatedAt}}" data-time="{{Stats.StatsUpdatedAt}}">{{formatCST(Stats.StatsUpdatedAt)}}</span>{{end}}</p>
</div>
<div class="rounded-lg border border-gray-200 px-5 py-4">
<p class="text-xs font-medium text-gray-400 uppercase tracking-wide">Activity (24h)</p>
<p class="text-2xl font-bold mt-1">{{formatNumberComma(PackagesUpdated24h)}} <span class="text-sm font-normal text-gray-500">updated</span></p>
<p class="text-xs text-gray-500 mt-0.5">{{Deactivated24h}} deactivated · {{Reactivated24h}} reactivated</p>
</div>
</div>
<div class="flex border-b border-gray-200 mb-6" role="tablist">
<button class="px-4 pb-3 text-sm font-medium cursor-pointer border-b-2 border-brand-primary text-brand-primary" id="tab-builds" role="tab" aria-selected="true" aria-controls="panel-builds" onclick="switchTab('builds')">Builds</button>
<button class="px-4 pb-3 text-sm font-medium cursor-pointer border-b-2 border-transparent text-gray-500 hover:text-gray-700" id="tab-checks" role="tab" aria-selected="false" aria-controls="panel-checks" onclick="switchTab('checks')">Status Checks</button>
</div>
<script>
function switchTab(t){
var tabs={builds:['tab-builds','panel-builds'],checks:['tab-checks','panel-checks']};
for(var k in tabs){
var btn=document.getElementById(tabs[k][0]),panel=document.getElementById(tabs[k][1]);
if(k===t){panel.classList.remove('hidden');btn.classList.add('border-brand-primary','text-brand-primary');btn.classList.remove('border-transparent','text-gray-500');btn.setAttribute('aria-selected','true')}
else{panel.classList.add('hidden');btn.classList.remove('border-brand-primary','text-brand-primary');btn.classList.add('border-transparent','text-gray-500');btn.setAttribute('aria-selected','false')}
}
history.replaceState(null,'',t==='builds'?'/status':'/status?tab='+t);
}
</script>
<div id="panel-builds" role="tabpanel" aria-labelledby="tab-builds">
{{if LastBuildStartedAt}}<p class="text-sm text-gray-500 mb-4">Builds run every 5 minutes. Last build: {{timeAgo(LastBuildStartedAt)}}</p>{{end}}
{{if !Builds}}
<p class="text-gray-500 text-sm">No builds found.</p>
{{else}}
<div class="bg-white rounded-lg border border-gray-200 overflow-x-auto">
<table class="w-full text-sm min-w-[700px]">
<thead class="bg-gray-50 text-left">
<tr>
<th class="px-4 py-2 font-medium">Build ID</th>
<th class="px-4 py-2 font-medium">Started</th>
<th class="px-4 py-2 font-medium text-right">Packages</th>
<th class="px-4 py-2 font-medium text-right">Changed</th>
<th class="px-4 py-2 font-medium text-right">Artifacts</th>
<th class="px-4 py-2 font-medium">Status</th>
<th class="px-4 py-2 font-medium">Duration</th>
<th class="px-4 py-2 font-medium">R2</th>
</tr>
</thead>
<tbody>
{{range _, build := Builds}}
<tr class="border-t border-gray-100 hover:bg-gray-50{{if build.Changes}} cursor-pointer{{end}}" {{if build.Changes}}tabindex="0" aria-expanded="false" onclick="var r=this.nextElementSibling;r.classList.toggle('hidden');this.setAttribute('aria-expanded',!r.classList.contains('hidden'))" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();this.click()}"{{end}}>
<td class="px-4 py-2 font-mono text-xs">{{build.ID}}</td>
<td class="px-4 py-2 text-xs text-gray-500" title="{{build.StartedAt}}" data-time="{{build.StartedAt}}">{{formatCST(build.StartedAt)}}</td>
<td class="px-4 py-2 text-right">{{if build.Status == "completed"}}{{build.PackagesTotal}}{{else if build.Status == "running"}}<span class="text-gray-300"></span>{{end}}</td>
<td class="px-4 py-2 text-right">{{if build.Status == "completed"}}{{build.PackagesChanged}}{{else if build.Status == "running"}}<span class="text-gray-300"></span>{{end}}</td>
<td class="px-4 py-2 text-right">{{if build.Status == "completed"}}{{build.ArtifactCount}}{{else if build.Status == "running"}}<span class="text-gray-300"></span>{{end}}</td>
<td class="px-4 py-2">
{{- if build.IsCurrent}}<span class="text-xs lowercase bg-green-100 text-green-700 px-2 py-0.5 rounded-full">Current</span>
{{- else if build.Status == "running"}}<span class="text-xs lowercase bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full animate-pulse">Running</span>
{{- else if build.Status == "cancelled"}}<span class="text-xs lowercase bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full">Cancelled</span>
{{- else if build.Status == "failed"}}<span class="text-xs lowercase bg-red-100 text-red-700 px-2 py-0.5 rounded-full" title="{{build.ErrorMessage}}">Failed</span>
{{- else}}<span class="text-xs lowercase bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">{{build.Status}}</span>{{end}}
</td>
<td class="px-4 py-2 text-xs font-mono whitespace-nowrap">
{{if build.DurationSeconds}}<span class="text-gray-700">{{formatDuration(build.DurationSeconds)}}</span>{{end}}
{{if build.DiscoverSeconds || build.UpdateSeconds || build.BuildSeconds || build.DeploySeconds || build.R2UploadSeconds}}
<span class="text-gray-400 block mt-0.5">{{- if build.DiscoverSeconds}}<span title="Discover">D:{{formatDuration(build.DiscoverSeconds)}}</span> {{end}}{{- if build.UpdateSeconds}}<span title="Update">U:{{formatDuration(build.UpdateSeconds)}}</span> {{end}}{{- if build.BuildSeconds}}<span title="Build">B:{{formatDuration(build.BuildSeconds)}}</span> {{end}}{{- if build.DeploySeconds}}<span title="Deploy">Dp:{{formatDuration(build.DeploySeconds)}}</span> {{end}}{{- if build.R2UploadSeconds}}<span title="R2 Upload">R2:{{formatDuration(build.R2UploadSeconds)}}</span>{{end}}</span>
{{end}}
</td>
<td class="px-4 py-2">
{{if build.R2SyncedAt}}<span class="text-xs lowercase bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full" title="Synced {{build.R2SyncedAt}}">Synced</span>
{{else}}<span class="text-xs text-gray-300"></span>{{end}}
</td>
</tr>
{{if build.Changes}}
<tr class="hidden">
<td colspan="8" class="p-0 bg-gray-50">
<table class="text-xs">
{{range _, change := build.Changes}}
<tr>
<td class="pl-6 pr-2 py-0.5"><span class="inline-block px-2 py-0.5 rounded-full {{if change.Action == "delete"}}bg-red-100 text-red-700{{else}}bg-blue-100 text-blue-700{{end}}">{{if change.Action == "delete"}}deleted{{else}}updated{{end}}</span></td>
<td class="pr-2 py-0.5">{{if change.Action == "delete"}}<span class="font-mono text-gray-700">{{change.PackageName}}</span>{{else}}<a href="/packages/{{change.PackageName}}" class="font-mono text-gray-700 hover:text-blue-600 hover:underline">{{change.PackageName}}</a>{{end}}</td>
<td class="pr-6 py-0.5">{{if change.Action == "delete"}}<a href="{{wporgURL(change.PackageName)}}" class="text-gray-400 hover:text-blue-600 hover:underline" rel="nofollow">View on WordPress.org</a>{{end}}</td>
</tr>
{{end}}
</table>
</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
<div id="panel-checks" class="hidden" role="tabpanel" aria-labelledby="tab-checks">
<p class="text-sm text-gray-500 mb-4">Hourly re-check of all packages against WordPress.org to detect closures and re-openings.</p>
{{if !StatusChecks}}
<p class="text-gray-500 text-sm">No status checks have run yet.</p>
{{else}}
<div class="bg-white rounded-lg border border-gray-200 overflow-x-auto">
<table class="w-full text-sm min-w-[600px]">
<thead class="bg-gray-50 text-left">
<tr>
<th class="px-4 py-2 font-medium">Started</th>
<th class="px-4 py-2 font-medium text-right">Checked</th>
<th class="px-4 py-2 font-medium text-right">Deactivated</th>
<th class="px-4 py-2 font-medium text-right">Reactivated</th>
<th class="px-4 py-2 font-medium">Status</th>
<th class="px-4 py-2 font-medium">Duration</th>
</tr>
</thead>
<tbody>
{{range _, check := StatusChecks}}
<tr class="border-t border-gray-100 hover:bg-gray-50{{if check.Changes}} cursor-pointer{{end}}" {{if check.Changes}}tabindex="0" aria-expanded="false" onclick="var r=this.nextElementSibling;r.classList.toggle('hidden');this.setAttribute('aria-expanded',!r.classList.contains('hidden'))" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();this.click()}"{{end}}>
<td class="px-4 py-2 text-xs text-gray-500" title="{{check.StartedAt}}" data-time="{{check.StartedAt}}">{{formatCST(check.StartedAt)}}</td>
<td class="px-4 py-2 text-right">{{if check.Status == "completed" || check.Status == "completed_with_errors"}}{{check.Checked}}{{else if check.Status == "running"}}<span class="text-gray-300"></span>{{end}}</td>
<td class="px-4 py-2 text-right">{{if check.Status == "completed" || check.Status == "completed_with_errors"}}{{if check.Deactivated}}<span class="text-red-600">{{check.Deactivated}}</span>{{else}}0{{end}}{{else if check.Status == "running"}}<span class="text-gray-300"></span>{{end}}</td>
<td class="px-4 py-2 text-right">{{if check.Status == "completed" || check.Status == "completed_with_errors"}}{{if check.Reactivated}}<span class="text-green-600">{{check.Reactivated}}</span>{{else}}0{{end}}{{else if check.Status == "running"}}<span class="text-gray-300"></span>{{end}}</td>
<td class="px-4 py-2">
{{- if check.Status == "running"}}<span class="text-xs lowercase bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full animate-pulse">Running</span>
{{- else if check.Status == "completed" || check.Status == "completed_with_errors"}}<span class="text-xs lowercase bg-green-100 text-green-700 px-2 py-0.5 rounded-full">Completed</span>
{{- else if check.Status == "failed"}}<span class="text-xs lowercase bg-red-100 text-red-700 px-2 py-0.5 rounded-full" title="{{check.ErrorMessage}}">Failed</span>
{{- else}}<span class="text-xs lowercase bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">{{check.Status}}</span>{{end}}
</td>
<td class="px-4 py-2 text-xs font-mono">{{if check.DurationSeconds}}{{formatDuration(check.DurationSeconds)}}{{end}}</td>
</tr>
{{if check.Changes}}
<tr class="hidden">
<td colspan="6" class="p-0 bg-gray-50">
<table class="text-xs">
{{range _, change := check.Changes}}
<tr>
<td class="pl-6 pr-2 py-0.5"><span class="inline-block px-2 py-0.5 rounded-full {{if change.Action == "deactivated"}}bg-red-100 text-red-700{{else}}bg-green-100 text-green-700{{end}}">{{change.Action}}</span></td>
<td class="pr-2 py-0.5">{{if change.Action == "deactivated"}}<span class="font-mono text-gray-700">wp-{{change.PackageType}}/{{change.PackageName}}</span>{{else}}<a href="/packages/wp-{{change.PackageType}}/{{change.PackageName}}" class="font-mono text-gray-700 hover:text-blue-600 hover:underline">wp-{{change.PackageType}}/{{change.PackageName}}</a>{{end}}</td>
<td class="pr-6 py-0.5">{{if change.Action == "deactivated"}}<a href="{{wporgURL("wp-" + change.PackageType + "/" + change.PackageName)}}" class="text-gray-400 hover:text-blue-600 hover:underline" rel="nofollow">View on WordPress.org</a>{{end}}</td>
</tr>
{{end}}
</table>
</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
<script>
document.querySelectorAll('[data-time]').forEach(function(el){
var raw=el.dataset.time;
var d=new Date(raw.indexOf('T')>-1||raw.indexOf('Z')>-1?raw:raw.replace(' ','T')+'Z');
if(isNaN(d))return;
el.textContent=d.toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});
});
var t=new URLSearchParams(location.search).get('tab');
if(t==='checks')switchTab('checks');
</script>
</div>
{{end}}