Commit 15b8e8f

Eric Bower  ·  2026-05-08 09:46:26 -0400 EDT
parent 595103a
chore: html artifact work
1 files changed,  +110, -19
+110, -19
......@@ -1030,7 +1030,7 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[str
10301030 }
10311031
10321032 // Generate and stage job index landing pages
1033- indexHTML, indexTXT := generateJobIndex(name, jobID, group)
1033+ indexHTML, indexTXT := generateJobIndex(cfg.ArtifactDir, name, jobID, group)
10341034 if err := stageArtifact(cfg.ArtifactDir, name, jobID, "index", indexHTML, ".html"); err != nil {
10351035 log.Error("stage index.html", "err", err)
10361036 }
......@@ -1380,7 +1380,7 @@ const sessionHTMLTemplate = `<!DOCTYPE html>
13801380 <span>Duration: {{.Duration}}</span>
13811381 {{if .ExitCode}}<span>Exit: {{.ExitCode}}</span>{{end}}
13821382 </div>
1383-<pre><code>{{.Content}}</code></pre>
1383+<div class="zmx-output">{{.Content}}</div>
13841384 <div class="links">
13851385 <a href="index.html">&larr; Back to job</a>
13861386 </div>
......@@ -1396,7 +1396,7 @@ type SessionArtifactData struct {
13961396 JobID string
13971397 Duration string
13981398 ExitCode string
1399- Content string
1399+ Content template.HTML
14001400 }
14011401
14021402 // fetchHistoryHTML fetches session history from zmx and wraps it in a full HTML document.
......@@ -1419,7 +1419,7 @@ func fetchHistoryHTML(sessionName, jobName, jobID, status, duration, exitCode st
14191419 JobID: jobID,
14201420 Duration: duration,
14211421 ExitCode: exitCode,
1422- Content: string(output),
1422+ Content: template.HTML(string(output)),
14231423 }
14241424
14251425 tmpl, err := template.New("session").Parse(sessionHTMLTemplate)
......@@ -1490,11 +1490,14 @@ func syncArtifacts(cfg *Cfg, log *slog.Logger) error {
14901490 continue
14911491 }
14921492 log.Debug("syncing artifacts", "repo", entry.Name(), "job_id", repoEntry.Name(), "dest", event.ArtifactDest)
1493- sshArgs := fmt.Sprintf(
1494- "-F ~/.ssh/config -i %s -o IdentitiesOnly=yes -o CertificateFile %s",
1495- cfg.KeyLocation,
1496- cfg.CertificateLocation,
1497- )
1493+ var sshArgs string
1494+ if cfg.KeyLocation != "" {
1495+ certFile := ""
1496+ if cfg.CertificateLocation != "" {
1497+ certFile = fmt.Sprintf(" -o CertificateFile %s", cfg.CertificateLocation)
1498+ }
1499+ sshArgs = fmt.Sprintf("-F ~/.ssh/config -i %s%s", cfg.KeyLocation, certFile)
1500+ }
14981501 srcDir := filepath.Join(cfg.ArtifactDir, entry.Name(), repoEntry.Name())
14991502 // Append "/" so rsync copies into the destination directory,
15001503 // not as a subdirectory named after the source.
......@@ -1502,9 +1505,18 @@ func syncArtifacts(cfg *Cfg, log *slog.Logger) error {
15021505 if !strings.HasSuffix(dest, "/") {
15031506 dest += "/"
15041507 }
1505- rsyncCmd := fmt.Sprintf("rsync -e '%s' -rv %s/ %s", sshArgs, srcDir, dest)
1508+
1509+ // Build rsync command, omitting -e if no ssh args
1510+ var cmd *exec.Cmd
1511+ if sshArgs != "" {
1512+ cmd = exec.Command("rsync", "-e", sshArgs, "-rv", srcDir+"/", dest)
1513+ } else {
1514+ cmd = exec.Command("rsync", "-rv", srcDir+"/", dest)
1515+ }
1516+ rsyncCmd := fmt.Sprintf("rsync %s %s %s",
1517+ strings.TrimLeft(cmd.Args[1], "-"),
1518+ srcDir+"/", dest)
15061519 log.Info("rsync", "cmd", rsyncCmd)
1507- cmd := exec.Command("rsync", "-e", sshArgs, "-rv", srcDir+"/", dest)
15081520 if err := runCmd(cmd, log); err != nil {
15091521 log.Error("sync artifacts", "repo", entry.Name(), "job_id", repoEntry.Name(), "err", err)
15101522 }
......@@ -1715,18 +1727,55 @@ type sessionRow struct {
17151727 Short string
17161728 Status string
17171729 ExitCode string
1730+ Started string
17181731 Ended string
17191732 Duration string
17201733 }
17211734
1735+// artifactRow holds display info for non-session artifacts (e.g., event.json, workspace.tar).
1736+type artifactRow struct {
1737+ Name string
1738+ Size string
1739+ ModTime string
1740+}
1741+
1742+// formatFileSize returns a human-readable file size string.
1743+func formatFileSize(bytes int64) string {
1744+ const unit = 1024
1745+ if bytes < unit {
1746+ return fmt.Sprintf("%d B", bytes)
1747+ }
1748+ div, exp := int64(unit), 0
1749+ for n := bytes / unit; n >= unit; n /= unit {
1750+ div *= unit
1751+ exp++
1752+ }
1753+ units := []string{"KB", "MB", "GB", "TB"}
1754+ return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp])
1755+}
1756+
1757+// formatTimestamp converts a unix timestamp to a human-readable format.
1758+func formatTimestamp(ts string) string {
1759+ if ts == "" {
1760+ return "—"
1761+ }
1762+ var t int64
1763+ if _, err := fmt.Sscanf(ts, "%d", &t); err != nil {
1764+ return "—"
1765+ }
1766+ return time.Unix(t, 0).UTC().Format("2006-01-02 15:04:05")
1767+}
1768+
17221769 // generateJobIndex produces HTML and plain-text index pages listing all
17231770 // sessions for a job with links and metadata (status, exit code, ended at).
1724-func generateJobIndex(name, jobID string, sessions []SessionInfo) (htmlContent, txtContent string) {
1771+// It also includes any other artifacts in the job directory (e.g., event.json, workspace.tar).
1772+func generateJobIndex(artifactDir, name, jobID string, sessions []SessionInfo) (htmlContent, txtContent string) {
17251773 rows := make([]sessionRow, 0, len(sessions))
17261774 for _, s := range sessions {
17271775 row := sessionRow{
1728- Name: s.Name,
1729- Short: s.Short,
1776+ Name: s.Name,
1777+ Short: s.Short,
1778+ Started: s.Created,
17301779 }
17311780 if s.Ended == "" {
17321781 row.Status = "running"
......@@ -1743,6 +1792,26 @@ func generateJobIndex(name, jobID string, sessions []SessionInfo) (htmlContent,
17431792 rows = append(rows, row)
17441793 }
17451794
1795+ // Gather other artifacts in the job directory
1796+ var artifacts []artifactRow
1797+ jobDir := filepath.Join(artifactDir, name, jobID)
1798+ if entries, err := os.ReadDir(jobDir); err == nil {
1799+ for _, e := range entries {
1800+ if e.IsDir() {
1801+ continue // skip subdirs (task dirs)
1802+ }
1803+ info, err := e.Info()
1804+ if err != nil {
1805+ continue
1806+ }
1807+ artifacts = append(artifacts, artifactRow{
1808+ Name: e.Name(),
1809+ Size: formatFileSize(info.Size()),
1810+ ModTime: formatTimestamp(fmt.Sprintf("%d", info.ModTime().Unix())),
1811+ })
1812+ }
1813+ }
1814+
17461815 // Resolve overall job status
17471816 jobStatus := "success"
17481817 hasRunning := false
......@@ -1760,7 +1829,9 @@ func generateJobIndex(name, jobID string, sessions []SessionInfo) (htmlContent,
17601829 }
17611830
17621831 // HTML index
1763- tmpl, err := template.New("index").Parse(indexHTMLTemplate)
1832+ tmpl, err := template.New("index").Funcs(template.FuncMap{
1833+ "formatTimestamp": formatTimestamp,
1834+ }).Parse(indexHTMLTemplate)
17641835 if err == nil {
17651836 var buf strings.Builder
17661837 if err := tmpl.Execute(&buf, struct {
......@@ -1768,7 +1839,8 @@ func generateJobIndex(name, jobID string, sessions []SessionInfo) (htmlContent,
17681839 JobID string
17691840 JobStatus string
17701841 Rows []sessionRow
1771- }{Name: name, JobID: jobID, JobStatus: jobStatus, Rows: rows}); err == nil {
1842+ Artifacts []artifactRow
1843+ }{Name: name, JobID: jobID, JobStatus: jobStatus, Rows: rows, Artifacts: artifacts}); err == nil {
17721844 htmlContent = buf.String()
17731845 }
17741846 }
......@@ -1814,10 +1886,10 @@ const indexHTMLTemplate = `<!DOCTYPE html>
18141886 </head>
18151887 <body>
18161888 <h1>{{.Name}} <span class="pill pill-{{.JobStatus}}">{{.JobStatus}}</span></h1>
1817-<p class="meta">Job ID: {{.JobID}} &middot; {{len .Rows}} session(s)</p>
1889+<p class="meta">Job ID: {{.JobID}} &middot; {{len .Rows}} task(s)</p>
18181890 <table>
18191891 <thead>
1820-<tr><th>Session</th><th>Status</th><th>Exit Code</th><th>Duration</th><th>Ended</th><th>Artifacts</th></tr>
1892+<tr><th>Task</th><th>Status</th><th>Exit Code</th><th>Duration</th><th>Started</th><th>Ended</th><th>Artifacts</th></tr>
18211893 </thead>
18221894 <tbody>
18231895 {{range .Rows}}
......@@ -1826,12 +1898,31 @@ const indexHTMLTemplate = `<!DOCTYPE html>
18261898 <td><span class="pill pill-{{.Status}}">{{.Status}}</span></td>
18271899 <td>{{.ExitCode}}</td>
18281900 <td>{{.Duration}}</td>
1829- <td>{{.Ended}}</td>
1901+ <td>{{.Started | formatTimestamp}}</td>
1902+ <td>{{.Ended | formatTimestamp}}</td>
18301903 <td><a href="{{.Short}}.html">html</a> / <a href="{{.Short}}.txt">txt</a></td>
18311904 </tr>
18321905 {{end}}
18331906 </tbody>
18341907 </table>
1908+
1909+{{if .Artifacts}}
1910+<h2 style="margin-top: 2rem; font-size: 1rem; color: #555;">Other Artifacts</h2>
1911+<table>
1912+<thead>
1913+<tr><th>Name</th><th>Size</th><th>Modified</th></tr>
1914+</thead>
1915+<tbody>
1916+{{range .Artifacts}}
1917+<tr>
1918+ <td><a href="{{.Name}}">{{.Name}}</a></td>
1919+ <td>{{.Size}}</td>
1920+ <td>{{.ModTime}}</td>
1921+</tr>
1922+{{end}}
1923+</tbody>
1924+</table>
1925+{{end}}
18351926 </body>
18361927 </html>`
18371928