tenseleyflow/shithub / 7aa789f

Browse files

web/actions: render run detail and static logs (S41f)

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
7aa789f3eff9827825f7ba7808d08bf2d5721230
Parents
ea43f03
Tree
1598afc

15 changed files

StatusFile+-
M CONTRIBUTING.md 7 0
M docs/internal/actions-runner-api.md 7 0
M internal/web/handlers/repo/actions.go 569 174
M internal/web/handlers/repo/repo.go 14 8
M internal/web/render/render.go 58 0
M internal/web/repo_wiring.go 2 0
M internal/web/server.go 1 1
M internal/web/static/css/shithub.css 154 9
A internal/web/static/vendor/htmx/LICENSE 13 0
A internal/web/static/vendor/htmx/htmx.min.js 1 0
M internal/web/templates/_layout.html 1 0
A internal/web/templates/repo/_action_run_status.html 10 0
M internal/web/templates/repo/action_run.html 65 55
A internal/web/templates/repo/action_run_status.html 3 0
A internal/web/templates/repo/action_step_log.html 55 0
CONTRIBUTING.mdmodified
@@ -76,6 +76,13 @@ and the TOTP/webhook AEAD keys.
76
 - Migrations: forward-only by convention; `down` exists for
76
 - Migrations: forward-only by convention; `down` exists for
77
   emergency rollback only and may drop data.
77
   emergency rollback only and may drop data.
78
 
78
 
79
+## Browser interactivity
80
+
81
+HTMX is for partial-render swaps where a full page reload would feel
82
+jarring. SSE is for true streaming. Do not reach for either without a
83
+specific UX reason, and keep HTMX swap targets separate from SSE-owned
84
+log panes so polling cannot replace an active stream.
85
+
79
 ## Reviewing
86
 ## Reviewing
80
 
87
 
81
 Maintainers (and other contributors with write access) review
88
 Maintainers (and other contributors with write access) review
docs/internal/actions-runner-api.mdmodified
@@ -106,6 +106,13 @@ When object storage is configured, terminal step updates enqueue
106
 `actions/runs/<run_id>/jobs/<job_id>/steps/<step_id>.log`, stores that
106
 `actions/runs/<run_id>/jobs/<job_id>/steps/<step_id>.log`, stores that
107
 key and byte count on `workflow_steps`, then deletes the SQL chunks.
107
 key and byte count on `workflow_steps`, then deletes the SQL chunks.
108
 
108
 
109
+The repository Actions UI reads logs from the same two-stage storage
110
+model. While chunks remain in SQL, a step log page concatenates them in
111
+sequence order and renders a static snapshot. After finalization, the
112
+page reads `workflow_steps.log_object_key` from object storage and
113
+offers a short-lived signed download URL. Live tailing is intentionally
114
+separate and lands in the S41f SSE slice.
115
+
109
 `POST /api/v1/jobs/{id}/status`
116
 `POST /api/v1/jobs/{id}/status`
110
 
117
 
111
 Auth: job JWT. Body:
118
 Auth: job JWT. Body:
internal/web/handlers/repo/actions.gomodified
@@ -3,8 +3,10 @@
3
 package repo
3
 package repo
4
 
4
 
5
 import (
5
 import (
6
+	"bytes"
7
+	"context"
6
 	"errors"
8
 	"errors"
7
-	"html/template"
9
+	"io"
8
 	"net/http"
10
 	"net/http"
9
 	"net/url"
11
 	"net/url"
10
 	"path"
12
 	"path"
@@ -18,11 +20,11 @@ import (
18
 
20
 
19
 	actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc"
21
 	actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc"
20
 	"github.com/tenseleyFlow/shithub/internal/auth/policy"
22
 	"github.com/tenseleyFlow/shithub/internal/auth/policy"
21
-	checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc"
23
+	"github.com/tenseleyFlow/shithub/internal/infra/storage"
22
-	"github.com/tenseleyFlow/shithub/internal/web/middleware"
23
 )
24
 )
24
 
25
 
25
 const actionsRunsPageSize = int32(20)
26
 const actionsRunsPageSize = int32(20)
27
+const actionsStepLogRenderLimit = 1 << 20
26
 
28
 
27
 type actionsWorkflowView struct {
29
 type actionsWorkflowView struct {
28
 	File   string
30
 	File   string
@@ -83,37 +85,83 @@ type actionsPaginationView struct {
83
 	ResultText string
85
 	ResultText string
84
 }
86
 }
85
 
87
 
86
-type actionsSuiteView struct {
88
+type actionsRunDetailView struct {
87
-	ID                 int64
89
+	ID             int64
88
-	AppSlug            string
90
+	RunIndex       int64
89
-	Title              string
91
+	WorkflowFile   string
90
-	HeadSha            string
92
+	WorkflowName   string
91
-	HeadShaShort       string
93
+	Title          string
92
-	PullNumber         int64
94
+	HeadSha        string
93
-	PullAuthorUsername string
95
+	HeadShaShort   string
94
-	HeadRef            string
96
+	HeadRef        string
95
-	BaseRef            string
97
+	Event          string
96
-	RunCount           int
98
+	EventLabel     string
97
-	StateText          string
99
+	ActorUsername  string
98
-	StateClass         string
100
+	StateText      string
99
-	StateIcon          string
101
+	StateClass     string
100
-	CreatedAt          time.Time
102
+	StateIcon      string
101
-	UpdatedAt          time.Time
103
+	CreatedAt      time.Time
102
-	Duration           string
104
+	UpdatedAt      time.Time
103
-	Runs               []actionsRunView
105
+	Duration       string
104
-	AnnotationCount    int
106
+	IsTerminal     bool
105
-}
107
+	StatusHref     string
106
-
108
+	ActionsHref    string
107
-type actionsRunView struct {
109
+	CodeHref       string
108
-	ID          int64
110
+	ArtifactCount  int
109
-	Name        string
111
+	JobCount       int
110
-	StateText   string
112
+	CompletedCount int
111
-	StateClass  string
113
+	FailureCount   int
112
-	StateIcon   string
114
+	Jobs           []actionsJobDetailView
113
-	Duration    string
115
+	Stages         []actionsJobStageView
114
-	CompletedAt time.Time
116
+}
115
-	DetailsURL  string
117
+
116
-	SummaryHTML template.HTML
118
+type actionsJobDetailView struct {
119
+	ID         int64
120
+	JobIndex   int32
121
+	JobKey     string
122
+	Name       string
123
+	RunsOn     string
124
+	Needs      []string
125
+	NeedsText  string
126
+	StateText  string
127
+	StateClass string
128
+	StateIcon  string
129
+	Duration   string
130
+	Anchor     string
131
+	Depth      int
132
+	Steps      []actionsStepDetailView
133
+}
134
+
135
+type actionsStepDetailView struct {
136
+	ID           int64
137
+	StepIndex    int32
138
+	StepID       string
139
+	Name         string
140
+	Kind         string
141
+	Detail       string
142
+	StateText    string
143
+	StateClass   string
144
+	StateIcon    string
145
+	Duration     string
146
+	LogByteCount int64
147
+	LogHref      string
148
+}
149
+
150
+type actionsJobStageView struct {
151
+	Index int
152
+	Jobs  []actionsJobDetailView
153
+}
154
+
155
+type actionsStepLogView struct {
156
+	Run          actionsRunDetailView
157
+	Job          actionsJobDetailView
158
+	Step         actionsStepDetailView
159
+	LogText      string
160
+	LogSource    string
161
+	LogError     string
162
+	LogTruncated bool
163
+	DownloadURL  string
164
+	BackHref     string
117
 }
165
 }
118
 
166
 
119
 func (h *Handlers) repoTabActions(w http.ResponseWriter, r *http.Request) {
167
 func (h *Handlers) repoTabActions(w http.ResponseWriter, r *http.Request) {
@@ -537,180 +585,527 @@ func (h *Handlers) repoActionRun(w http.ResponseWriter, r *http.Request) {
537
 	if !ok {
585
 	if !ok {
538
 		return
586
 		return
539
 	}
587
 	}
540
-	suiteID, err := strconv.ParseInt(chi.URLParam(r, "suiteID"), 10, 64)
588
+	runIndex, ok := parsePositiveInt64Param(r, "runIndex")
589
+	if !ok {
590
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
591
+		return
592
+	}
593
+	view, err := h.loadActionsRunDetail(r.Context(), row.ID, owner.Username, row.Name, runIndex)
541
 	if err != nil {
594
 	if err != nil {
595
+		if errors.Is(err, pgx.ErrNoRows) {
596
+			h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
597
+		} else {
598
+			h.d.Logger.WarnContext(r.Context(), "repo actions: get run detail", "repo_id", row.ID, "run_index", runIndex, "error", err)
599
+			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
600
+		}
601
+		return
602
+	}
603
+
604
+	data := h.repoHeaderData(r, row, owner.Username, "actions")
605
+	data["Title"] = view.Title + " #" + strconv.FormatInt(view.RunIndex, 10) + " · " + row.Name
606
+	data["Run"] = view
607
+	data["UseHTMX"] = true
608
+	if err := h.d.Render.RenderPage(w, r, "repo/action_run", data); err != nil {
609
+		h.d.Logger.ErrorContext(r.Context(), "repo action run render", "run_index", runIndex, "error", err)
610
+	}
611
+}
612
+
613
+func (h *Handlers) repoActionRunStatus(w http.ResponseWriter, r *http.Request) {
614
+	row, owner, ok := h.loadRepoAndAuthorize(w, r, policy.ActionRepoRead)
615
+	if !ok {
616
+		return
617
+	}
618
+	runIndex, ok := parsePositiveInt64Param(r, "runIndex")
619
+	if !ok {
542
 		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
620
 		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
543
 		return
621
 		return
544
 	}
622
 	}
545
-	suite, err := h.cq.GetCheckSuiteForRepo(r.Context(), h.d.Pool, checksdb.GetCheckSuiteForRepoParams{
623
+	view, err := h.loadActionsRunDetail(r.Context(), row.ID, owner.Username, row.Name, runIndex)
546
-		RepoID: row.ID,
624
+	if err != nil {
547
-		ID:     suiteID,
625
+		if errors.Is(err, pgx.ErrNoRows) {
548
-	})
626
+			h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
627
+		} else {
628
+			h.d.Logger.WarnContext(r.Context(), "repo actions: get run status", "repo_id", row.ID, "run_index", runIndex, "error", err)
629
+			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
630
+		}
631
+		return
632
+	}
633
+
634
+	data := h.repoHeaderData(r, row, owner.Username, "actions")
635
+	data["Run"] = view
636
+	if err := h.d.Render.RenderFragment(w, "repo/action_run_status", data); err != nil {
637
+		h.d.Logger.ErrorContext(r.Context(), "repo action run status render", "run_index", runIndex, "error", err)
638
+	}
639
+}
640
+
641
+func (h *Handlers) repoActionStepLog(w http.ResponseWriter, r *http.Request) {
642
+	row, owner, ok := h.loadRepoAndAuthorize(w, r, policy.ActionRepoRead)
643
+	if !ok {
644
+		return
645
+	}
646
+	runIndex, ok := parsePositiveInt64Param(r, "runIndex")
647
+	if !ok {
648
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
649
+		return
650
+	}
651
+	jobIndex, ok := parseNonNegativeInt32Param(r, "jobIndex")
652
+	if !ok {
653
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
654
+		return
655
+	}
656
+	stepIndex, ok := parseNonNegativeInt32Param(r, "stepIndex")
657
+	if !ok {
658
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
659
+		return
660
+	}
661
+	run, err := h.loadActionsRunDetail(r.Context(), row.ID, owner.Username, row.Name, runIndex)
549
 	if err != nil {
662
 	if err != nil {
550
 		if errors.Is(err, pgx.ErrNoRows) {
663
 		if errors.Is(err, pgx.ErrNoRows) {
551
 			h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
664
 			h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
552
 		} else {
665
 		} else {
553
-			h.d.Logger.WarnContext(r.Context(), "repo actions: get suite", "suite_id", suiteID, "error", err)
666
+			h.d.Logger.WarnContext(r.Context(), "repo actions: get run for step log", "repo_id", row.ID, "run_index", runIndex, "error", err)
554
 			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
667
 			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
555
 		}
668
 		}
556
 		return
669
 		return
557
 	}
670
 	}
558
-	runs, err := h.cq.ListCheckRunsBySuite(r.Context(), h.d.Pool, suite.ID)
671
+
672
+	job, step, ok := findActionStep(run, jobIndex, stepIndex)
673
+	if !ok {
674
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
675
+		return
676
+	}
677
+	logContent, err := h.loadStepLogContent(r.Context(), step.ID)
559
 	if err != nil {
678
 	if err != nil {
560
-		h.d.Logger.WarnContext(r.Context(), "repo actions: get suite runs", "suite_id", suiteID, "error", err)
679
+		h.d.Logger.WarnContext(r.Context(), "repo actions: load step log", "step_id", step.ID, "error", err)
561
 		h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
680
 		h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
562
 		return
681
 		return
563
 	}
682
 	}
564
 
683
 
565
-	view := actionsSuiteViewFromGetRow(suite, runs)
684
+	view := actionsStepLogView{
685
+		Run:          run,
686
+		Job:          job,
687
+		Step:         step,
688
+		LogText:      logContent.Text,
689
+		LogSource:    logContent.Source,
690
+		LogError:     logContent.Error,
691
+		LogTruncated: logContent.Truncated,
692
+		DownloadURL:  logContent.DownloadURL,
693
+		BackHref:     run.ActionsHref + "/runs/" + strconv.FormatInt(run.RunIndex, 10) + "#job-" + strconv.FormatInt(int64(job.JobIndex), 10),
694
+	}
566
 	data := h.repoHeaderData(r, row, owner.Username, "actions")
695
 	data := h.repoHeaderData(r, row, owner.Username, "actions")
567
-	data["Title"] = view.Title + " · " + row.Name
696
+	data["Title"] = step.Name + " · " + run.Title + " #" + strconv.FormatInt(run.RunIndex, 10)
568
-	data["Run"] = view
697
+	data["Log"] = view
569
-	data["CSRFToken"] = middleware.CSRFTokenForRequest(r)
698
+	if err := h.d.Render.RenderPage(w, r, "repo/action_step_log", data); err != nil {
570
-	if err := h.d.Render.RenderPage(w, r, "repo/action_run", data); err != nil {
699
+		h.d.Logger.ErrorContext(r.Context(), "repo action step log render", "run_index", runIndex, "job_index", jobIndex, "step_index", stepIndex, "error", err)
571
-		h.d.Logger.ErrorContext(r.Context(), "repo action run render", "suite_id", suiteID, "error", err)
700
+	}
572
-	}
701
+}
573
-}
702
+
574
-
703
+func (h *Handlers) loadActionsRunDetail(ctx context.Context, repoID int64, owner, repoName string, runIndex int64) (actionsRunDetailView, error) {
575
-func actionsSuiteViewFromGetRow(row checksdb.GetCheckSuiteForRepoRow, runs []checksdb.CheckRun) actionsSuiteView {
704
+	q := actionsdb.New()
576
-	return actionsSuiteViewFromParts(
705
+	run, err := q.GetWorkflowRunForRepoByIndex(ctx, h.d.Pool, actionsdb.GetWorkflowRunForRepoByIndexParams{
577
-		row.ID,
706
+		RepoID:   repoID,
578
-		row.HeadSha,
707
+		RunIndex: runIndex,
579
-		row.AppSlug,
708
+	})
580
-		row.Status,
709
+	if err != nil {
581
-		row.Conclusion,
710
+		return actionsRunDetailView{}, err
582
-		row.CreatedAt.Time,
711
+	}
583
-		row.UpdatedAt.Time,
712
+	jobs, err := q.ListJobsForRun(ctx, h.d.Pool, run.ID)
584
-		row.PullNumber,
713
+	if err != nil {
585
-		row.PullTitle,
714
+		return actionsRunDetailView{}, err
586
-		row.PullAuthorUsername,
715
+	}
587
-		row.HeadRef,
716
+	artifacts, err := q.ListArtifactsForRun(ctx, h.d.Pool, run.ID)
588
-		row.BaseRef,
717
+	if err != nil {
589
-		runs,
718
+		return actionsRunDetailView{}, err
590
-	)
719
+	}
591
-}
720
+
592
-
721
+	basePath := "/" + owner + "/" + repoName + "/actions"
593
-func actionsSuiteViewFromParts(
722
+	runPath := basePath + "/runs/" + strconv.FormatInt(run.RunIndex, 10)
594
-	id int64,
723
+	now := time.Now()
595
-	headSHA string,
724
+	stateText, stateClass, stateIcon := workflowRunState(run.Status, run.Conclusion)
596
-	appSlug string,
725
+	updatedAt := pgTime(run.UpdatedAt, run.CreatedAt.Time)
597
-	status checksdb.CheckStatus,
726
+	view := actionsRunDetailView{
598
-	conclusion checksdb.NullCheckConclusion,
727
+		ID:             run.ID,
599
-	createdAt time.Time,
728
+		RunIndex:       run.RunIndex,
600
-	updatedAt time.Time,
729
+		WorkflowFile:   run.WorkflowFile,
601
-	pullNumber int64,
730
+		WorkflowName:   run.WorkflowName,
602
-	pullTitle string,
731
+		Title:          workflowDisplayName(run.WorkflowName, run.WorkflowFile),
603
-	pullAuthorUsername string,
732
+		HeadSha:        run.HeadSha,
604
-	headRef string,
733
+		HeadShaShort:   shortSHA(run.HeadSha),
605
-	baseRef string,
734
+		HeadRef:        run.HeadRef,
606
-	runs []checksdb.CheckRun,
735
+		Event:          string(run.Event),
607
-) actionsSuiteView {
736
+		EventLabel:     workflowRunEventLabel(string(run.Event)),
608
-	title := pullTitle
737
+		ActorUsername:  run.ActorUsername,
609
-	if title == "" {
738
+		StateText:      stateText,
610
-		title = appSlug + " checks for " + shortSHA(headSHA)
739
+		StateClass:     stateClass,
611
-	}
740
+		StateIcon:      stateIcon,
612
-	stateText, stateClass, stateIcon := checkActionState(status, conclusion)
741
+		CreatedAt:      run.CreatedAt.Time,
613
-	runViews := make([]actionsRunView, 0, len(runs))
742
+		UpdatedAt:      updatedAt,
614
-	annotationCount := 0
743
+		Duration:       workflowRunDuration(run.Status, run.StartedAt, run.CompletedAt, run.CreatedAt, updatedAt, now),
615
-	for _, run := range runs {
744
+		IsTerminal:     workflowRunTerminal(run.Status),
616
-		view := actionsRunViewFromRun(run)
745
+		StatusHref:     runPath + "/status",
617
-		if view.SummaryHTML != "" {
746
+		ActionsHref:    basePath,
618
-			annotationCount++
747
+		CodeHref:       "/" + owner + "/" + repoName + "/tree/" + codeTarget(run.HeadRef, run.HeadSha),
748
+		ArtifactCount:  len(artifacts),
749
+		JobCount:       len(jobs),
750
+		CompletedCount: 0,
751
+		FailureCount:   0,
752
+		Jobs:           make([]actionsJobDetailView, 0, len(jobs)),
753
+	}
754
+	for _, job := range jobs {
755
+		steps, err := q.ListStepsForJob(ctx, h.d.Pool, job.ID)
756
+		if err != nil {
757
+			return actionsRunDetailView{}, err
619
 		}
758
 		}
620
-		runViews = append(runViews, view)
759
+		jobView := actionsJobDetailViewFromRow(job, owner, repoName, run.RunIndex, now)
621
-	}
760
+		jobView.Steps = make([]actionsStepDetailView, 0, len(steps))
622
-	return actionsSuiteView{
761
+		for _, step := range steps {
623
-		ID:                 id,
762
+			jobView.Steps = append(jobView.Steps, actionsStepDetailViewFromRow(step, owner, repoName, run.RunIndex, job.JobIndex, now))
624
-		AppSlug:            appSlug,
763
+		}
625
-		Title:              title,
764
+		if job.Status == actionsdb.WorkflowJobStatusCompleted || job.Status == actionsdb.WorkflowJobStatusCancelled || job.Status == actionsdb.WorkflowJobStatusSkipped {
626
-		HeadSha:            headSHA,
765
+			view.CompletedCount++
627
-		HeadShaShort:       shortSHA(headSHA),
628
-		PullNumber:         pullNumber,
629
-		PullAuthorUsername: pullAuthorUsername,
630
-		HeadRef:            headRef,
631
-		BaseRef:            baseRef,
632
-		RunCount:           len(runs),
633
-		StateText:          stateText,
634
-		StateClass:         stateClass,
635
-		StateIcon:          stateIcon,
636
-		CreatedAt:          createdAt,
637
-		UpdatedAt:          updatedAt,
638
-		Duration:           actionSuiteDuration(runs, createdAt, updatedAt),
639
-		Runs:               runViews,
640
-		AnnotationCount:    annotationCount,
641
-	}
642
-}
643
-
644
-func actionsRunViewFromRun(run checksdb.CheckRun) actionsRunView {
645
-	stateText, stateClass, stateIcon := checkActionState(run.Status, run.Conclusion)
646
-	start := run.CreatedAt.Time
647
-	if run.StartedAt.Valid {
648
-		start = run.StartedAt.Time
649
-	}
650
-	end := run.UpdatedAt.Time
651
-	if run.CompletedAt.Valid {
652
-		end = run.CompletedAt.Time
653
-	}
654
-	return actionsRunView{
655
-		ID:          run.ID,
656
-		Name:        run.Name,
657
-		StateText:   stateText,
658
-		StateClass:  stateClass,
659
-		StateIcon:   stateIcon,
660
-		Duration:    formatDuration(end.Sub(start)),
661
-		CompletedAt: end,
662
-		DetailsURL:  run.DetailsUrl,
663
-		SummaryHTML: renderCheckSummary(run.Output),
664
-	}
665
-}
666
-
667
-func checkActionState(status checksdb.CheckStatus, conclusion checksdb.NullCheckConclusion) (string, string, string) {
668
-	if !conclusion.Valid {
669
-		switch status {
670
-		case checksdb.CheckStatusCompleted:
671
-			return "Completed", "neutral", "check-circle"
672
-		case checksdb.CheckStatusInProgress:
673
-			return "In progress", "running", "dot-fill"
674
-		case checksdb.CheckStatusQueued, checksdb.CheckStatusPending:
675
-			return "Queued", "pending", "dot-fill"
676
-		default:
677
-			return string(status), "neutral", "dot-fill"
678
 		}
766
 		}
767
+		if jobView.StateClass == "failure" {
768
+			view.FailureCount++
769
+		}
770
+		view.Jobs = append(view.Jobs, jobView)
679
 	}
771
 	}
680
-	switch conclusion.CheckConclusion {
772
+	view.Stages = actionsJobStages(view.Jobs)
681
-	case checksdb.CheckConclusionSuccess, checksdb.CheckConclusionSkipped, checksdb.CheckConclusionNeutral:
773
+	return view, nil
774
+}
775
+
776
+func actionsJobDetailViewFromRow(row actionsdb.ListJobsForRunRow, owner, repoName string, runIndex int64, now time.Time) actionsJobDetailView {
777
+	stateText, stateClass, stateIcon := workflowJobState(row.Status, row.Conclusion)
778
+	name := strings.TrimSpace(row.JobName)
779
+	if name == "" {
780
+		name = row.JobKey
781
+	}
782
+	return actionsJobDetailView{
783
+		ID:         row.ID,
784
+		JobIndex:   row.JobIndex,
785
+		JobKey:     row.JobKey,
786
+		Name:       name,
787
+		RunsOn:     row.RunsOn,
788
+		Needs:      append([]string(nil), row.NeedsJobs...),
789
+		NeedsText:  strings.Join(row.NeedsJobs, ", "),
790
+		StateText:  stateText,
791
+		StateClass: stateClass,
792
+		StateIcon:  stateIcon,
793
+		Duration:   actionItemDuration(string(row.Status), string(actionsdb.WorkflowJobStatusQueued), row.StartedAt, row.CompletedAt, row.CreatedAt, row.UpdatedAt, now),
794
+		Anchor:     "job-" + strconv.FormatInt(int64(row.JobIndex), 10),
795
+	}
796
+}
797
+
798
+func actionsStepDetailViewFromRow(row actionsdb.ListStepsForJobRow, owner, repoName string, runIndex int64, jobIndex int32, now time.Time) actionsStepDetailView {
799
+	stateText, stateClass, stateIcon := workflowStepState(row.Status, row.Conclusion)
800
+	name, kind, detail := workflowStepDisplay(row)
801
+	return actionsStepDetailView{
802
+		ID:           row.ID,
803
+		StepIndex:    row.StepIndex,
804
+		StepID:       row.StepID,
805
+		Name:         name,
806
+		Kind:         kind,
807
+		Detail:       detail,
808
+		StateText:    stateText,
809
+		StateClass:   stateClass,
810
+		StateIcon:    stateIcon,
811
+		Duration:     actionItemDuration(string(row.Status), string(actionsdb.WorkflowStepStatusQueued), row.StartedAt, row.CompletedAt, row.CreatedAt, row.UpdatedAt, now),
812
+		LogByteCount: row.LogByteCount,
813
+		LogHref: "/" + owner + "/" + repoName + "/actions/runs/" + strconv.FormatInt(runIndex, 10) +
814
+			"/jobs/" + strconv.FormatInt(int64(jobIndex), 10) +
815
+			"/steps/" + strconv.FormatInt(int64(row.StepIndex), 10),
816
+	}
817
+}
818
+
819
+func workflowStepDisplay(row actionsdb.ListStepsForJobRow) (name, kind, detail string) {
820
+	if row.UsesAlias != "" {
821
+		kind = "uses"
822
+		detail = row.UsesAlias
823
+	} else {
824
+		kind = "run"
825
+		detail = firstCommandLine(row.RunCommand)
826
+	}
827
+	name = strings.TrimSpace(row.StepName)
828
+	if name == "" {
829
+		name = strings.TrimSpace(detail)
830
+	}
831
+	if name == "" {
832
+		name = "Step " + strconv.Itoa(int(row.StepIndex)+1)
833
+	}
834
+	if len(detail) > 120 {
835
+		detail = detail[:117] + "..."
836
+	}
837
+	return name, kind, detail
838
+}
839
+
840
+func firstCommandLine(command string) string {
841
+	for _, line := range strings.Split(command, "\n") {
842
+		line = strings.TrimSpace(line)
843
+		if line != "" {
844
+			return line
845
+		}
846
+	}
847
+	return ""
848
+}
849
+
850
+func actionsJobStages(jobs []actionsJobDetailView) []actionsJobStageView {
851
+	indexByKey := make(map[string]int, len(jobs))
852
+	for i := range jobs {
853
+		indexByKey[jobs[i].JobKey] = i
854
+	}
855
+	state := make(map[string]int, len(jobs))
856
+	var depthFor func(string) int
857
+	depthFor = func(key string) int {
858
+		i, ok := indexByKey[key]
859
+		if !ok {
860
+			return 0
861
+		}
862
+		switch state[key] {
863
+		case 1:
864
+			return 0
865
+		case 2:
866
+			return jobs[i].Depth
867
+		}
868
+		state[key] = 1
869
+		depth := 0
870
+		for _, need := range jobs[i].Needs {
871
+			if _, ok := indexByKey[need]; ok {
872
+				if depDepth := depthFor(need) + 1; depDepth > depth {
873
+					depth = depDepth
874
+				}
875
+			}
876
+		}
877
+		jobs[i].Depth = depth
878
+		state[key] = 2
879
+		return depth
880
+	}
881
+	maxDepth := 0
882
+	for i := range jobs {
883
+		depth := depthFor(jobs[i].JobKey)
884
+		if depth > maxDepth {
885
+			maxDepth = depth
886
+		}
887
+	}
888
+	stages := make([]actionsJobStageView, maxDepth+1)
889
+	for i := range stages {
890
+		stages[i].Index = i
891
+	}
892
+	for _, job := range jobs {
893
+		stages[job.Depth].Jobs = append(stages[job.Depth].Jobs, job)
894
+	}
895
+	return stages
896
+}
897
+
898
+func workflowJobState(status actionsdb.WorkflowJobStatus, conclusion actionsdb.NullCheckConclusion) (string, string, string) {
899
+	if status == actionsdb.WorkflowJobStatusSkipped && !conclusion.Valid {
900
+		return "Skipped", "neutral", "dash"
901
+	}
902
+	if status == actionsdb.WorkflowJobStatusCompleted && conclusion.Valid {
903
+		return workflowConclusionState(conclusion.CheckConclusion)
904
+	}
905
+	switch status {
906
+	case actionsdb.WorkflowJobStatusQueued:
907
+		return "Queued", "pending", "dot-fill"
908
+	case actionsdb.WorkflowJobStatusRunning:
909
+		return "In progress", "running", "dot-fill"
910
+	case actionsdb.WorkflowJobStatusCancelled:
911
+		return "Cancelled", "neutral", "x-circle"
912
+	case actionsdb.WorkflowJobStatusCompleted:
913
+		return "Completed", "neutral", "check-circle"
914
+	default:
915
+		if conclusion.Valid {
916
+			return workflowConclusionState(conclusion.CheckConclusion)
917
+		}
918
+		return string(status), "neutral", "dot-fill"
919
+	}
920
+}
921
+
922
+func workflowStepState(status actionsdb.WorkflowStepStatus, conclusion actionsdb.NullCheckConclusion) (string, string, string) {
923
+	if status == actionsdb.WorkflowStepStatusSkipped && !conclusion.Valid {
924
+		return "Skipped", "neutral", "dash"
925
+	}
926
+	if status == actionsdb.WorkflowStepStatusCompleted && conclusion.Valid {
927
+		return workflowConclusionState(conclusion.CheckConclusion)
928
+	}
929
+	switch status {
930
+	case actionsdb.WorkflowStepStatusQueued:
931
+		return "Queued", "pending", "dot-fill"
932
+	case actionsdb.WorkflowStepStatusRunning:
933
+		return "In progress", "running", "dot-fill"
934
+	case actionsdb.WorkflowStepStatusCancelled:
935
+		return "Cancelled", "neutral", "x-circle"
936
+	case actionsdb.WorkflowStepStatusCompleted:
937
+		return "Completed", "neutral", "check-circle"
938
+	default:
939
+		if conclusion.Valid {
940
+			return workflowConclusionState(conclusion.CheckConclusion)
941
+		}
942
+		return string(status), "neutral", "dot-fill"
943
+	}
944
+}
945
+
946
+func workflowConclusionState(conclusion actionsdb.CheckConclusion) (string, string, string) {
947
+	switch conclusion {
948
+	case actionsdb.CheckConclusionSuccess, actionsdb.CheckConclusionSkipped, actionsdb.CheckConclusionNeutral:
682
 		return "Success", "success", "check-circle-fill"
949
 		return "Success", "success", "check-circle-fill"
683
-	case checksdb.CheckConclusionFailure, checksdb.CheckConclusionTimedOut, checksdb.CheckConclusionActionRequired:
950
+	case actionsdb.CheckConclusionFailure, actionsdb.CheckConclusionTimedOut, actionsdb.CheckConclusionActionRequired:
684
 		return "Failure", "failure", "x-circle-fill"
951
 		return "Failure", "failure", "x-circle-fill"
685
-	case checksdb.CheckConclusionCancelled, checksdb.CheckConclusionStale:
952
+	case actionsdb.CheckConclusionCancelled, actionsdb.CheckConclusionStale:
686
 		return "Cancelled", "neutral", "x-circle"
953
 		return "Cancelled", "neutral", "x-circle"
687
 	default:
954
 	default:
688
-		return string(conclusion.CheckConclusion), "neutral", "dot-fill"
955
+		return string(conclusion), "neutral", "dot-fill"
956
+	}
957
+}
958
+
959
+func workflowRunTerminal(status actionsdb.WorkflowRunStatus) bool {
960
+	return status == actionsdb.WorkflowRunStatusCompleted || status == actionsdb.WorkflowRunStatusCancelled
961
+}
962
+
963
+func actionItemDuration(status string, queuedStatus string, startedAt, completedAt, createdAt, updatedAt pgtype.Timestamptz, now time.Time) string {
964
+	if status == queuedStatus {
965
+		return "—"
966
+	}
967
+	start := createdAt.Time
968
+	if startedAt.Valid {
969
+		start = startedAt.Time
970
+	}
971
+	end := pgTime(updatedAt, now)
972
+	if status == "running" {
973
+		end = now
974
+	} else if completedAt.Valid {
975
+		end = completedAt.Time
689
 	}
976
 	}
977
+	return formatDuration(end.Sub(start))
690
 }
978
 }
691
 
979
 
692
-func actionSuiteDuration(runs []checksdb.CheckRun, createdAt, updatedAt time.Time) string {
980
+func pgTime(ts pgtype.Timestamptz, fallback time.Time) time.Time {
693
-	if len(runs) == 0 {
981
+	if ts.Valid && !ts.Time.IsZero() {
694
-		return formatDuration(updatedAt.Sub(createdAt))
982
+		return ts.Time
695
 	}
983
 	}
696
-	var start, end time.Time
984
+	return fallback
697
-	for _, run := range runs {
985
+}
698
-		runStart := run.CreatedAt.Time
986
+
699
-		if run.StartedAt.Valid {
987
+func codeTarget(ref, sha string) string {
700
-			runStart = run.StartedAt.Time
988
+	if ref != "" {
989
+		return ref
990
+	}
991
+	return sha
992
+}
993
+
994
+func findActionStep(run actionsRunDetailView, jobIndex, stepIndex int32) (actionsJobDetailView, actionsStepDetailView, bool) {
995
+	for _, job := range run.Jobs {
996
+		if job.JobIndex != jobIndex {
997
+			continue
998
+		}
999
+		for _, step := range job.Steps {
1000
+			if step.StepIndex == stepIndex {
1001
+				return job, step, true
1002
+			}
701
 		}
1003
 		}
702
-		runEnd := run.UpdatedAt.Time
1004
+		return actionsJobDetailView{}, actionsStepDetailView{}, false
703
-		if run.CompletedAt.Valid {
1005
+	}
704
-			runEnd = run.CompletedAt.Time
1006
+	return actionsJobDetailView{}, actionsStepDetailView{}, false
1007
+}
1008
+
1009
+type actionsStepLogContent struct {
1010
+	Text        string
1011
+	Source      string
1012
+	Error       string
1013
+	Truncated   bool
1014
+	DownloadURL string
1015
+}
1016
+
1017
+func (h *Handlers) loadStepLogContent(ctx context.Context, stepID int64) (actionsStepLogContent, error) {
1018
+	q := actionsdb.New()
1019
+	step, err := q.GetWorkflowStepByID(ctx, h.d.Pool, stepID)
1020
+	if err != nil {
1021
+		return actionsStepLogContent{}, err
1022
+	}
1023
+	if step.LogObjectKey.Valid && step.LogObjectKey.String != "" {
1024
+		return h.loadArchivedStepLog(ctx, step.LogObjectKey.String)
1025
+	}
1026
+	chunks, err := q.ListAllStepLogChunksForStep(ctx, h.d.Pool, step.ID)
1027
+	if err != nil {
1028
+		return actionsStepLogContent{}, err
1029
+	}
1030
+	buf := bytes.NewBuffer(make([]byte, 0, minInt(actionsStepLogRenderLimit, int(step.LogByteCount)+1)))
1031
+	truncated := false
1032
+	for _, chunk := range chunks {
1033
+		if buf.Len() >= actionsStepLogRenderLimit {
1034
+			truncated = true
1035
+			break
705
 		}
1036
 		}
706
-		if start.IsZero() || runStart.Before(start) {
1037
+		remaining := actionsStepLogRenderLimit - buf.Len()
707
-			start = runStart
1038
+		if len(chunk.Chunk) > remaining {
1039
+			_, _ = buf.Write(chunk.Chunk[:remaining])
1040
+			truncated = true
1041
+			break
708
 		}
1042
 		}
709
-		if end.IsZero() || runEnd.After(end) {
1043
+		_, _ = buf.Write(chunk.Chunk)
710
-			end = runEnd
1044
+	}
1045
+	return actionsStepLogContent{
1046
+		Text:      strings.ToValidUTF8(buf.String(), "\uFFFD"),
1047
+		Source:    "SQL chunks",
1048
+		Truncated: truncated,
1049
+	}, nil
1050
+}
1051
+
1052
+func (h *Handlers) loadArchivedStepLog(ctx context.Context, key string) (actionsStepLogContent, error) {
1053
+	if h.d.ObjectStore == nil {
1054
+		return actionsStepLogContent{
1055
+			Source: "object storage",
1056
+			Error:  "Archived log storage is not configured for this server.",
1057
+		}, nil
1058
+	}
1059
+	rc, _, err := h.d.ObjectStore.Get(ctx, key)
1060
+	if err != nil {
1061
+		if errors.Is(err, storage.ErrNotFound) {
1062
+			return actionsStepLogContent{
1063
+				Source: "object storage",
1064
+				Error:  "Archived log object was not found.",
1065
+			}, nil
711
 		}
1066
 		}
1067
+		return actionsStepLogContent{}, err
712
 	}
1068
 	}
713
-	return formatDuration(end.Sub(start))
1069
+	defer rc.Close()
1070
+	body, truncated, err := readLimitedLog(rc, actionsStepLogRenderLimit)
1071
+	if err != nil {
1072
+		return actionsStepLogContent{}, err
1073
+	}
1074
+	downloadURL, _ := h.d.ObjectStore.SignedURL(ctx, key, 15*time.Minute, http.MethodGet)
1075
+	return actionsStepLogContent{
1076
+		Text:        strings.ToValidUTF8(string(body), "\uFFFD"),
1077
+		Source:      "object storage",
1078
+		Truncated:   truncated,
1079
+		DownloadURL: downloadURL,
1080
+	}, nil
1081
+}
1082
+
1083
+func readLimitedLog(r io.Reader, limit int) ([]byte, bool, error) {
1084
+	body, err := io.ReadAll(io.LimitReader(r, int64(limit)+1))
1085
+	if err != nil {
1086
+		return nil, false, err
1087
+	}
1088
+	if len(body) > limit {
1089
+		return body[:limit], true, nil
1090
+	}
1091
+	return body, false, nil
1092
+}
1093
+
1094
+func parsePositiveInt64Param(r *http.Request, name string) (int64, bool) {
1095
+	v, err := strconv.ParseInt(chi.URLParam(r, name), 10, 64)
1096
+	return v, err == nil && v > 0
1097
+}
1098
+
1099
+func parseNonNegativeInt32Param(r *http.Request, name string) (int32, bool) {
1100
+	v, err := strconv.ParseInt(chi.URLParam(r, name), 10, 32)
1101
+	return int32(v), err == nil && v >= 0
1102
+}
1103
+
1104
+func minInt(a, b int) int {
1105
+	if a < b {
1106
+		return a
1107
+	}
1108
+	return b
714
 }
1109
 }
715
 
1110
 
716
 func formatDuration(d time.Duration) string {
1111
 func formatDuration(d time.Duration) string {
internal/web/handlers/repo/repo.gomodified
@@ -61,13 +61,17 @@ func (h *Handlers) cloneSSH(owner, name string) string {
61
 
61
 
62
 // Deps wires the handler set.
62
 // Deps wires the handler set.
63
 type Deps struct {
63
 type Deps struct {
64
-	Logger    *slog.Logger
64
+	Logger *slog.Logger
65
-	Render    *render.Renderer
65
+	Render *render.Renderer
66
-	Pool      *pgxpool.Pool
66
+	Pool   *pgxpool.Pool
67
-	RepoFS    *storage.RepoFS
67
+	RepoFS *storage.RepoFS
68
-	Audit     *audit.Recorder
68
+	// ObjectStore serves archived Actions logs and other repo-scoped blobs.
69
-	Limiter   *throttle.Limiter
69
+	// nil keeps pages renderable in dev/test but archived logs show an
70
-	CloneURLs CloneURLs
70
+	// unavailable message instead of exposing storage details.
71
+	ObjectStore storage.ObjectStore
72
+	Audit       *audit.Recorder
73
+	Limiter     *throttle.Limiter
74
+	CloneURLs   CloneURLs
71
 	// SecretBox AEAD-wraps webhook secrets at rest (S33). nil disables
75
 	// SecretBox AEAD-wraps webhook secrets at rest (S33). nil disables
72
 	// the webhook surface (the handler renders a placeholder page).
76
 	// the webhook surface (the handler renders a placeholder page).
73
 	SecretBox *secretbox.Box
77
 	SecretBox *secretbox.Box
@@ -128,7 +132,9 @@ func (h *Handlers) MountRepoActionsAPI(r chi.Router) {
128
 // two-segment route doesn't collide with the /{username} catch-all from S09;
132
 // two-segment route doesn't collide with the /{username} catch-all from S09;
129
 // caller is responsible for ordering this BEFORE /{username}.
133
 // caller is responsible for ordering this BEFORE /{username}.
130
 func (h *Handlers) MountRepoHome(r chi.Router) {
134
 func (h *Handlers) MountRepoHome(r chi.Router) {
131
-	r.Get("/{owner}/{repo}/actions/runs/{suiteID}", h.repoActionRun)
135
+	r.Get("/{owner}/{repo}/actions/runs/{runIndex}/jobs/{jobIndex}/steps/{stepIndex}", h.repoActionStepLog)
136
+	r.Get("/{owner}/{repo}/actions/runs/{runIndex}/status", h.repoActionRunStatus)
137
+	r.Get("/{owner}/{repo}/actions/runs/{runIndex}", h.repoActionRun)
132
 	r.Get("/{owner}/{repo}/actions", h.repoTabActions)
138
 	r.Get("/{owner}/{repo}/actions", h.repoTabActions)
133
 	r.Get("/{owner}/{repo}/projects", h.repoTabProjects)
139
 	r.Get("/{owner}/{repo}/projects", h.repoTabProjects)
134
 	r.Get("/{owner}/{repo}/wiki", h.repoTabWiki)
140
 	r.Get("/{owner}/{repo}/wiki", h.repoTabWiki)
internal/web/render/render.gomodified
@@ -14,6 +14,7 @@ import (
14
 	"io/fs"
14
 	"io/fs"
15
 	"net/http"
15
 	"net/http"
16
 	"path"
16
 	"path"
17
+	"reflect"
17
 	"sort"
18
 	"sort"
18
 	"strings"
19
 	"strings"
19
 	"text/template/parse"
20
 	"text/template/parse"
@@ -320,6 +321,10 @@ func funcMap(octicon OcticonResolver) template.FuncMap {
320
 			}
321
 			}
321
 			return ""
322
 			return ""
322
 		},
323
 		},
324
+		// flag reads an optional boolean-ish field from map or struct
325
+		// template data. Layout-level feature toggles use this so pages
326
+		// backed by typed structs don't fail when the toggle is absent.
327
+		"flag": dataFlag,
323
 		// csrfToken pulls the per-request token from the request context.
328
 		// csrfToken pulls the per-request token from the request context.
324
 		// Templates use this in <input type="hidden" name="csrf_token">.
329
 		// Templates use this in <input type="hidden" name="csrf_token">.
325
 		"csrfToken": middleware.CSRFTokenForRequest,
330
 		"csrfToken": middleware.CSRFTokenForRequest,
@@ -347,6 +352,59 @@ func funcMap(octicon OcticonResolver) template.FuncMap {
347
 	}
352
 	}
348
 }
353
 }
349
 
354
 
355
+func dataFlag(data any, name string) bool {
356
+	v := reflect.ValueOf(data)
357
+	if !v.IsValid() {
358
+		return false
359
+	}
360
+	for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
361
+		if v.IsNil() {
362
+			return false
363
+		}
364
+		v = v.Elem()
365
+	}
366
+	switch v.Kind() {
367
+	case reflect.Map:
368
+		if v.Type().Key().Kind() != reflect.String {
369
+			return false
370
+		}
371
+		field := v.MapIndex(reflect.ValueOf(name))
372
+		return truthyValue(field)
373
+	case reflect.Struct:
374
+		field := v.FieldByName(name)
375
+		if !field.IsValid() || !field.CanInterface() {
376
+			return false
377
+		}
378
+		return truthyValue(field)
379
+	default:
380
+		return false
381
+	}
382
+}
383
+
384
+func truthyValue(v reflect.Value) bool {
385
+	if !v.IsValid() {
386
+		return false
387
+	}
388
+	for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
389
+		if v.IsNil() {
390
+			return false
391
+		}
392
+		v = v.Elem()
393
+	}
394
+	switch v.Kind() {
395
+	case reflect.Bool:
396
+		return v.Bool()
397
+	case reflect.String:
398
+		return v.String() != ""
399
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
400
+		return v.Int() != 0
401
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
402
+		return v.Uint() != 0
403
+	default:
404
+		return !v.IsZero()
405
+	}
406
+}
407
+
350
 // relativeTime returns a human-readable relative-time string. The intent is
408
 // relativeTime returns a human-readable relative-time string. The intent is
351
 // to read naturally; absolute precision below the level of "minutes" isn't
409
 // to read naturally; absolute precision below the level of "minutes" isn't
352
 // useful for UI labels.
410
 // useful for UI labels.
internal/web/repo_wiring.gomodified
@@ -27,6 +27,7 @@ import (
27
 func buildRepoHandlers(
27
 func buildRepoHandlers(
28
 	cfg config.Config,
28
 	cfg config.Config,
29
 	pool *pgxpool.Pool,
29
 	pool *pgxpool.Pool,
30
+	objectStore storage.ObjectStore,
30
 	tmplFS fs.FS,
31
 	tmplFS fs.FS,
31
 	logger *slog.Logger,
32
 	logger *slog.Logger,
32
 ) (*repoh.Handlers, error) {
33
 ) (*repoh.Handlers, error) {
@@ -75,6 +76,7 @@ func buildRepoHandlers(
75
 		Render:       rr,
76
 		Render:       rr,
76
 		Pool:         pool,
77
 		Pool:         pool,
77
 		RepoFS:       rfs,
78
 		RepoFS:       rfs,
79
+		ObjectStore:  objectStore,
78
 		Audit:        audit.NewRecorder(),
80
 		Audit:        audit.NewRecorder(),
79
 		Limiter:      throttle.NewLimiter(),
81
 		Limiter:      throttle.NewLimiter(),
80
 		SecretBox:    hookBox,
82
 		SecretBox:    hookBox,
internal/web/server.gomodified
@@ -198,7 +198,7 @@ func Run(ctx context.Context, opts Options) error {
198
 		deps.ProfileMounter = profile.MountProfile
198
 		deps.ProfileMounter = profile.MountProfile
199
 		deps.OrgRepositoriesMounter = profile.MountOrgRepositories
199
 		deps.OrgRepositoriesMounter = profile.MountOrgRepositories
200
 
200
 
201
-		repoH, err := buildRepoHandlers(cfg, pool, deps.TemplatesFS, logger)
201
+		repoH, err := buildRepoHandlers(cfg, pool, objectStore, deps.TemplatesFS, logger)
202
 		if err != nil {
202
 		if err != nil {
203
 			return fmt.Errorf("repo handlers: %w", err)
203
 			return fmt.Errorf("repo handlers: %w", err)
204
 		}
204
 		}
internal/web/static/css/shithub.cssmodified
@@ -5063,6 +5063,16 @@ button.shithub-repo-action {
5063
 .shithub-actions-run-status {
5063
 .shithub-actions-run-status {
5064
   pointer-events: none;
5064
   pointer-events: none;
5065
 }
5065
 }
5066
+.shithub-actions-run-status-wrap {
5067
+  display: flex;
5068
+  align-items: center;
5069
+  gap: 0.55rem;
5070
+}
5071
+.shithub-actions-run-status-meta {
5072
+  color: var(--fg-muted);
5073
+  font-size: 0.82rem;
5074
+  white-space: nowrap;
5075
+}
5066
 .shithub-actions-run-layout {
5076
 .shithub-actions-run-layout {
5067
   grid-template-columns: 14rem minmax(0, 1fr);
5077
   grid-template-columns: 14rem minmax(0, 1fr);
5068
   padding: 1.5rem 0 0;
5078
   padding: 1.5rem 0 0;
@@ -5109,21 +5119,30 @@ button.shithub-repo-action {
5109
 }
5119
 }
5110
 .shithub-actions-workflow-graph {
5120
 .shithub-actions-workflow-graph {
5111
   position: relative;
5121
   position: relative;
5112
-  min-height: 13rem;
5122
+  overflow-x: auto;
5113
-  padding: 2.5rem;
5123
+  min-height: 11rem;
5124
+  padding: 1.25rem;
5114
   background: var(--canvas-inset);
5125
   background: var(--canvas-inset);
5115
 }
5126
 }
5127
+.shithub-actions-workflow-stages {
5128
+  display: grid;
5129
+  grid-auto-flow: column;
5130
+  grid-auto-columns: minmax(14rem, 1fr);
5131
+  gap: 1rem;
5132
+  min-width: min-content;
5133
+}
5116
 .shithub-actions-workflow-stage {
5134
 .shithub-actions-workflow-stage {
5117
   display: flex;
5135
   display: flex;
5118
   flex-direction: column;
5136
   flex-direction: column;
5119
   gap: 0.75rem;
5137
   gap: 0.75rem;
5120
-  max-width: 20rem;
5121
 }
5138
 }
5122
 .shithub-actions-job-card {
5139
 .shithub-actions-job-card {
5140
+  position: relative;
5123
   display: grid;
5141
   display: grid;
5124
   grid-template-columns: 1rem minmax(0, 1fr) auto;
5142
   grid-template-columns: 1rem minmax(0, 1fr) auto;
5125
   align-items: center;
5143
   align-items: center;
5126
   gap: 0.55rem;
5144
   gap: 0.55rem;
5145
+  min-height: 4.25rem;
5127
   padding: 0.7rem;
5146
   padding: 0.7rem;
5128
   border: 1px solid var(--border-default);
5147
   border: 1px solid var(--border-default);
5129
   border-radius: 6px;
5148
   border-radius: 6px;
@@ -5131,22 +5150,148 @@ button.shithub-repo-action {
5131
   color: var(--fg-default);
5150
   color: var(--fg-default);
5132
   text-decoration: none;
5151
   text-decoration: none;
5133
 }
5152
 }
5153
+.shithub-actions-workflow-stage:not(:first-child) .shithub-actions-job-card::before {
5154
+  content: "";
5155
+  position: absolute;
5156
+  top: 50%;
5157
+  left: -1rem;
5158
+  width: 1rem;
5159
+  border-top: 1px solid var(--border-muted);
5160
+}
5134
 .shithub-actions-job-card:hover { text-decoration: none; }
5161
 .shithub-actions-job-card:hover { text-decoration: none; }
5135
 .shithub-actions-job-card strong {
5162
 .shithub-actions-job-card strong {
5136
   overflow: hidden;
5163
   overflow: hidden;
5137
   text-overflow: ellipsis;
5164
   text-overflow: ellipsis;
5138
   white-space: nowrap;
5165
   white-space: nowrap;
5139
 }
5166
 }
5140
-.shithub-actions-job-card span:last-child {
5167
+.shithub-actions-job-card span:nth-child(3) {
5141
   color: var(--fg-muted);
5168
   color: var(--fg-muted);
5142
   font-size: 0.82rem;
5169
   font-size: 0.82rem;
5143
 }
5170
 }
5144
-.shithub-actions-graph-controls {
5171
+.shithub-actions-job-card small {
5145
-  position: absolute;
5172
+  grid-column: 2 / 4;
5146
-  right: 0.75rem;
5173
+  overflow: hidden;
5147
-  bottom: 0.75rem;
5174
+  color: var(--fg-muted);
5175
+  font-size: 0.78rem;
5176
+  text-overflow: ellipsis;
5177
+  white-space: nowrap;
5178
+}
5179
+.shithub-actions-empty-compact {
5180
+  border: 0;
5181
+  border-top: 1px solid var(--border-default);
5182
+  border-radius: 0;
5183
+  padding: 2rem 1rem;
5184
+}
5185
+.shithub-actions-jobs {
5148
   display: flex;
5186
   display: flex;
5149
-  gap: 0.35rem;
5187
+  flex-direction: column;
5188
+  gap: 0.75rem;
5189
+  margin-top: 1rem;
5190
+}
5191
+.shithub-actions-job-detail {
5192
+  overflow: hidden;
5193
+  border: 1px solid var(--border-default);
5194
+  border-radius: 6px;
5195
+  background: var(--canvas-default);
5196
+}
5197
+.shithub-actions-job-detail summary {
5198
+  display: grid;
5199
+  grid-template-columns: 1rem minmax(0, 1fr);
5200
+  align-items: center;
5201
+  gap: 0.6rem;
5202
+  padding: 0.9rem 1rem;
5203
+  cursor: pointer;
5204
+  list-style: none;
5205
+}
5206
+.shithub-actions-job-detail summary::-webkit-details-marker {
5207
+  display: none;
5208
+}
5209
+.shithub-actions-job-detail summary strong,
5210
+.shithub-actions-step-row strong {
5211
+  display: block;
5212
+  overflow: hidden;
5213
+  text-overflow: ellipsis;
5214
+  white-space: nowrap;
5215
+}
5216
+.shithub-actions-job-detail summary small,
5217
+.shithub-actions-step-row small {
5218
+  display: block;
5219
+  overflow: hidden;
5220
+  margin-top: 0.15rem;
5221
+  color: var(--fg-muted);
5222
+  font-size: 0.82rem;
5223
+  text-overflow: ellipsis;
5224
+  white-space: nowrap;
5225
+}
5226
+.shithub-actions-step-list {
5227
+  margin: 0;
5228
+  padding: 0;
5229
+  border-top: 1px solid var(--border-default);
5230
+  list-style: none;
5231
+}
5232
+.shithub-actions-step-row {
5233
+  display: grid;
5234
+  grid-template-columns: 1rem minmax(0, 1fr) auto;
5235
+  align-items: center;
5236
+  gap: 0.65rem;
5237
+  min-height: 3.25rem;
5238
+  padding: 0.65rem 1rem;
5239
+  border-top: 1px solid var(--border-default);
5240
+  color: var(--fg-default);
5241
+  text-decoration: none;
5242
+}
5243
+.shithub-actions-step-list li:first-child .shithub-actions-step-row {
5244
+  border-top: 0;
5245
+}
5246
+.shithub-actions-step-row:hover {
5247
+  background: var(--canvas-subtle);
5248
+  text-decoration: none;
5249
+}
5250
+.shithub-actions-step-row > span:last-child {
5251
+  color: var(--fg-muted);
5252
+  font-size: 0.82rem;
5253
+  white-space: nowrap;
5254
+}
5255
+.shithub-actions-step-empty {
5256
+  padding: 0.8rem 1rem;
5257
+  color: var(--fg-muted);
5258
+}
5259
+.shithub-actions-log-panel {
5260
+  overflow: hidden;
5261
+  border: 1px solid var(--border-default);
5262
+  border-radius: 6px;
5263
+  background: var(--canvas-default);
5264
+}
5265
+.shithub-actions-log-panel > header {
5266
+  padding: 1rem;
5267
+  border-bottom: 1px solid var(--border-default);
5268
+}
5269
+.shithub-actions-log-panel h2 {
5270
+  margin: 0;
5271
+  font-size: 1rem;
5272
+}
5273
+.shithub-actions-log-panel p {
5274
+  margin: 0.25rem 0 0;
5275
+  color: var(--fg-muted);
5276
+}
5277
+.shithub-actions-log-output {
5278
+  overflow: auto;
5279
+  max-height: 72vh;
5280
+  margin: 0;
5281
+  padding: 1rem;
5282
+  background: #0d1117;
5283
+  color: #e6edf3;
5284
+  font: 0.82rem/1.45 ui-monospace, SFMono-Regular, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
5285
+  white-space: pre-wrap;
5286
+  word-break: break-word;
5287
+}
5288
+.shithub-actions-log-output code {
5289
+  font: inherit;
5290
+}
5291
+.shithub-actions-log-empty {
5292
+  padding: 2rem 1rem;
5293
+  color: var(--fg-muted);
5294
+  text-align: center;
5150
 }
5295
 }
5151
 .shithub-actions-annotations {
5296
 .shithub-actions-annotations {
5152
   margin-top: 1rem;
5297
   margin-top: 1rem;
internal/web/static/vendor/htmx/LICENSEadded
@@ -0,0 +1,13 @@
1
+Zero-Clause BSD
2
+=============
3
+
4
+Permission to use, copy, modify, and/or distribute this software for
5
+any purpose with or without fee is hereby granted.
6
+
7
+THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
8
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
9
+OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
10
+FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
11
+DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
12
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
13
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
internal/web/static/vendor/htmx/htmx.min.jsadded
@@ -0,0 +1,1 @@
1
+(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.12"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t){return new RegExp("<"+e+"(\\s[^>]*>|>)([\\s\\S]*?)<\\/"+e+">",!!t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/<body/.test(e)}function l(e){var t=!N(e);var r=A(e);var n=e;if(r==="head"){n=n.replace(S,"")}if(Q.config.useTemplateFragments&&t){var i=s("<body><template>"+n+"</template></body>",0);var a=i.querySelector("template").content;if(Q.config.allowScriptTags){oe(a.querySelectorAll("script"),function(e){if(Q.config.inlineScriptNonce){e.nonce=Q.config.inlineScriptNonce}e.htmxExecuted=navigator.userAgent.indexOf("Firefox")===-1})}else{oe(a.querySelectorAll("script"),function(e){_(e)})}return a}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return s("<table>"+n+"</table>",1);case"col":return s("<table><colgroup>"+n+"</colgroup></table>",2);case"tr":return s("<table><tbody>"+n+"</tbody></table>",2);case"td":case"th":return s("<table><tbody><tr>"+n+"</tr></tbody></table>",3);case"script":case"style":return s("<div>"+n+"</div>",1);default:return s(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function oe(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function X(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function D(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){b(e);return null}}function U(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function B(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function t(e){return Tr(re().body,function(){return eval(e)})}function F(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function j(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function _(e,t){e=p(e);if(t){setTimeout(function(){_(e);e=null},t)}else{e.parentElement.removeChild(e)}}function z(e,t,r){e=p(e);if(r){setTimeout(function(){z(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=p(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function $(e,t){e=p(e);e.classList.toggle(t)}function W(e,t){e=p(e);oe(e.parentElement.children,function(e){n(e,t)});z(e,t)}function v(e,t){e=p(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function G(e,t){return e.substring(e.length-t.length)===t}function J(e){var t=e.trim();if(g(t,"<")&&G(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function Z(e,t){if(t.indexOf("closest ")===0){return[v(e,J(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,J(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[K(e,J(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[Y(e,J(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(J(t))}}var K=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n<r.length;n++){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING){return i}}};var Y=function(e,t){var r=re().querySelectorAll(t);for(var n=r.length-1;n>=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return Z(e,t)[0]}else{return Z(re().body,e)[0]}}function p(e){if(I(e,"String")){return C(e)}else{return e}}function ve(e,t,r){if(k(t)){return{target:re().body,event:e,listener:t}}else{return{target:p(e),event:t,listener:r}}}function de(t,r,n){jr(function(){var e=ve(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=k(r);return e?r:n}function ge(t,r,n){jr(function(){var e=ve(t,r,n);e.target.removeEventListener(e.event,e.listener)});return k(r)?r:n}var pe=re().createElement("output");function me(e,t){var r=ne(e,t);if(r){if(r==="this"){return[xe(e,t)]}else{var n=Z(e,r);if(n.length===0){b('The selector "'+r+'" on '+t+" returned no matches!");return[pe]}else{return n}}}}function xe(e,t){return c(e,function(e){return te(e,t)!=null})}function ye(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return xe(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function be(e){var t=Q.config.attributesToSettle;for(var r=0;r<t.length;r++){if(e===t[r]){return true}}return false}function we(t,r){oe(t.attributes,function(e){if(!r.hasAttribute(e.name)&&be(e.name)){t.removeAttribute(e.name)}});oe(r.attributes,function(e){if(be(e.name)){t.setAttribute(e.name,e.value)}})}function Se(e,t){var r=Fr(t);for(var n=0;n<r.length;n++){var i=r[n];try{if(i.isInlineSwap(e)){return true}}catch(e){b(e)}}return e==="outerHTML"}function Ee(e,i,a){var t="#"+ee(i,"id");var o="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!Se(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Fe(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function Ce(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(var a=0;a<i.length;a++){var o=i[a].split(":",2);var s=o[0].trim();if(s.indexOf("#")===0){s=s.substring(1)}var l=o[1]||"true";var u=t.querySelector("#"+s);if(u){Ee(l,u,r)}}}oe(f(t,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=te(e,"hx-swap-oob");if(t!=null){Ee(t,e,r)}})}function Re(e){oe(f(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=te(e,"id");var r=re().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function Te(o,e,s){oe(e.querySelectorAll("[id]"),function(e){var t=ee(e,"id");if(t&&t.length>0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();we(e,i);s.tasks.push(function(){we(e,a)})}}})}function Oe(e){return function(){n(e,Q.config.addedClass);zt(e);Nt(e);qe(e);ce(e,"htmx:load")}}function qe(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Te(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;z(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Oe(i))}}}function He(e,t){var r=0;while(r<e.length){t=(t<<5)-t+e.charCodeAt(r++)|0}return t}function Le(e){var t=0;if(e.attributes){for(var r=0;r<e.attributes.length;r++){var n=e.attributes[r];if(n.value){t=He(n.name,t);t=He(n.value,t)}}}return t}function Ae(e){var t=ae(e);if(t.onHandlers){for(var r=0;r<t.onHandlers.length;r++){const n=t.onHandlers[r];e.removeEventListener(n.event,n.listener)}delete t.onHandlers}}function Ne(e){var t=ae(e);if(t.timeout){clearTimeout(t.timeout)}if(t.webSocket){t.webSocket.close()}if(t.sseEventSource){t.sseEventSource.close()}if(t.listenerInfos){oe(t.listenerInfos,function(e){if(e.on){e.on.removeEventListener(e.trigger,e.listener)}})}Ae(e);oe(Object.keys(t),function(e){delete t[e]})}function m(e){ce(e,"htmx:beforeCleanupElement");Ne(e);if(e.children){oe(e.children,function(e){m(e)})}}function Ie(t,e,r){if(t.tagName==="BODY"){return Ue(t,e,r)}else{var n;var i=t.previousSibling;a(u(t),t,e,r);if(i==null){n=u(t).firstChild}else{n=i.nextSibling}r.elts=r.elts.filter(function(e){return e!=t});while(n&&n!==t){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}m(t);u(t).removeChild(t)}}function ke(e,t,r){return a(e,e.firstChild,t,r)}function Pe(e,t,r){return a(u(e),e,t,r)}function Me(e,t,r){return a(e,null,t,r)}function Xe(e,t,r){return a(u(e),e.nextSibling,t,r)}function De(e,t,r){m(e);return u(e).removeChild(e)}function Ue(e,t,r){var n=e.firstChild;a(e,n,t,r);if(n){while(n.nextSibling){m(n.nextSibling);e.removeChild(n.nextSibling)}m(n);e.removeChild(n)}}function Be(e,t,r){var n=r||ne(e,"hx-select");if(n){var i=re().createDocumentFragment();oe(t.querySelectorAll(n),function(e){i.appendChild(e)});t=i}return t}function Fe(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":Ie(r,n,i);return;case"afterbegin":ke(r,n,i);return;case"beforebegin":Pe(r,n,i);return;case"beforeend":Me(r,n,i);return;case"afterend":Xe(r,n,i);return;case"delete":De(r,n,i);return;default:var a=Fr(t);for(var o=0;o<a.length;o++){var s=a[o];try{var l=s.handleSwap(e,r,n,i);if(l){if(typeof l.length!=="undefined"){for(var u=0;u<l.length;u++){var f=l[u];if(f.nodeType!==Node.TEXT_NODE&&f.nodeType!==Node.COMMENT_NODE){i.tasks.push(Oe(f))}}}return}}catch(e){b(e)}}if(e==="innerHTML"){Ue(r,n,i)}else{Fe(Q.config.defaultSwapStyle,t,r,n,i)}}}function Ve(e){if(e.indexOf("<title")>-1){var t=e.replace(H,"");var r=t.match(q);if(r){return r[2]}}}function je(e,t,r,n,i,a){i.title=Ve(n);var o=l(n);if(o){Ce(r,o,i);o=Be(r,o,a);Re(o);return Fe(e,r,t,o,i)}}function _e(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!P(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l<s.length;l++){ce(r,s[l].trim(),[])}}}var ze=/\s/;var x=/[\s,]/;var $e=/[_$a-zA-Z]/;var We=/[_$a-zA-Z0-9]/;var Ge=['"',"'","/"];var Je=/[^\s]/;var Ze=/[{(]/;var Ke=/[})]/;function Ye(e){var t=[];var r=0;while(r<e.length){if($e.exec(e.charAt(r))){var n=r;while(We.exec(e.charAt(r+1))){r++}t.push(e.substr(n,r-n+1))}else if(Ge.indexOf(e.charAt(r))!==-1){var i=e.charAt(r);var n=r;r++;while(r<e.length&&e.charAt(r)!==i){if(e.charAt(r)==="\\"){r++}r++}t.push(e.substr(n,r-n+1))}else{var a=e.charAt(r);t.push(a)}r++}return t}function Qe(e,t,r){return $e.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==r&&t!=="."}function et(e,t,r){if(t[0]==="["){t.shift();var n=1;var i=" return (function("+r+"){ return (";var a=null;while(t.length>0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Tr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Qe(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!t.test(e[0])){r+=e.shift()}return r}function tt(e){var t;if(e.length>0&&Ze.test(e[0])){e.shift();t=y(e,Ke).trim();e.shift()}else{t=y(e,x)}return t}var rt="input, textarea, select";function nt(e,t,r){var n=[];var i=Ye(t);do{y(i,Je);var a=i.length;var o=y(i,/[,\[\s]/);if(o!==""){if(o==="every"){var s={trigger:"every"};y(i,Je);s.pollInterval=d(y(i,/[,\[\s]/));y(i,Je);var l=et(e,i,"event");if(l){s.eventFilter=l}n.push(s)}else if(o.indexOf("sse:")===0){n.push({trigger:"sse",sseEvent:o.substr(4)})}else{var u={trigger:o};var l=et(e,i,"event");if(l){u.eventFilter=l}while(i.length>0&&i[0]!==","){y(i,Je);var f=i.shift();if(f==="changed"){u.changed=true}else if(f==="once"){u.once=true}else if(f==="consume"){u.consume=true}else if(f==="delay"&&i[0]===":"){i.shift();u.delay=d(y(i,x))}else if(f==="from"&&i[0]===":"){i.shift();if(Ze.test(i[0])){var c=tt(i)}else{var c=y(i,x);if(c==="closest"||c==="find"||c==="next"||c==="previous"){i.shift();var h=tt(i);if(h.length>0){c+=" "+h}}}u.from=c}else if(f==="target"&&i[0]===":"){i.shift();u.target=tt(i)}else if(f==="throttle"&&i[0]===":"){i.shift();u.throttle=d(y(i,x))}else if(f==="queue"&&i[0]===":"){i.shift();u.queue=y(i,x)}else if(f==="root"&&i[0]===":"){i.shift();u[f]=tt(i)}else if(f==="threshold"&&i[0]===":"){i.shift();u[f]=y(i,x)}else{fe(e,"htmx:syntax:error",{token:i.shift()})}}n.push(u)}}if(i.length===a){fe(e,"htmx:syntax:error",{token:i.shift()})}y(i,Je)}while(i[0]===","&&i.shift());if(r){r[t]=n}return n}function it(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Q.config.triggerSpecsCache;r=n&&n[t]||nt(e,t,n)}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,rt)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function at(e){ae(e).cancelled=true}function ot(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ct(r,e,Wt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}ot(e,t,r)}},r.pollInterval)}function st(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function lt(t,r,e){if(t.tagName==="A"&&st(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){ht(t,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(n,i,e,t)},r,e,true)})}}function ut(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ft(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ct(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function ht(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=Z(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(ft(a,e)){return}if(l||ut(e,a)){e.preventDefault()}if(ct(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle>0){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay>0){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var vt=false;var dt=null;function gt(){if(!dt){dt=function(){vt=true};window.addEventListener("scroll",dt);setInterval(function(){if(vt){vt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){pt(e)})}},200)}}function pt(t){if(!o(t,"data-hx-revealed")&&X(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function mt(e,t,r){var n=D(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){xt(e,a[1],0)}if(a[0]==="send"){bt(e)}}}function xt(s,r,n){if(!se(s)){return}if(r.indexOf("/")==0){var e=location.hostname+(location.port?":"+location.port:"");if(location.protocol=="https:"){r="wss://"+e+r}else if(location.protocol=="http:"){r="ws://"+e+r}}var t=Q.createWebSocket(r);t.onerror=function(e){fe(s,"htmx:wsError",{error:e,socket:t});yt(s)};t.onclose=function(e){if([1006,1012,1013].indexOf(e.code)>=0){var t=wt(n);setTimeout(function(){xt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(yt(s)){return}var t=e.data;R(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=M(n.children);for(var a=0;a<i.length;a++){var o=i[a];Ee(te(o,"hx-swap-oob")||"true",o,r)}nr(r.tasks)})}function yt(e){if(!se(e)){ae(e).webSocket.close();return true}}function bt(u){var f=c(u,function(e){return ae(e).webSocket!=null});if(f){u.addEventListener(it(u)[0].trigger,function(e){var t=ae(f).webSocket;var r=xr(u,f);var n=dr(u,"post");var i=n.errors;var a=n.values;var o=Hr(u);var s=le(a,o);var l=yr(s,u);l["HEADERS"]=r;if(i&&i.length>0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(ut(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function wt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}b('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function St(e,t,r){var n=D(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){Et(e,a[1])}if(a[0]==="swap"){Ct(e,a[1])}}}function Et(t,e){var r=Q.createEventSource(e);r.onerror=function(e){fe(t,"htmx:sseError",{error:e,source:r});Tt(t)};ae(t).sseEventSource=r}function Ct(a,o){var s=c(a,Ot);if(s){var l=ae(s).sseEventSource;var u=function(e){if(Tt(s)){return}if(!se(a)){l.removeEventListener(o,u);return}var t=e.data;R(a,function(e){t=e.transformResponse(t,null,a)});var r=wr(a);var n=ye(a);var i=T(a);je(r.swapStyle,n,a,t,i);nr(i.tasks);ce(a,"htmx:sseMessage",e)};ae(a).sseListener=u;l.addEventListener(o,u)}else{fe(a,"htmx:noSSESourceError")}}function Rt(e,t,r){var n=c(e,Ot);if(n){var i=ae(n).sseEventSource;var a=function(){if(!Tt(n)){if(se(e)){t(e)}else{i.removeEventListener(r,a)}}};ae(e).sseListener=a;i.addEventListener(r,a)}else{fe(e,"htmx:noSSESourceError")}}function Tt(e){if(!se(e)){ae(e).sseEventSource.close();return true}}function Ot(e){return ae(e).sseEventSource!=null}function qt(e,t,r,n){var i=function(){if(!r.loaded){r.loaded=true;t(e)}};if(n>0){setTimeout(i,n)}else{i()}}function Ht(t,i,e){var a=false;oe(w,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Lt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(r,n,e,t)})})}});return a}function Lt(n,e,t,r){if(e.sseEvent){Rt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){gt();ht(n,r,t,e);pt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t<e.length;t++){var r=e[t];if(r.isIntersecting){ce(n,"intersect");break}}},i);a.observe(n);ht(n,r,t,e)}else if(e.trigger==="load"){if(!ct(e,n,Wt("load",{elt:n}))){qt(n,r,t,e.delay)}}else if(e.pollInterval>0){t.polling=true;ot(n,r,e)}else{ht(n,r,t,e)}}function At(e){if(!e.htmxExecuted&&Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){b(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Nt(e){if(h(e,"script")){At(e)}oe(f(e,"script"),function(e){At(e)})}function It(e){var t=e.attributes;if(!t){return false}for(var r=0;r<t.length;r++){var n=t[r].name;if(g(n,"hx-on:")||g(n,"data-hx-on:")||g(n,"hx-on-")||g(n,"data-hx-on-")){return true}}return false}function kt(e){var t=null;var r=[];if(It(e)){r.push(e)}if(document.evaluate){var n=document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or'+' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]',e);while(t=n.iterateNext())r.push(t)}else if(typeof e.getElementsByTagName==="function"){var i=e.getElementsByTagName("*");for(var a=0;a<i.length;a++){if(It(i[a])){r.push(i[a])}}}return r}function Pt(e){if(e.querySelectorAll){var t=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";var r=e.querySelectorAll(i+t+", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws],"+" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");return r}else{return[]}}function Mt(e){var t=v(e.target,"button, input[type='submit']");var r=Dt(e);if(r){r.lastButtonClicked=t}}function Xt(e){var t=Dt(e);if(t){t.lastButtonClicked=null}}function Dt(e){var t=v(e.target,"button, input[type='submit']");if(!t){return}var r=p("#"+ee(t,"form"))||v(t,"form");if(!r){return}return ae(r)}function Ut(e){e.addEventListener("click",Mt);e.addEventListener("focusin",Mt);e.addEventListener("focusout",Xt)}function Bt(e){var t=Ye(e);var r=0;for(var n=0;n<t.length;n++){const i=t[n];if(i==="{"){r++}else if(i==="}"){r--}}return r}function Ft(t,e,r){var n=ae(t);if(!Array.isArray(n.onHandlers)){n.onHandlers=[]}var i;var a=function(e){return Tr(t,function(){if(!i){i=new Function("event",r)}i.call(t,e)})};t.addEventListener(e,a);n.onHandlers.push({event:e,listener:a})}function Vt(e){var t=te(e,"hx-on");if(t){var r={};var n=t.split("\n");var i=null;var a=0;while(n.length>0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Bt(o)}for(var l in r){Ft(e,l,r[l])}}}function jt(e){Ae(e);for(var t=0;t<e.attributes.length;t++){var r=e.attributes[t].name;var n=e.attributes[t].value;if(g(r,"hx-on")||g(r,"data-hx-on")){var i=r.indexOf("-on")+3;var a=r.slice(i,i+1);if(a==="-"||a===":"){var o=r.slice(i+1);if(g(o,":")){o="htmx"+o}else if(g(o,"-")){o="htmx:"+o.slice(1)}else if(g(o,"htmx-")){o="htmx:"+o.slice(5)}Ft(e,o,n)}}}}function _t(t){if(v(t,Q.config.disableSelector)){m(t);return}var r=ae(t);if(r.initHash!==Le(t)){Ne(t);r.initHash=Le(t);Vt(t);ce(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=it(t);var n=Ht(t,r,e);if(!n){if(ne(t,"hx-boost")==="true"){lt(t,r,e)}else if(o(t,"hx-trigger")){e.forEach(function(e){Lt(t,e,r,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&o(t,"form")){Ut(t)}var i=te(t,"hx-sse");if(i){St(t,r,i)}var a=te(t,"hx-ws");if(a){mt(t,r,a)}ce(t,"htmx:afterProcessNode")}}function zt(e){e=p(e);if(v(e,Q.config.disableSelector)){m(e);return}_t(e);oe(Pt(e),function(e){_t(e)});oe(kt(e),jt)}function $t(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Wt(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=re().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function fe(e,t,r){ce(e,t,le({error:t},r))}function Gt(e){return e==="htmx:afterProcessNode"}function R(e,t){oe(Fr(e),function(e){try{t(e)}catch(e){b(e)}})}function b(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function ce(e,t,r){e=p(e);if(r==null){r={}}r["elt"]=e;var n=Wt(t,r);if(Q.logger&&!Gt(t)){Q.logger(e,t,r)}if(r.error){b(r.error);ce(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=$t(t);if(i&&a!==t){var o=Wt(a,n.detail);i=i&&e.dispatchEvent(o)}R(e,function(e){i=i&&(e.onEvent(t,n)!==false&&!n.defaultPrevented)});return i}var Jt=location.pathname+location.search;function Zt(){var e=re().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||re().body}function Kt(e,t,r,n){if(!U()){return}if(Q.config.historyCacheSize<=0){localStorage.removeItem("htmx-history-cache");return}e=B(e);var i=E(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;a<i.length;a++){if(i[a].url===e){i.splice(a,1);break}}var o={url:e,content:t,title:r,scroll:n};ce(re().body,"htmx:historyItemCreated",{item:o,cache:i});i.push(o);while(i.length>Q.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Yt(e){if(!U()){return null}e=B(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r<t.length;r++){if(t[r].url===e){return t[r]}}return null}function Qt(e){var t=Q.config.requestClass;var r=e.cloneNode(true);oe(f(r,"."+t),function(e){n(e,t)});return r.innerHTML}function er(){var e=Zt();var t=Jt||location.pathname+location.search;var r;try{r=re().querySelector('[hx-history="false" i],[data-hx-history="false" i]')}catch(e){r=re().querySelector('[hx-history="false"],[data-hx-history="false"]')}if(!r){ce(re().body,"htmx:beforeHistorySave",{path:t,historyElt:e});Kt(t,Qt(e),re().title,window.scrollY)}if(Q.config.historyEnabled)history.replaceState({htmx:true},re().title,window.location.href)}function tr(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(G(e,"&")||G(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}Jt=e}function rr(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);Jt=e}function nr(e){oe(e,function(e){e.call()})}function ir(a){var e=new XMLHttpRequest;var o={path:a,xhr:e};ce(re().body,"htmx:historyCacheMiss",o);e.open("GET",a,true);e.setRequestHeader("HX-Request","true");e.setRequestHeader("HX-History-Restore-Request","true");e.setRequestHeader("HX-Current-URL",re().location.href);e.onload=function(){if(this.status>=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Zt();var r=T(t);var n=Ve(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ue(t,e,r);nr(r.tasks);Jt=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function ar(e){er();e=e||location.pathname+location.search;var t=Yt(e);if(t){var r=l(t.content);var n=Zt();var i=T(n);Ue(n,r,i);nr(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Jt=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{ir(e)}}}function or(e){var t=me(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function sr(e){var t=me(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function lr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ur(e,t){for(var r=0;r<e.length;r++){var n=e[r];if(n.isSameNode(t)){return true}}return false}function fr(e){if(e.name===""||e.name==null||e.disabled||v(e,"fieldset[disabled]")){return false}if(e.type==="button"||e.type==="submit"||e.tagName==="image"||e.tagName==="reset"||e.tagName==="file"){return false}if(e.type==="checkbox"||e.type==="radio"){return e.checked}return true}function cr(e,t,r){if(e!=null&&t!=null){var n=r[e];if(n===undefined){r[e]=t}else if(Array.isArray(n)){if(Array.isArray(t)){r[e]=n.concat(t)}else{n.push(t)}}else{if(Array.isArray(t)){r[e]=[n].concat(t)}else{r[e]=[n,t]}}}}function hr(t,r,n,e,i){if(e==null||ur(t,e)){return}else{t.push(e)}if(fr(e)){var a=ee(e,"name");var o=e.value;if(e.multiple&&e.tagName==="SELECT"){o=M(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e.files){o=M(e.files)}cr(a,o,r);if(i){vr(e,n)}}if(h(e,"form")){var s=e.elements;oe(s,function(e){hr(t,r,n,e,i)})}}function vr(e,t){if(e.willValidate){ce(e,"htmx:validation:validate");if(!e.checkValidity()){t.push({elt:e,message:e.validationMessage,validity:e.validity});ce(e,"htmx:validation:failed",{message:e.validationMessage,validity:e.validity})}}}function dr(e,t){var r=[];var n={};var i={};var a=[];var o=ae(e);if(o.lastButtonClicked&&!se(o.lastButtonClicked)){o.lastButtonClicked=null}var s=h(e,"form")&&e.noValidate!==true||te(e,"hx-validate")==="true";if(o.lastButtonClicked){s=s&&o.lastButtonClicked.formNoValidate!==true}if(t!=="get"){hr(r,i,a,v(e,"form"),s)}hr(r,n,a,e,s);if(o.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){var l=o.lastButtonClicked||e;var u=ee(l,"name");cr(u,l.value,i)}var f=me(e,"hx-include");oe(f,function(e){hr(r,n,a,e,s);if(!h(e,"form")){oe(e.querySelectorAll(rt),function(e){hr(r,n,a,e,s)})}});n=le(n,i);return{errors:a,values:n}}function gr(e,t,r){if(e!==""){e+="&"}if(String(r)==="[object Object]"){r=JSON.stringify(r)}var n=encodeURIComponent(r);e+=encodeURIComponent(t)+"="+n;return e}function pr(e){var t="";for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){oe(n,function(e){t=gr(t,r,e)})}else{t=gr(t,r,n)}}}return t}function mr(e){var t=new FormData;for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){oe(n,function(e){t.append(r,e)})}else{t.append(r,n)}}}return t}function xr(e,t,r){var n={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":re().location.href};Rr(e,"hx-headers",false,n);if(r!==undefined){n["HX-Prompt"]=r}if(ae(e).boosted){n["HX-Boosted"]="true"}return n}function yr(t,e){var r=ne(e,"hx-params");if(r){if(r==="none"){return{}}else if(r==="*"){return t}else if(r.indexOf("not ")===0){oe(r.substr(4).split(","),function(e){e=e.trim();delete t[e]});return t}else{var n={};oe(r.split(","),function(e){e=e.trim();n[e]=t[e]});return n}}else{return t}}function br(e){return ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function wr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!br(e)){n["show"]="top"}if(r){var i=D(r);if(i.length>0){for(var a=0;a<i.length;a++){var o=i[a];if(o.indexOf("swap:")===0){n["swapDelay"]=d(o.substr(5))}else if(o.indexOf("settle:")===0){n["settleDelay"]=d(o.substr(7))}else if(o.indexOf("transition:")===0){n["transition"]=o.substr(11)==="true"}else if(o.indexOf("ignoreTitle:")===0){n["ignoreTitle"]=o.substr(12)==="true"}else if(o.indexOf("scroll:")===0){var s=o.substr(7);var l=s.split(":");var u=l.pop();var f=l.length>0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{b("Unknown modifier in hx-swap: "+o)}}}}return n}function Sr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function Er(t,r,n){var i=null;R(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Sr(r)){return mr(n)}else{return pr(n)}}}function T(e){return{tasks:[],elts:[e]}}function Cr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Rr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Tr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Rr(u(e),t,r,n)}function Tr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Or(e,t){return Rr(e,"hx-vars",true,t)}function qr(e,t){return Rr(e,"hx-vals",false,t)}function Hr(e){return le(Or(e),qr(e))}function Lr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Ar(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Nr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||I(r,"String")){return he(e,t,null,null,{targetOverride:p(r),returnPromise:true})}else{return he(e,t,p(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:p(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Ir(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function kr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Mr;var X=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ye(n);if(u==null||u==pe){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var D=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var p=ne(n,"hx-sync");var m=null;var x=false;if(p){var B=p.split(":");var F=B[0].trim();if(F==="this"){g=xe(n,"hx-sync")}else{g=ue(n,F)}p=(B[1]||"drop").trim();f=ae(g);if(p==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(p==="abort"){if(f.xhr){ie(o);return l}else{x=true}}else if(p==="replace"){ce(g,"htmx:abort")}else if(p.indexOf("queue")===0){var V=p.split(" ");m=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(m==null){if(i){var y=ae(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){m=y.triggerSpec.queue}}if(m==null){m="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(m==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=xr(n,u,S);if(t!=="get"&&!Sr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=dr(n,t);var C=_.errors;var R=_.values;if(a.values){R=le(R,a.values)}var z=Hr(n);var $=le(R,z);var T=yr($,n);if(Q.config.getCacheBusterParam&&t==="get"){T["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=Rr(n,"hx-request");var W=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:T,unfilteredParameters:$,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;T=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(T).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=pr(T);if(L){A+="#"+L}}}if(!kr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Lr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,select:X,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Ir(n);I.pathInfo.responsePath=Ar(b);M(n,I);lr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=or(n);var P=sr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:Er(b,n,T);b.send(Y);return l}function Pr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Mr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){_e(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){er();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Nr("GET",r,v).then(function(){tr(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){if(f.getResponseHeader("HX-Retarget")==="this"){u.target=l}else{u.target=ue(l,f.getResponseHeader("HX-Retarget"))}}var d=Pr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var p=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:p},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;p=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){at(l)}R(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){er()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=wr(l,s);if(v.hasOwnProperty("ignoreTitle")){p=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var m=null;var x=null;var y=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){tr(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{rr(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=T(c);je(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}_e(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!p){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Cr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}_e(f,"HX-Trigger-After-Settle",r)}ie(m)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(x);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){m=e;x=t});var S=y;y=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(y,v.swapDelay)}else{y()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Xr={};function Dr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ur(e,t){if(t.init){t.init(r)}Xr[e]=le(Dr(),t)}function Br(e){delete Xr[e]}function Fr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Xr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Fr(u(e),r,n)}var Vr=false;re().addEventListener("DOMContentLoaded",function(){Vr=true});function jr(e){if(Vr||re().readyState==="complete"){e()}else{re().addEventListener("DOMContentLoaded",e)}}function _r(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","<style>                      ."+Q.config.indicatorClass+"{opacity:0}                      ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;}                      ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;}                    </style>")}}function zr(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function $r(){var e=zr();if(e){Q.config=le(Q.config,e)}}jr(function(){$r();_r();var e=re().body;zt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});const r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){ar();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()});
internal/web/templates/_layout.htmlmodified
@@ -27,6 +27,7 @@
27
   <link rel="stylesheet" href="/static/primer/primer.css" onerror="this.remove()">
27
   <link rel="stylesheet" href="/static/primer/primer.css" onerror="this.remove()">
28
   <link rel="stylesheet" href="/static/css/shithub.css">
28
   <link rel="stylesheet" href="/static/css/shithub.css">
29
   <link rel="stylesheet" href="/static/css/chroma.css">
29
   <link rel="stylesheet" href="/static/css/chroma.css">
30
+  {{ if flag . "UseHTMX" }}<script src="/static/vendor/htmx/htmx.min.js" defer></script>{{ end }}
30
 </head>
31
 </head>
31
 <body class="shithub-body">
32
 <body class="shithub-body">
32
 {{ template "nav" . }}
33
 {{ template "nav" . }}
internal/web/templates/repo/_action_run_status.htmladded
@@ -0,0 +1,10 @@
1
+{{ define "action-run-status" -}}
2
+{{ with .Run -}}
3
+<div id="shithub-actions-run-status" class="shithub-actions-run-status-wrap"{{ if not .IsTerminal }} hx-get="{{ .StatusHref }}" hx-trigger="every 2s" hx-swap="outerHTML"{{ end }}>
4
+  <span class="shithub-button shithub-actions-run-status shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }} {{ .StateText }}</span>
5
+  <span class="shithub-actions-run-status-meta">Updated <time datetime="{{ .UpdatedAt.Format "2006-01-02T15:04:05Z" }}">{{ relativeTime .UpdatedAt }}</time></span>
6
+</div>
7
+{{- else -}}
8
+<div id="shithub-actions-run-status" class="shithub-actions-run-status-wrap"></div>
9
+{{- end }}
10
+{{- end }}
internal/web/templates/repo/action_run.htmlmodified
@@ -3,25 +3,21 @@
3
 <section class="shithub-actions-run-page">
3
 <section class="shithub-actions-run-page">
4
   <header class="shithub-actions-run-head">
4
   <header class="shithub-actions-run-head">
5
     <div>
5
     <div>
6
-      <a href="/{{ .Owner }}/{{ .Repo.Name }}/actions" class="shithub-actions-back">← {{ .Run.AppSlug }}</a>
6
+      <a href="{{ .Run.ActionsHref }}" class="shithub-actions-back">{{ octicon "workflow" }} {{ .Run.Title }}</a>
7
       <h1>
7
       <h1>
8
         <span class="shithub-actions-state shithub-actions-state-{{ .Run.StateClass }}">{{ octicon .Run.StateIcon }}</span>
8
         <span class="shithub-actions-state shithub-actions-state-{{ .Run.StateClass }}">{{ octicon .Run.StateIcon }}</span>
9
-        {{ .Run.Title }} <span>#{{ .Run.ID }}</span>
9
+        {{ .Run.Title }} <span>#{{ .Run.RunIndex }}</span>
10
       </h1>
10
       </h1>
11
       <p>
11
       <p>
12
-        {{ if .Run.PullNumber }}
12
+        Triggered via {{ .Run.EventLabel }}
13
-          {{ if .Run.PullAuthorUsername }}<a href="/{{ .Run.PullAuthorUsername }}">{{ .Run.PullAuthorUsername }}</a>{{ end }}
13
+        {{ if .Run.ActorUsername }}by <a href="/{{ .Run.ActorUsername }}">{{ .Run.ActorUsername }}</a>{{ end }}
14
-          opened <a href="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .Run.PullNumber }}">#{{ .Run.PullNumber }}</a>
14
+        for <a href="/{{ .Owner }}/{{ .Repo.Name }}/commit/{{ .Run.HeadSha }}"><code>{{ .Run.HeadShaShort }}</code></a>
15
-          {{ if .Run.HeadRef }}from <a class="shithub-branch-name" href="/{{ .Owner }}/{{ .Repo.Name }}/tree/{{ .Run.HeadRef }}">{{ .Run.HeadRef }}</a>{{ end }}
15
+        {{ if .Run.HeadRef }}on <a class="shithub-branch-name" href="/{{ .Owner }}/{{ .Repo.Name }}/tree/{{ .Run.HeadRef }}">{{ .Run.HeadRef }}</a>{{ end }}
16
-          {{ if .Run.BaseRef }}into <a class="shithub-branch-name" href="/{{ .Owner }}/{{ .Repo.Name }}/tree/{{ .Run.BaseRef }}">{{ .Run.BaseRef }}</a>{{ end }}
17
-        {{ else }}
18
-          Check suite for <a href="/{{ .Owner }}/{{ .Repo.Name }}/commit/{{ .Run.HeadSha }}"><code>{{ .Run.HeadShaShort }}</code></a>
19
-        {{ end }}
20
       </p>
16
       </p>
21
     </div>
17
     </div>
22
     <div class="shithub-actions-run-head-actions">
18
     <div class="shithub-actions-run-head-actions">
23
-      <span class="shithub-button shithub-actions-run-status shithub-actions-state-{{ .Run.StateClass }}">{{ octicon .Run.StateIcon }} {{ .Run.StateText }}</span>
19
+      {{ template "action-run-status" . }}
24
-      <a class="shithub-button" href="/{{ .Owner }}/{{ .Repo.Name }}/tree/{{ if .Run.HeadRef }}{{ .Run.HeadRef }}{{ else }}{{ .Run.HeadSha }}{{ end }}">{{ octicon "code" }} Code</a>
20
+      <a class="shithub-button" href="{{ .Run.CodeHref }}">{{ octicon "code" }} Code</a>
25
     </div>
21
     </div>
26
   </header>
22
   </header>
27
 
23
 
@@ -29,18 +25,19 @@
29
     <aside class="shithub-actions-sidebar shithub-actions-run-sidebar" aria-label="Run navigation">
25
     <aside class="shithub-actions-sidebar shithub-actions-run-sidebar" aria-label="Run navigation">
30
       <a href="#summary" class="shithub-actions-nav-item is-active" aria-current="page">{{ octicon "home" }} <span>Summary</span></a>
26
       <a href="#summary" class="shithub-actions-nav-item is-active" aria-current="page">{{ octicon "home" }} <span>Summary</span></a>
31
       <div class="shithub-actions-sidebar-section">
27
       <div class="shithub-actions-sidebar-section">
32
-        <h2>All jobs</h2>
28
+        <h2>Jobs</h2>
33
-        {{ range .Run.Runs }}
29
+        {{ range .Run.Jobs }}
34
-          <a href="#job-{{ .ID }}" class="shithub-actions-nav-item">
30
+          <a href="#{{ .Anchor }}" class="shithub-actions-nav-item">
35
             <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
31
             <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
36
             <span>{{ .Name }}</span>
32
             <span>{{ .Name }}</span>
37
           </a>
33
           </a>
34
+        {{ else }}
35
+          <p>No jobs were created for this run.</p>
38
         {{ end }}
36
         {{ end }}
39
       </div>
37
       </div>
40
       <div class="shithub-actions-sidebar-section">
38
       <div class="shithub-actions-sidebar-section">
41
         <h2>Run details</h2>
39
         <h2>Run details</h2>
42
-        <span class="shithub-actions-nav-item">{{ octicon "pulse" }} <span>Usage</span></span>
40
+        <span class="shithub-actions-nav-item">{{ octicon "file" }} <span>{{ .Run.WorkflowFile }}</span></span>
43
-        <span class="shithub-actions-nav-item">{{ octicon "file" }} <span>Workflow file</span></span>
44
       </div>
41
       </div>
45
     </aside>
42
     </aside>
46
 
43
 
@@ -48,7 +45,7 @@
48
       <section id="summary" class="shithub-actions-summary-strip">
45
       <section id="summary" class="shithub-actions-summary-strip">
49
         <div>
46
         <div>
50
           <span>Triggered via</span>
47
           <span>Triggered via</span>
51
-          <strong>{{ if .Run.PullNumber }}pull request{{ else }}checks API{{ end }}</strong>
48
+          <strong>{{ .Run.EventLabel }}</strong>
52
         </div>
49
         </div>
53
         <div>
50
         <div>
54
           <span>Status</span>
51
           <span>Status</span>
@@ -60,56 +57,69 @@
60
         </div>
57
         </div>
61
         <div>
58
         <div>
62
           <span>Artifacts</span>
59
           <span>Artifacts</span>
63
-          <strong>—</strong>
60
+          <strong>{{ .Run.ArtifactCount }}</strong>
64
         </div>
61
         </div>
65
       </section>
62
       </section>
66
 
63
 
67
       <section class="shithub-actions-workflow-card">
64
       <section class="shithub-actions-workflow-card">
68
         <header>
65
         <header>
69
           <div>
66
           <div>
70
-            <h2><a href="/{{ .Owner }}/{{ .Repo.Name }}/actions">{{ .Run.AppSlug }}.yml</a></h2>
67
+            <h2>{{ .Run.WorkflowFile }}</h2>
71
-            <p>on: {{ if .Run.PullNumber }}pull_request{{ else }}check_run{{ end }}</p>
68
+            <p>{{ .Run.CompletedCount }} of {{ .Run.JobCount }} jobs finished{{ if .Run.FailureCount }} · {{ .Run.FailureCount }} failed{{ end }}</p>
72
           </div>
69
           </div>
73
         </header>
70
         </header>
74
-        <div class="shithub-actions-workflow-graph">
71
+        {{ if .Run.Jobs }}
75
-          <div class="shithub-actions-workflow-stage">
72
+          <div class="shithub-actions-workflow-graph" aria-label="Workflow job graph">
76
-            {{ range .Run.Runs }}
73
+            <div class="shithub-actions-workflow-stages">
77
-              <a id="job-{{ .ID }}" href="{{ if .DetailsURL }}{{ .DetailsURL }}{{ else }}#job-{{ .ID }}{{ end }}" class="shithub-actions-job-card">
74
+              {{ range .Run.Stages }}
78
-                <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
75
+                <div class="shithub-actions-workflow-stage">
79
-                <strong>{{ .Name }}</strong>
76
+                  {{ range .Jobs }}
80
-                <span>{{ .Duration }}</span>
77
+                    <a href="#{{ .Anchor }}" class="shithub-actions-job-card">
81
-              </a>
78
+                      <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
82
-            {{ end }}
79
+                      <strong>{{ .Name }}</strong>
80
+                      <span>{{ .Duration }}</span>
81
+                      {{ if .NeedsText }}<small>needs {{ .NeedsText }}</small>{{ end }}
82
+                    </a>
83
+                  {{ end }}
84
+                </div>
85
+              {{ end }}
86
+            </div>
83
           </div>
87
           </div>
84
-          <div class="shithub-actions-graph-controls" aria-hidden="true">
88
+        {{ else }}
85
-            <button type="button" class="shithub-icon-button">{{ octicon "screen-full" }}</button>
89
+          <div class="shithub-actions-empty shithub-actions-empty-compact">
86
-            <button type="button" class="shithub-icon-button">{{ octicon "dash" }}</button>
90
+            <h2>No jobs</h2>
87
-            <button type="button" class="shithub-icon-button">{{ octicon "plus" }}</button>
91
+            <p>This workflow run has not materialized any jobs yet.</p>
88
           </div>
92
           </div>
89
-        </div>
93
+        {{ end }}
90
       </section>
94
       </section>
91
 
95
 
92
-      <section class="shithub-actions-annotations">
96
+      <section class="shithub-actions-jobs" aria-label="Workflow jobs">
93
-        <header>
97
+        {{ range .Run.Jobs }}
94
-          <h2>Annotations</h2>
98
+          <details id="{{ .Anchor }}" class="shithub-actions-job-detail" open>
95
-          <p>{{ .Run.AnnotationCount }} warning{{ if ne .Run.AnnotationCount 1 }}s{{ end }}</p>
99
+            <summary>
96
-        </header>
100
+              <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
97
-        {{ if .Run.AnnotationCount }}
101
+              <span>
98
-          <div class="shithub-actions-annotation-list">
102
+                <strong>{{ .Name }}</strong>
99
-            {{ range .Run.Runs }}
103
+                <small>{{ .StateText }} · {{ .Duration }}{{ if .RunsOn }} · runs-on {{ .RunsOn }}{{ end }}{{ if .NeedsText }} · needs {{ .NeedsText }}{{ end }}</small>
100
-              {{ if .SummaryHTML }}
104
+              </span>
101
-                <article class="shithub-actions-annotation">
105
+            </summary>
102
-                  {{ octicon "alert" }}
106
+            <ol class="shithub-actions-step-list">
103
-                  <div>
107
+              {{ range .Steps }}
104
-                    <strong>{{ .Name }}</strong>
108
+                <li>
105
-                    <div class="markdown-body">{{ .SummaryHTML }}</div>
109
+                  <a href="{{ .LogHref }}" class="shithub-actions-step-row">
106
-                  </div>
110
+                    <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
107
-                </article>
111
+                    <span>
112
+                      <strong>{{ .Name }}</strong>
113
+                      {{ if .Detail }}<small>{{ .Kind }} · {{ .Detail }}</small>{{ else }}<small>{{ .Kind }}</small>{{ end }}
114
+                    </span>
115
+                    <span>{{ .Duration }}</span>
116
+                  </a>
117
+                </li>
118
+              {{ else }}
119
+                <li class="shithub-actions-step-empty">No steps were created for this job.</li>
108
               {{ end }}
120
               {{ end }}
109
-            {{ end }}
121
+            </ol>
110
-          </div>
122
+          </details>
111
-        {{ else }}
112
-          <p class="shithub-muted">No annotations were reported for this run.</p>
113
         {{ end }}
123
         {{ end }}
114
       </section>
124
       </section>
115
     </div>
125
     </div>
internal/web/templates/repo/action_run_status.htmladded
@@ -0,0 +1,3 @@
1
+{{ define "page" -}}
2
+{{ template "action-run-status" . }}
3
+{{- end }}
internal/web/templates/repo/action_step_log.htmladded
@@ -0,0 +1,55 @@
1
+{{ define "page" -}}
2
+{{ template "repo-header" . }}
3
+<section class="shithub-actions-run-page shithub-actions-log-page">
4
+  <header class="shithub-actions-run-head">
5
+    <div>
6
+      <a href="{{ .Log.BackHref }}" class="shithub-actions-back">{{ octicon "workflow" }} {{ .Log.Run.Title }} #{{ .Log.Run.RunIndex }}</a>
7
+      <h1>
8
+        <span class="shithub-actions-state shithub-actions-state-{{ .Log.Step.StateClass }}">{{ octicon .Log.Step.StateIcon }}</span>
9
+        {{ .Log.Step.Name }}
10
+      </h1>
11
+      <p>
12
+        {{ .Log.Job.Name }} · {{ .Log.Step.StateText }} · {{ .Log.Step.Duration }}
13
+        {{ if .Log.Step.LogByteCount }} · {{ .Log.Step.LogByteCount }} bytes{{ end }}
14
+      </p>
15
+    </div>
16
+    <div class="shithub-actions-run-head-actions">
17
+      <a class="shithub-button" href="{{ .Log.BackHref }}">{{ octicon "list-unordered" }} Run</a>
18
+      {{ if .Log.DownloadURL }}<a class="shithub-button" href="{{ .Log.DownloadURL }}">{{ octicon "download" }} Download</a>{{ end }}
19
+    </div>
20
+  </header>
21
+
22
+  <div class="shithub-actions-run-layout">
23
+    <aside class="shithub-actions-sidebar shithub-actions-run-sidebar" aria-label="Run navigation">
24
+      <a href="{{ .Log.BackHref }}" class="shithub-actions-nav-item">{{ octicon "home" }} <span>Summary</span></a>
25
+      <div class="shithub-actions-sidebar-section">
26
+        <h2>Steps</h2>
27
+        {{ range .Log.Job.Steps }}
28
+          <a href="{{ .LogHref }}" class="shithub-actions-nav-item{{ if eq .StepIndex $.Log.Step.StepIndex }} is-active{{ end }}"{{ if eq .StepIndex $.Log.Step.StepIndex }} aria-current="page"{{ end }}>
29
+            <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
30
+            <span>{{ .Name }}</span>
31
+          </a>
32
+        {{ end }}
33
+      </div>
34
+    </aside>
35
+
36
+    <div class="shithub-actions-run-main">
37
+      <section class="shithub-actions-log-panel">
38
+        <header>
39
+          <div>
40
+            <h2>{{ .Log.Step.Name }}</h2>
41
+            <p>{{ .Log.LogSource }}{{ if .Log.LogTruncated }} · truncated to 1 MiB{{ end }}</p>
42
+          </div>
43
+        </header>
44
+        {{ if .Log.LogError }}
45
+          <p class="shithub-actions-log-empty">{{ .Log.LogError }}</p>
46
+        {{ else if .Log.LogText }}
47
+          <pre class="shithub-actions-log-output"><code>{{ .Log.LogText }}</code></pre>
48
+        {{ else }}
49
+          <p class="shithub-actions-log-empty">No log output has been recorded for this step.</p>
50
+        {{ end }}
51
+      </section>
52
+    </div>
53
+  </div>
54
+</section>
55
+{{- end }}