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
91
+	WorkflowFile   string
92
+	WorkflowName   string
89
 	Title          string
93
 	Title          string
90
 	HeadSha        string
94
 	HeadSha        string
91
 	HeadShaShort   string
95
 	HeadShaShort   string
92
-	PullNumber         int64
93
-	PullAuthorUsername string
94
 	HeadRef        string
96
 	HeadRef        string
95
-	BaseRef            string
97
+	Event          string
96
-	RunCount           int
98
+	EventLabel     string
99
+	ActorUsername  string
97
 	StateText      string
100
 	StateText      string
98
 	StateClass     string
101
 	StateClass     string
99
 	StateIcon      string
102
 	StateIcon      string
100
 	CreatedAt      time.Time
103
 	CreatedAt      time.Time
101
 	UpdatedAt      time.Time
104
 	UpdatedAt      time.Time
102
 	Duration       string
105
 	Duration       string
103
-	Runs               []actionsRunView
106
+	IsTerminal     bool
104
-	AnnotationCount    int
107
+	StatusHref     string
108
+	ActionsHref    string
109
+	CodeHref       string
110
+	ArtifactCount  int
111
+	JobCount       int
112
+	CompletedCount int
113
+	FailureCount   int
114
+	Jobs           []actionsJobDetailView
115
+	Stages         []actionsJobStageView
116
+}
117
+
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
105
 }
133
 }
106
 
134
 
107
-type actionsRunView struct {
135
+type actionsStepDetailView struct {
108
 	ID           int64
136
 	ID           int64
137
+	StepIndex    int32
138
+	StepID       string
109
 	Name         string
139
 	Name         string
140
+	Kind         string
141
+	Detail       string
110
 	StateText    string
142
 	StateText    string
111
 	StateClass   string
143
 	StateClass   string
112
 	StateIcon    string
144
 	StateIcon    string
113
 	Duration     string
145
 	Duration     string
114
-	CompletedAt time.Time
146
+	LogByteCount int64
115
-	DetailsURL  string
147
+	LogHref      string
116
-	SummaryHTML template.HTML
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")
541
-	if err != nil {
589
+	if !ok {
542
 		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
590
 		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
543
 		return
591
 		return
544
 	}
592
 	}
545
-	suite, err := h.cq.GetCheckSuiteForRepo(r.Context(), h.d.Pool, checksdb.GetCheckSuiteForRepoParams{
593
+	view, err := h.loadActionsRunDetail(r.Context(), row.ID, owner.Username, row.Name, runIndex)
546
-		RepoID: row.ID,
547
-		ID:     suiteID,
548
-	})
549
 	if err != nil {
594
 	if err != nil {
550
 		if errors.Is(err, pgx.ErrNoRows) {
595
 		if errors.Is(err, pgx.ErrNoRows) {
551
 			h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
596
 			h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
552
 		} else {
597
 		} else {
553
-			h.d.Logger.WarnContext(r.Context(), "repo actions: get suite", "suite_id", suiteID, "error", err)
598
+			h.d.Logger.WarnContext(r.Context(), "repo actions: get run detail", "repo_id", row.ID, "run_index", runIndex, "error", err)
554
 			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
599
 			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
555
 		}
600
 		}
556
 		return
601
 		return
557
 	}
602
 	}
558
-	runs, err := h.cq.ListCheckRunsBySuite(r.Context(), h.d.Pool, suite.ID)
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 {
620
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
621
+		return
622
+	}
623
+	view, err := h.loadActionsRunDetail(r.Context(), row.ID, owner.Username, row.Name, runIndex)
559
 	if err != nil {
624
 	if err != nil {
560
-		h.d.Logger.WarnContext(r.Context(), "repo actions: get suite runs", "suite_id", suiteID, "error", err)
625
+		if errors.Is(err, pgx.ErrNoRows) {
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)
561
 			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
629
 			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
630
+		}
562
 		return
631
 		return
563
 	}
632
 	}
564
 
633
 
565
-	view := actionsSuiteViewFromGetRow(suite, runs)
566
 	data := h.repoHeaderData(r, row, owner.Username, "actions")
634
 	data := h.repoHeaderData(r, row, owner.Username, "actions")
567
-	data["Title"] = view.Title + " · " + row.Name
568
 	data["Run"] = view
635
 	data["Run"] = view
569
-	data["CSRFToken"] = middleware.CSRFTokenForRequest(r)
636
+	if err := h.d.Render.RenderFragment(w, "repo/action_run_status", data); err != nil {
570
-	if err := h.d.Render.RenderPage(w, r, "repo/action_run", data); err != nil {
637
+		h.d.Logger.ErrorContext(r.Context(), "repo action run status render", "run_index", runIndex, "error", err)
571
-		h.d.Logger.ErrorContext(r.Context(), "repo action run render", "suite_id", suiteID, "error", err)
638
+	}
572
-	}
573
-}
574
-
575
-func actionsSuiteViewFromGetRow(row checksdb.GetCheckSuiteForRepoRow, runs []checksdb.CheckRun) actionsSuiteView {
576
-	return actionsSuiteViewFromParts(
577
-		row.ID,
578
-		row.HeadSha,
579
-		row.AppSlug,
580
-		row.Status,
581
-		row.Conclusion,
582
-		row.CreatedAt.Time,
583
-		row.UpdatedAt.Time,
584
-		row.PullNumber,
585
-		row.PullTitle,
586
-		row.PullAuthorUsername,
587
-		row.HeadRef,
588
-		row.BaseRef,
589
-		runs,
590
-	)
591
 }
639
 }
592
 
640
 
593
-func actionsSuiteViewFromParts(
641
+func (h *Handlers) repoActionStepLog(w http.ResponseWriter, r *http.Request) {
594
-	id int64,
642
+	row, owner, ok := h.loadRepoAndAuthorize(w, r, policy.ActionRepoRead)
595
-	headSHA string,
643
+	if !ok {
596
-	appSlug string,
644
+		return
597
-	status checksdb.CheckStatus,
598
-	conclusion checksdb.NullCheckConclusion,
599
-	createdAt time.Time,
600
-	updatedAt time.Time,
601
-	pullNumber int64,
602
-	pullTitle string,
603
-	pullAuthorUsername string,
604
-	headRef string,
605
-	baseRef string,
606
-	runs []checksdb.CheckRun,
607
-) actionsSuiteView {
608
-	title := pullTitle
609
-	if title == "" {
610
-		title = appSlug + " checks for " + shortSHA(headSHA)
611
-	}
612
-	stateText, stateClass, stateIcon := checkActionState(status, conclusion)
613
-	runViews := make([]actionsRunView, 0, len(runs))
614
-	annotationCount := 0
615
-	for _, run := range runs {
616
-		view := actionsRunViewFromRun(run)
617
-		if view.SummaryHTML != "" {
618
-			annotationCount++
619
 	}
645
 	}
620
-		runViews = append(runViews, view)
646
+	runIndex, ok := parsePositiveInt64Param(r, "runIndex")
647
+	if !ok {
648
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
649
+		return
621
 	}
650
 	}
622
-	return actionsSuiteView{
651
+	jobIndex, ok := parseNonNegativeInt32Param(r, "jobIndex")
623
-		ID:                 id,
652
+	if !ok {
624
-		AppSlug:            appSlug,
653
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
625
-		Title:              title,
654
+		return
626
-		HeadSha:            headSHA,
655
+	}
627
-		HeadShaShort:       shortSHA(headSHA),
656
+	stepIndex, ok := parseNonNegativeInt32Param(r, "stepIndex")
628
-		PullNumber:         pullNumber,
657
+	if !ok {
629
-		PullAuthorUsername: pullAuthorUsername,
658
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
630
-		HeadRef:            headRef,
659
+		return
631
-		BaseRef:            baseRef,
660
+	}
632
-		RunCount:           len(runs),
661
+	run, err := h.loadActionsRunDetail(r.Context(), row.ID, owner.Username, row.Name, runIndex)
662
+	if err != nil {
663
+		if errors.Is(err, pgx.ErrNoRows) {
664
+			h.d.Render.HTTPError(w, r, http.StatusNotFound, "")
665
+		} else {
666
+			h.d.Logger.WarnContext(r.Context(), "repo actions: get run for step log", "repo_id", row.ID, "run_index", runIndex, "error", err)
667
+			h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
668
+		}
669
+		return
670
+	}
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)
678
+	if err != nil {
679
+		h.d.Logger.WarnContext(r.Context(), "repo actions: load step log", "step_id", step.ID, "error", err)
680
+		h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
681
+		return
682
+	}
683
+
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
+	}
695
+	data := h.repoHeaderData(r, row, owner.Username, "actions")
696
+	data["Title"] = step.Name + " · " + run.Title + " #" + strconv.FormatInt(run.RunIndex, 10)
697
+	data["Log"] = view
698
+	if err := h.d.Render.RenderPage(w, r, "repo/action_step_log", 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)
700
+	}
701
+}
702
+
703
+func (h *Handlers) loadActionsRunDetail(ctx context.Context, repoID int64, owner, repoName string, runIndex int64) (actionsRunDetailView, error) {
704
+	q := actionsdb.New()
705
+	run, err := q.GetWorkflowRunForRepoByIndex(ctx, h.d.Pool, actionsdb.GetWorkflowRunForRepoByIndexParams{
706
+		RepoID:   repoID,
707
+		RunIndex: runIndex,
708
+	})
709
+	if err != nil {
710
+		return actionsRunDetailView{}, err
711
+	}
712
+	jobs, err := q.ListJobsForRun(ctx, h.d.Pool, run.ID)
713
+	if err != nil {
714
+		return actionsRunDetailView{}, err
715
+	}
716
+	artifacts, err := q.ListArtifactsForRun(ctx, h.d.Pool, run.ID)
717
+	if err != nil {
718
+		return actionsRunDetailView{}, err
719
+	}
720
+
721
+	basePath := "/" + owner + "/" + repoName + "/actions"
722
+	runPath := basePath + "/runs/" + strconv.FormatInt(run.RunIndex, 10)
723
+	now := time.Now()
724
+	stateText, stateClass, stateIcon := workflowRunState(run.Status, run.Conclusion)
725
+	updatedAt := pgTime(run.UpdatedAt, run.CreatedAt.Time)
726
+	view := actionsRunDetailView{
727
+		ID:             run.ID,
728
+		RunIndex:       run.RunIndex,
729
+		WorkflowFile:   run.WorkflowFile,
730
+		WorkflowName:   run.WorkflowName,
731
+		Title:          workflowDisplayName(run.WorkflowName, run.WorkflowFile),
732
+		HeadSha:        run.HeadSha,
733
+		HeadShaShort:   shortSHA(run.HeadSha),
734
+		HeadRef:        run.HeadRef,
735
+		Event:          string(run.Event),
736
+		EventLabel:     workflowRunEventLabel(string(run.Event)),
737
+		ActorUsername:  run.ActorUsername,
633
 		StateText:      stateText,
738
 		StateText:      stateText,
634
 		StateClass:     stateClass,
739
 		StateClass:     stateClass,
635
 		StateIcon:      stateIcon,
740
 		StateIcon:      stateIcon,
636
-		CreatedAt:          createdAt,
741
+		CreatedAt:      run.CreatedAt.Time,
637
 		UpdatedAt:      updatedAt,
742
 		UpdatedAt:      updatedAt,
638
-		Duration:           actionSuiteDuration(runs, createdAt, updatedAt),
743
+		Duration:       workflowRunDuration(run.Status, run.StartedAt, run.CompletedAt, run.CreatedAt, updatedAt, now),
639
-		Runs:               runViews,
744
+		IsTerminal:     workflowRunTerminal(run.Status),
640
-		AnnotationCount:    annotationCount,
745
+		StatusHref:     runPath + "/status",
746
+		ActionsHref:    basePath,
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
758
+		}
759
+		jobView := actionsJobDetailViewFromRow(job, owner, repoName, run.RunIndex, now)
760
+		jobView.Steps = make([]actionsStepDetailView, 0, len(steps))
761
+		for _, step := range steps {
762
+			jobView.Steps = append(jobView.Steps, actionsStepDetailViewFromRow(step, owner, repoName, run.RunIndex, job.JobIndex, now))
763
+		}
764
+		if job.Status == actionsdb.WorkflowJobStatusCompleted || job.Status == actionsdb.WorkflowJobStatusCancelled || job.Status == actionsdb.WorkflowJobStatusSkipped {
765
+			view.CompletedCount++
766
+		}
767
+		if jobView.StateClass == "failure" {
768
+			view.FailureCount++
641
 		}
769
 		}
770
+		view.Jobs = append(view.Jobs, jobView)
771
+	}
772
+	view.Stages = actionsJobStages(view.Jobs)
773
+	return view, nil
642
 }
774
 }
643
 
775
 
644
-func actionsRunViewFromRun(run checksdb.CheckRun) actionsRunView {
776
+func actionsJobDetailViewFromRow(row actionsdb.ListJobsForRunRow, owner, repoName string, runIndex int64, now time.Time) actionsJobDetailView {
645
-	stateText, stateClass, stateIcon := checkActionState(run.Status, run.Conclusion)
777
+	stateText, stateClass, stateIcon := workflowJobState(row.Status, row.Conclusion)
646
-	start := run.CreatedAt.Time
778
+	name := strings.TrimSpace(row.JobName)
647
-	if run.StartedAt.Valid {
779
+	if name == "" {
648
-		start = run.StartedAt.Time
780
+		name = row.JobKey
649
 	}
781
 	}
650
-	end := run.UpdatedAt.Time
782
+	return actionsJobDetailView{
651
-	if run.CompletedAt.Valid {
783
+		ID:         row.ID,
652
-		end = run.CompletedAt.Time
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),
653
 	}
795
 	}
654
-	return actionsRunView{
796
+}
655
-		ID:          run.ID,
797
+
656
-		Name:        run.Name,
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,
657
 		StateText:    stateText,
808
 		StateText:    stateText,
658
 		StateClass:   stateClass,
809
 		StateClass:   stateClass,
659
 		StateIcon:    stateIcon,
810
 		StateIcon:    stateIcon,
660
-		Duration:    formatDuration(end.Sub(start)),
811
+		Duration:     actionItemDuration(string(row.Status), string(actionsdb.WorkflowStepStatusQueued), row.StartedAt, row.CompletedAt, row.CreatedAt, row.UpdatedAt, now),
661
-		CompletedAt: end,
812
+		LogByteCount: row.LogByteCount,
662
-		DetailsURL:  run.DetailsUrl,
813
+		LogHref: "/" + owner + "/" + repoName + "/actions/runs/" + strconv.FormatInt(runIndex, 10) +
663
-		SummaryHTML: renderCheckSummary(run.Output),
814
+			"/jobs/" + strconv.FormatInt(int64(jobIndex), 10) +
815
+			"/steps/" + strconv.FormatInt(int64(row.StepIndex), 10),
664
 	}
816
 	}
665
 }
817
 }
666
 
818
 
667
-func checkActionState(status checksdb.CheckStatus, conclusion checksdb.NullCheckConclusion) (string, string, string) {
819
+func workflowStepDisplay(row actionsdb.ListStepsForJobRow) (name, kind, detail string) {
668
-	if !conclusion.Valid {
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
+	}
669
 	switch status {
905
 	switch status {
670
-		case checksdb.CheckStatusCompleted:
906
+	case actionsdb.WorkflowJobStatusQueued:
671
-			return "Completed", "neutral", "check-circle"
907
+		return "Queued", "pending", "dot-fill"
672
-		case checksdb.CheckStatusInProgress:
908
+	case actionsdb.WorkflowJobStatusRunning:
673
 		return "In progress", "running", "dot-fill"
909
 		return "In progress", "running", "dot-fill"
674
-		case checksdb.CheckStatusQueued, checksdb.CheckStatusPending:
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:
675
 		return "Queued", "pending", "dot-fill"
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"
676
 	default:
938
 	default:
939
+		if conclusion.Valid {
940
+			return workflowConclusionState(conclusion.CheckConclusion)
941
+		}
677
 		return string(status), "neutral", "dot-fill"
942
 		return string(status), "neutral", "dot-fill"
678
 	}
943
 	}
679
 }
944
 }
680
-	switch conclusion.CheckConclusion {
945
+
681
-	case checksdb.CheckConclusionSuccess, checksdb.CheckConclusionSkipped, checksdb.CheckConclusionNeutral:
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"
689
 	}
956
 	}
690
 }
957
 }
691
 
958
 
692
-func actionSuiteDuration(runs []checksdb.CheckRun, createdAt, updatedAt time.Time) string {
959
+func workflowRunTerminal(status actionsdb.WorkflowRunStatus) bool {
693
-	if len(runs) == 0 {
960
+	return status == actionsdb.WorkflowRunStatusCompleted || status == actionsdb.WorkflowRunStatusCancelled
694
-		return formatDuration(updatedAt.Sub(createdAt))
695
 }
961
 }
696
-	var start, end time.Time
962
+
697
-	for _, run := range runs {
963
+func actionItemDuration(status string, queuedStatus string, startedAt, completedAt, createdAt, updatedAt pgtype.Timestamptz, now time.Time) string {
698
-		runStart := run.CreatedAt.Time
964
+	if status == queuedStatus {
699
-		if run.StartedAt.Valid {
965
+		return "—"
700
-			runStart = run.StartedAt.Time
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
701
 	}
976
 	}
702
-		runEnd := run.UpdatedAt.Time
977
+	return formatDuration(end.Sub(start))
703
-		if run.CompletedAt.Valid {
704
-			runEnd = run.CompletedAt.Time
705
 }
978
 }
706
-		if start.IsZero() || runStart.Before(start) {
979
+
707
-			start = runStart
980
+func pgTime(ts pgtype.Timestamptz, fallback time.Time) time.Time {
981
+	if ts.Valid && !ts.Time.IsZero() {
982
+		return ts.Time
708
 	}
983
 	}
709
-		if end.IsZero() || runEnd.After(end) {
984
+	return fallback
710
-			end = runEnd
711
 }
985
 }
986
+
987
+func codeTarget(ref, sha string) string {
988
+	if ref != "" {
989
+		return ref
712
 	}
990
 	}
713
-	return formatDuration(end.Sub(start))
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
+			}
1003
+		}
1004
+		return actionsJobDetailView{}, actionsStepDetailView{}, false
1005
+	}
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
1036
+		}
1037
+		remaining := actionsStepLogRenderLimit - buf.Len()
1038
+		if len(chunk.Chunk) > remaining {
1039
+			_, _ = buf.Write(chunk.Chunk[:remaining])
1040
+			truncated = true
1041
+			break
1042
+		}
1043
+		_, _ = buf.Write(chunk.Chunk)
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
1066
+		}
1067
+		return actionsStepLogContent{}, err
1068
+	}
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
@@ -65,6 +65,10 @@ type Deps struct {
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
+	// ObjectStore serves archived Actions logs and other repo-scoped blobs.
69
+	// nil keeps pages renderable in dev/test but archived logs show an
70
+	// unavailable message instead of exposing storage details.
71
+	ObjectStore storage.ObjectStore
68
 	Audit       *audit.Recorder
72
 	Audit       *audit.Recorder
69
 	Limiter     *throttle.Limiter
73
 	Limiter     *throttle.Limiter
70
 	CloneURLs   CloneURLs
74
 	CloneURLs   CloneURLs
@@ -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 }}
72
+          <div class="shithub-actions-workflow-graph" aria-label="Workflow job graph">
73
+            <div class="shithub-actions-workflow-stages">
74
+              {{ range .Run.Stages }}
75
                 <div class="shithub-actions-workflow-stage">
75
                 <div class="shithub-actions-workflow-stage">
76
-            {{ range .Run.Runs }}
76
+                  {{ range .Jobs }}
77
-              <a id="job-{{ .ID }}" href="{{ if .DetailsURL }}{{ .DetailsURL }}{{ else }}#job-{{ .ID }}{{ end }}" class="shithub-actions-job-card">
77
+                    <a href="#{{ .Anchor }}" class="shithub-actions-job-card">
78
                       <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
78
                       <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
79
                       <strong>{{ .Name }}</strong>
79
                       <strong>{{ .Name }}</strong>
80
                       <span>{{ .Duration }}</span>
80
                       <span>{{ .Duration }}</span>
81
+                      {{ if .NeedsText }}<small>needs {{ .NeedsText }}</small>{{ end }}
81
                     </a>
82
                     </a>
82
                   {{ end }}
83
                   {{ end }}
83
                 </div>
84
                 </div>
84
-          <div class="shithub-actions-graph-controls" aria-hidden="true">
85
+              {{ end }}
85
-            <button type="button" class="shithub-icon-button">{{ octicon "screen-full" }}</button>
86
+            </div>
86
-            <button type="button" class="shithub-icon-button">{{ octicon "dash" }}</button>
87
-            <button type="button" class="shithub-icon-button">{{ octicon "plus" }}</button>
88
           </div>
87
           </div>
88
+        {{ else }}
89
+          <div class="shithub-actions-empty shithub-actions-empty-compact">
90
+            <h2>No jobs</h2>
91
+            <p>This workflow run has not materialized any jobs yet.</p>
89
           </div>
92
           </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">
99
-            {{ range .Run.Runs }}
100
-              {{ if .SummaryHTML }}
101
-                <article class="shithub-actions-annotation">
102
-                  {{ octicon "alert" }}
103
-                  <div>
104
                 <strong>{{ .Name }}</strong>
102
                 <strong>{{ .Name }}</strong>
105
-                    <div class="markdown-body">{{ .SummaryHTML }}</div>
103
+                <small>{{ .StateText }} · {{ .Duration }}{{ if .RunsOn }} · runs-on {{ .RunsOn }}{{ end }}{{ if .NeedsText }} · needs {{ .NeedsText }}{{ end }}</small>
106
-                  </div>
104
+              </span>
107
-                </article>
105
+            </summary>
108
-              {{ end }}
106
+            <ol class="shithub-actions-step-list">
109
-            {{ end }}
107
+              {{ range .Steps }}
110
-          </div>
108
+                <li>
109
+                  <a href="{{ .LogHref }}" class="shithub-actions-step-row">
110
+                    <span class="shithub-actions-state shithub-actions-state-{{ .StateClass }}">{{ octicon .StateIcon }}</span>
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>
111
               {{ else }}
118
               {{ else }}
112
-          <p class="shithub-muted">No annotations were reported for this run.</p>
119
+                <li class="shithub-actions-step-empty">No steps were created for this job.</li>
120
+              {{ end }}
121
+            </ol>
122
+          </details>
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 }}