package engineeringdashboard import ( "encoding/json" "fmt" "os" "path/filepath" "strings" ) // ExportManifest describes what was embedded in an offline HTML. type ExportManifest struct { Included []string `json:"included"` Truncated []string `json:"truncated"` MetadataOnly []string `json:"metadata_only"` SensitiveWarn []string `json:"sensitive_warnings,omitempty"` } // ExportHTML writes a self-contained offline snapshot (mode 0600). func ExportHTML(snap Snapshot, outputPath string) error { css, err := readAsset("assets/app.css") if err != nil { return err } manifest := ExportManifest{} docs := make([]DocumentNode, 0, len(snap.Documents)) for _, d := range snap.Documents { cp := d if IsAssetsPath(cp.Path) { cp.Body = "" manifest.MetadataOnly = append(manifest.MetadataOnly, cp.Path) } else if cp.BodyTruncated { manifest.Truncated = append(manifest.Truncated, cp.Path) manifest.Included = append(manifest.Included, cp.Path) } else if cp.Body != "" { manifest.Included = append(manifest.Included, cp.Path) } else { manifest.MetadataOnly = append(manifest.MetadataOnly, cp.Path) } // strip any accidental absolute secret-looking paths from body display only — keep relative if strings.Contains(strings.ToLower(cp.Body), "/.secrets/") { manifest.SensitiveWarn = append(manifest.SensitiveWarn, cp.Path+": contains .secrets path mention") } docs = append(docs, cp) } exportSnap := snap exportSnap.Documents = docs payload, err := MarshalSnapshotJSON(exportSnap) if err != nil { return err } // HTML-safe JSON: break sequences safeJSON := strings.ReplaceAll(string(payload), "<", "\\u003c") safeJSON = strings.ReplaceAll(safeJSON, ">", "\\u003e") safeJSON = strings.ReplaceAll(safeJSON, "&", "\\u0026") safeJSON = strings.ReplaceAll(safeJSON, " 工程中控台快照 %s

个人工程中控台(离线快照)

repo=%s · HEAD=%s · generation=%s · revision=%s · generated_at=%s

覆盖清单


  
`, snap.RevisionID, string(css), snap.Repository.Root, snap.Repository.Head, snap.GenerationID, snap.RevisionID, snap.GeneratedAt.Format(timeRFC3339), safeJSON, manSafe) dir := filepath.Dir(outputPath) if err := os.MkdirAll(dir, 0o755); err != nil { return err } tmp, err := os.CreateTemp(dir, ".engdash-*.html") if err != nil { return err } tmpName := tmp.Name() if _, err := tmp.Write([]byte(html)); err != nil { tmp.Close() _ = os.Remove(tmpName) return err } if err := tmp.Chmod(0o600); err != nil { tmp.Close() _ = os.Remove(tmpName) return err } if err := tmp.Close(); err != nil { _ = os.Remove(tmpName) return err } if err := os.Rename(tmpName, outputPath); err != nil { _ = os.Remove(tmpName) return err } return nil } const timeRFC3339 = "2006-01-02T15:04:05Z07:00"