Git Repository
Stackyard
Stackyard scannt lokale Entwicklungsordner und bündelt Projekte, Git-Status, TODOs, Toolchains, Docker-Compose-Setups, Datei- und Launcher-Funktionen sowie Operations-Informationen in einer lokalen WebUI.
HTTPS
https://zanvex.de/git/stackyard.gitinternal/web/operations_template.go
Zum Verzeichnispackage web
import "html/template"
var operationsTemplate = template.Must(template.New("operations").Parse(`<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Stackyard Operations</title><script src="https://unpkg.com/htmx.org@1.9.12"></script><style>
:root{color-scheme:dark;--bg:#080d18;--panel:#111827;--line:#27344a;--text:#e7eef9;--muted:#91a0b8;--accent:#0e7490}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 10% 0,#17294a,transparent 35rem),var(--bg);color:var(--text);font:14px/1.5 "Segoe UI",system-ui,sans-serif}a{color:#7dd3fc;text-decoration:none}.shell{max-width:1500px;margin:auto;padding:1.4rem}.top{display:flex;justify-content:space-between;gap:1rem;align-items:center}.top h1{margin:.1rem 0}.top p,.muted{color:var(--muted)}.nav{display:flex;gap:.5rem}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;margin-top:1rem}.card{background:rgba(17,24,39,.94);border:1px solid var(--line);border-radius:.85rem;padding:1rem;min-width:0}.wide{grid-column:1/-1}.card h2{margin:0 0 .8rem;font-size:1rem}.list{list-style:none;margin:0;padding:0;max-height:28rem;overflow:auto}.list li{padding:.55rem 0;border-bottom:1px solid var(--line)}.row,.inline{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.row{justify-content:space-between}.pill{display:inline-flex;padding:.15rem .45rem;border:1px solid var(--line);border-radius:999px;font-size:.75rem}.ok{color:#bbf7d0;border-color:#166534}.warn{color:#fde68a;border-color:#854d0e}.error{color:#fecaca;border-color:#991b1b}button,.button,input,select{font:inherit;border:1px solid var(--line);border-radius:.5rem;padding:.5rem .65rem;background:#0b1220;color:var(--text)}button,.button{background:var(--accent);border-color:#155e75;cursor:pointer}.secondary{background:#111827;border-color:var(--line)}input,select{min-width:10rem}.flash{padding:.7rem 1rem;border:1px solid;border-radius:.6rem;margin-top:1rem}.flash-success{color:#bbf7d0;background:#052e16}.flash-error{color:#fecaca;background:#450a0a}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:.55rem;border-bottom:1px solid var(--line);vertical-align:top}th{color:var(--muted);font-size:.75rem;text-transform:uppercase}.empty{color:var(--muted)}pre{white-space:pre-wrap;max-height:12rem;overflow:auto}@media(max-width:900px){.grid{grid-template-columns:1fr}.wide{grid-column:auto}.top{display:block}.nav{margin-top:.8rem}}
</style></head><body><main class="shell"><header class="top"><div><div class="muted">Local Dev Control Center</div><h1>Operations</h1><p>Dienste, Aufgaben, Git-Zustand und Workspace-Verwaltung.</p></div><nav class="nav"><a class="button secondary" href="/">Dashboard</a><a class="button" href="/operations/backup">Backup exportieren</a><button id="notifications" class="secondary" type="button">Benachrichtigungen</button></nav></header>{{if .Flash.Text}}<div class="flash flash-{{.Flash.Kind}}">{{.Flash.Text}}</div>{{end}}<div class="grid">
<section id="service-health" class="card wide" hx-get="/operations/health" hx-trigger="every {{.HealthRefreshSeconds}}s" hx-swap="outerHTML"><h2>Dienste und Ports <span class="muted">· alle {{.HealthRefreshSeconds}}s</span></h2><table><thead><tr><th>Projekt</th><th>Dienst</th><th>Port</th><th>TCP</th><th>HTTP</th><th>Öffnen</th></tr></thead><tbody>{{range .Ports}}<tr><td><a href="/projects/{{.ProjectID}}">{{.ProjectName}}</a></td><td>{{.Name}}</td><td>{{.Port}}</td><td>{{if .Reachable}}<span class="pill ok">Erreichbar</span>{{else}}<span class="pill warn">Frei/Offline</span>{{end}}</td><td>{{if .Healthy}}<span class="pill ok">Healthy</span>{{else}}<span class="muted">{{.Detail}}</span>{{end}}</td><td>{{if .URL}}<a href="{{.URL}}" target="_blank" rel="noreferrer">Browser ↗</a>{{end}}</td></tr>{{else}}<tr><td colspan="6" class="empty">Keine Ports erkannt oder konfiguriert.</td></tr>{{end}}</tbody></table></section>
<section class="card"><h2>Globales TODO-Board</h2><ul class="list">{{range .Todos}}<li class="row"><span>{{if .Done}}☑{{else}}☐{{end}} <a href="/projects/{{.ProjectID}}">{{.ProjectName}}</a> · {{.Text}}</span><span class="muted">Zeile {{.Line}}</span></li>{{else}}<li class="empty">Keine Markdown-Checkboxen gefunden.</li>{{end}}</ul></section>
<section class="card"><h2>Git-Arbeitszentrale</h2><ul class="list">{{range .Projects}}{{if .Git.IsRepo}}<li class="row"><span><a href="/projects/{{.ID}}">{{.Name}}</a> · {{.Git.Branch}}</span><span>{{if .Git.Dirty}}<span class="pill warn">dirty</span>{{else}}<span class="pill ok">clean</span>{{end}} {{if .Git.Ahead}}↑{{.Git.Ahead}}{{end}} {{if .Git.Behind}}↓{{.Git.Behind}}{{end}}</span></li>{{end}}{{else}}<li class="empty">Keine Repositories.</li>{{end}}</ul></section>
<section class="card"><h2>Workspace-Snapshots</h2><form class="inline" method="post" action="/operations/snapshots/create"><input name="name" maxlength="80" required placeholder="Snapshot-Name"><button>Aktuellen Workspace sichern</button></form><ul class="list">{{range .Snapshots}}<li class="row"><span>{{.Name}} <span class="muted">· {{len .ProjectIDs}} Projekte</span></span><span><a class="button secondary" href="/?snapshot={{.ID}}">Öffnen</a></span><form method="post" action="/operations/snapshots/delete"><input type="hidden" name="id" value="{{.ID}}"><button class="secondary">Entfernen</button></form></li>{{else}}<li class="empty">Noch keine Snapshots.</li>{{end}}</ul></section>
<section class="card"><h2>Scanner-Steuerung</h2><p><a class="button secondary" href="/operations/scanner/preview">Scan-Vorschau öffnen</a></p><form class="inline" method="post" action="/operations/scanner/set"><input name="path" required placeholder="Absoluter Projektpfad"><select name="kind"><option value="manual">Manuell aufnehmen</option><option value="ignore">Ignorieren</option></select><button>Regel speichern</button></form><ul class="list">{{range .ScannerRules}}<li class="row"><span><span class="pill">{{.Kind}}</span> {{.Path}}</span><form method="post" action="/operations/scanner/delete"><input type="hidden" name="path" value="{{.Path}}"><button class="secondary">Entfernen</button></form></li>{{else}}<li class="empty">Keine manuellen Regeln.</li>{{end}}</ul></section>
<section class="card"><h2>Projektabhängigkeiten</h2><form class="inline" method="post" action="/operations/dependencies/set"><select name="project_id">{{range .Projects}}<option value="{{.ID}}">{{.Name}}</option>{{end}}</select><span>benötigt</span><select name="depends_on">{{range .Projects}}<option value="{{.ID}}">{{.Name}}</option>{{end}}</select><button>Verknüpfen</button></form><ul class="list">{{range .Dependencies}}<li class="row"><span>{{index $.ProjectNames .ProjectID}} → {{index $.ProjectNames .DependsOnProjectID}}</span><form method="post" action="/operations/dependencies/delete"><input type="hidden" name="project_id" value="{{.ProjectID}}"><input type="hidden" name="depends_on" value="{{.DependsOnProjectID}}"><button class="secondary">Entfernen</button></form></li>{{else}}<li class="empty">Keine Abhängigkeiten hinterlegt.</li>{{end}}</ul></section>
<section class="card wide"><h2>Deklarative Projektprofile und Aktionen</h2><table><thead><tr><th>Projekt</th><th>Provider</th><th>Services</th><th>Sichere Aktionen</th></tr></thead><tbody>{{range .Projects}}{{$project := .}}{{$manifest := index $.Manifests .ID}}<tr><td><a href="/projects/{{.ID}}">{{.Name}}</a></td><td>{{if $manifest.Provider}}<span class="pill">{{$manifest.Provider}}</span>{{else}}<span class="muted">auto</span>{{end}}</td><td>{{len $manifest.Services}}</td><td>{{range $manifest.Actions}}<div class="inline"><strong>{{.Name}}</strong>{{if eq .Kind "go-run"}}<form method="post" action="/operations/actions/start"><input type="hidden" name="project_id" value="{{$project.ID}}"><input type="hidden" name="kind" value="{{.Kind}}"><button>Start</button></form><form method="post" action="/operations/actions/stop"><input type="hidden" name="project_id" value="{{$project.ID}}"><input type="hidden" name="kind" value="{{.Kind}}"><button class="secondary">Stop</button></form>{{else}}<form method="post" action="/operations/actions/run" onsubmit="return confirm('Deklarierte Aktion {{.Name}} starten?')"><input type="hidden" name="project_id" value="{{$project.ID}}"><input type="hidden" name="kind" value="{{.Kind}}"><button>Ausführen</button></form>{{end}}</div>{{if eq .Kind "go-run"}}<div hx-get="/operations/actions/status?project_id={{$project.ID}}&kind={{.Kind}}" hx-trigger="load, every 2s" hx-swap="innerHTML"><span class="muted">Status wird geladen …</span></div>{{end}}{{else}}<span class="muted">Keine .stackyard/project.yaml</span>{{end}}</td></tr>{{end}}</tbody></table><p class="muted">Erlaubte Aktionstypen: go-test, go-build und go-run. Freie Shell-Kommandos werden nicht akzeptiert.</p></section>
<section class="card"><h2>Backup importieren</h2><form method="post" enctype="multipart/form-data" action="/operations/backup/import"><input type="file" name="backup" accept="application/json,.json" required><button>Import prüfen</button></form><p class="muted">Vor dem Import erscheint eine Vorschau. Projektdateien werden nicht überschrieben.</p></section><section class="card wide"><h2>Ereignisse und lokale Benachrichtigungen</h2><ul class="list">{{range .Events}}<li><span class="pill {{.Level}}">{{.Level}}</span> {{.Message}} <span class="muted">· {{.CreatedAt.Format "02.01.2006 15:04"}}</span></li>{{else}}<li class="empty">Noch keine Ereignisse.</li>{{end}}</ul></section>
</div></main><script>document.getElementById('notifications')?.addEventListener('click',async()=>{if(!('Notification' in window))return alert('Benachrichtigungen werden nicht unterstützt.');const permission=await Notification.requestPermission();if(permission==='granted')new Notification('Stackyard',{body:'Lokale Benachrichtigungen sind aktiviert.'})});</script></body></html>`))
var actionStatusTemplate = template.Must(template.New("action-status").Parse(`
<div class="row">
<span>{{if .Running}}<span class="pill ok">Läuft · PID {{.PID}}</span>{{else if .StartedAt.IsZero}}<span class="pill">Nicht gestartet</span>{{else}}<span class="pill {{if .Error}}error{{end}}">Beendet</span>{{end}}</span>
{{if .Error}}<span class="error">{{.Error}}</span>{{end}}
</div>
{{if .Log}}<pre>{{.Log}}</pre>{{end}}`))
var healthPanelTemplate = template.Must(template.New("health-panel").Parse(`
<section id="service-health" class="card wide" hx-get="/operations/health" hx-trigger="every {{.RefreshSeconds}}s" hx-swap="outerHTML">
<h2>Dienste und Ports <span class="muted">· alle {{.RefreshSeconds}}s</span></h2>
<table><thead><tr><th>Projekt</th><th>Dienst</th><th>Port</th><th>TCP</th><th>HTTP</th><th>Öffnen</th></tr></thead><tbody>
{{range .Ports}}<tr><td><a href="/projects/{{.ProjectID}}">{{.ProjectName}}</a></td><td>{{.Name}}</td><td>{{.Port}}</td><td>{{if .Reachable}}<span class="pill ok">Erreichbar</span>{{else}}<span class="pill warn">Frei/Offline</span>{{end}}</td><td>{{if .Healthy}}<span class="pill ok">Healthy</span>{{else}}<span class="muted">{{.Detail}}</span>{{end}}</td><td>{{if .URL}}<a href="{{.URL}}" target="_blank" rel="noreferrer">Browser ↗</a>{{end}}</td></tr>
{{else}}<tr><td colspan="6" class="empty">Keine Ports erkannt oder konfiguriert.</td></tr>{{end}}
</tbody></table>
</section>`))