mirror of
https://github.com/roots/wp-packages
synced 2026-07-22 02:10:54 +08:00
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>
182 lines
12 KiB
HTML
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}}
|