xetup/internal/report/report.go
X9 Dev d30767ef8b
Some checks failed
release / build-and-release (push) Failing after 32s
fix: comprehensive reliability and robustness improvements
Critical fixes:
- Fix resume mode: StepsByIDs returned Enabled=false, all resume steps
  would be SKIPPED (deployment could never resume after reboot)
- Add reboot loop protection: per-step retry counter (max 5) prevents
  infinite reboot cycles when a step always exits with code 9
- Block reboot when state.Save() fails in resumePhase (prevents state
  loss leading to full restart from scratch)
- Atomic state file write (write-to-tmp + rename) prevents JSON
  corruption on BSOD/power loss mid-write
- Script watchdog: kills scripts after 30 min of no output (resets on
  each line, so active long-running scripts are never killed)
- Fix copyFile: check Close() error explicitly instead of deferred
  close that silently drops flush errors (e.g. disk full)

High severity:
- Cleanup() now logs errors instead of silently ignoring them
- Email report: 3 retries with backoff + always saves C:\X9\report.html
- Winget parallel jobs: 10 min timeout, kill hung jobs
- UCPD stop verification: 2s wait + state check before PDF association
- Atera installer: /qn -> /qb so MFA window can appear
- GVLK activation: match by EditionID (registry, not localized) instead
  of fragile OS caption string matching

Medium severity:
- Default profile hive unload: retry loop (5 attempts, increasing delay)
- LayoutModification.xml: UTF-8 without BOM (PS 5.1 Set-Content adds BOM)
- Set-Reg SYSTEM task: try/finally ensures temp file + task cleanup
- Windows Update: @($available).Count for PS 5.1 single-result edge case
- config.json: add missing kmsServer field in activation section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 11:49:43 +02:00

161 lines
4.7 KiB
Go

// Package report sends a deployment summary email via SMTP.
package report
import (
"fmt"
"net/smtp"
"os"
"path/filepath"
"strings"
"time"
)
// SMTP2Go relay configuration for X9.cz deployment reports.
const (
smtpHost = "mail-eu.smtp2go.com"
smtpPort = "2525"
smtpUser = "xetup"
smtpPass = "M9ahxHOnJ8fM0CEF"
mailFrom = "xetup@x9.cz"
mailTo = "net@x9.cz"
)
// localReportPath is where a local HTML copy of the report is always saved.
const localReportPath = `C:\X9\report.html`
// StepResult holds one row of the deployment report.
type StepResult struct {
Num string
Name string
Status string // OK, ERROR, SKIPPED, CANCELLED
Elapsed time.Duration
}
// Send builds the deployment report, saves a local HTML copy to C:\X9\,
// and emails it via SMTP with retries. Returns the last SMTP error if all
// attempts fail (the local copy is always written regardless).
func Send(results []StepResult) error {
hostname, _ := os.Hostname()
now := time.Now().Format("2006-01-02 15:04")
subject := fmt.Sprintf("xetup report %s", hostname)
body := buildHTML(results, hostname, now)
// Always save local copy so technician has a record even if SMTP fails
_ = os.MkdirAll(filepath.Dir(localReportPath), 0755)
if err := os.WriteFile(localReportPath, []byte(body), 0644); err != nil {
fmt.Fprintf(os.Stderr, "[WARN] Failed to save local report: %v\n", err)
}
// Retry SMTP up to 3 times with exponential backoff (1s, 5s, 15s)
delays := []time.Duration{0, 1 * time.Second, 5 * time.Second}
var lastErr error
for attempt, delay := range delays {
if delay > 0 {
time.Sleep(delay)
}
if err := sendMail(subject, body); err != nil {
lastErr = err
fmt.Fprintf(os.Stderr, "[WARN] Email attempt %d/3 failed: %v\n", attempt+1, err)
continue
}
return nil
}
fmt.Fprintf(os.Stderr, "[ERROR] All email attempts failed. Local copy saved: %s\n", localReportPath)
return lastErr
}
func sendMail(subject, body string) error {
msg := strings.Join([]string{
"From: " + mailFrom,
"To: " + mailTo,
"Subject: " + subject,
"MIME-Version: 1.0",
"Content-Type: text/html; charset=UTF-8",
"",
body,
}, "\r\n")
auth := smtp.PlainAuth("", smtpUser, smtpPass, smtpHost)
return smtp.SendMail(
smtpHost+":"+smtpPort,
auth,
mailFrom,
[]string{mailTo},
[]byte(msg),
)
}
func buildHTML(results []StepResult, hostname, dateTime string) string {
var ok, errs, skipped int
var rows strings.Builder
for _, r := range results {
color := "#333"
icon := ""
switch r.Status {
case "OK":
ok++
color = "#2e7d32"
icon = "&#10003;"
case "ERROR":
errs++
color = "#c62828"
icon = "&#10007;"
default:
skipped++
color = "#9e9e9e"
icon = "&#8211;"
}
elapsed := ""
if r.Elapsed > 0 {
elapsed = r.Elapsed.Round(time.Second).String()
}
fmt.Fprintf(&rows,
`<tr><td style="padding:4px 8px;color:%s;font-size:16px;text-align:center">%s</td>`+
`<td style="padding:4px 8px;color:#666">%s</td>`+
`<td style="padding:4px 8px">%s</td>`+
`<td style="padding:4px 8px;color:%s;font-weight:bold">%s</td>`+
`<td style="padding:4px 8px;color:#999;text-align:right">%s</td></tr>`,
color, icon, r.Num, r.Name, color, r.Status, elapsed)
}
summaryColor := "#2e7d32"
summaryText := "Deployment OK"
if errs > 0 {
summaryColor = "#c62828"
summaryText = fmt.Sprintf("Deployment finished with %d error(s)", errs)
}
return fmt.Sprintf(`<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head>
<body style="font-family:Segoe UI,Arial,sans-serif;margin:0;padding:20px;background:#f5f5f5">
<div style="max-width:640px;margin:0 auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.1)">
<div style="background:#223B47;padding:20px 24px;color:#fff">
<h1 style="margin:0;font-size:20px">xetup report</h1>
<p style="margin:6px 0 0;opacity:0.8">%s &mdash; %s</p>
</div>
<table style="width:100%%;border-collapse:collapse;margin:16px 0">
<tr style="background:#f9f9f9">
<th style="padding:6px 8px;text-align:center;width:30px"></th>
<th style="padding:6px 8px;text-align:left;width:40px">Krok</th>
<th style="padding:6px 8px;text-align:left">Nazev</th>
<th style="padding:6px 8px;text-align:left;width:70px">Status</th>
<th style="padding:6px 8px;text-align:right;width:60px">Cas</th>
</tr>
%s
</table>
<div style="padding:16px 24px;background:%s;color:#fff;text-align:center;font-weight:bold">
%s &mdash; OK: %d &nbsp; CHYBY: %d &nbsp; PRESKOCENO: %d
</div>
</div>
<p style="text-align:center;color:#999;font-size:12px;margin-top:16px">
Odeslano z xetup.exe &mdash; log: C:\Windows\Setup\Scripts\Deploy.log
</p>
</body></html>`,
hostname, dateTime,
rows.String(),
summaryColor, summaryText, ok, errs, skipped)
}