good_pipeline 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +25 -0
  3. data/CHANGELOG.md +58 -0
  4. data/README.md +67 -11
  5. data/app/controllers/good_pipeline/frontends_controller.rb +18 -1
  6. data/app/controllers/good_pipeline/pipelines_controller.rb +254 -36
  7. data/app/controllers/good_pipeline/themes_controller.rb +15 -0
  8. data/app/frontend/good_pipeline/dashboard.css +407 -0
  9. data/app/frontend/good_pipeline/dashboard.js +542 -0
  10. data/app/helpers/good_pipeline/mermaid_diagram_builder.rb +225 -66
  11. data/app/helpers/good_pipeline/pipelines_helper.rb +89 -5
  12. data/app/jobs/good_pipeline/pipeline_callback_job.rb +2 -2
  13. data/app/jobs/good_pipeline/pipeline_reconciliation_job.rb +1 -2
  14. data/app/jobs/good_pipeline/step_finished_job.rb +6 -2
  15. data/app/models/good_pipeline/pipeline_record.rb +7 -4
  16. data/app/models/good_pipeline/step_record.rb +6 -3
  17. data/app/views/good_pipeline/pipelines/_actions.html.erb +28 -0
  18. data/app/views/good_pipeline/pipelines/_chain_links.html.erb +17 -29
  19. data/app/views/good_pipeline/pipelines/_diagram.html.erb +101 -0
  20. data/app/views/good_pipeline/pipelines/_filter_bar.html.erb +51 -0
  21. data/app/views/good_pipeline/pipelines/_kpi_strip.html.erb +74 -0
  22. data/app/views/good_pipeline/pipelines/_pager.html.erb +31 -0
  23. data/app/views/good_pipeline/pipelines/_row.html.erb +103 -0
  24. data/app/views/good_pipeline/pipelines/_row_detail.html.erb +93 -0
  25. data/app/views/good_pipeline/pipelines/_sidebar.html.erb +115 -0
  26. data/app/views/good_pipeline/pipelines/_steps_table.html.erb +46 -33
  27. data/app/views/good_pipeline/pipelines/_table.html.erb +33 -0
  28. data/app/views/good_pipeline/pipelines/_timeline.html.erb +55 -0
  29. data/app/views/good_pipeline/pipelines/_topbar.html.erb +54 -0
  30. data/app/views/good_pipeline/pipelines/definitions.html.erb +88 -55
  31. data/app/views/good_pipeline/pipelines/index.html.erb +31 -42
  32. data/app/views/good_pipeline/pipelines/show.html.erb +54 -47
  33. data/app/views/layouts/good_pipeline/application.html.erb +24 -153
  34. data/config/routes.rb +4 -0
  35. data/demo/config/environments/development.rb +3 -0
  36. data/demo/config/environments/test.rb +2 -1
  37. data/demo/db/migrate/20260319205325_create_good_jobs.rb +1 -1
  38. data/demo/db/migrate/20260319205326_create_good_pipeline_tables.rb +1 -1
  39. data/demo/db/migrate/20260810000000_add_good_pipeline_dashboard_indexes.rb +20 -0
  40. data/demo/db/seeds.rb +325 -11
  41. data/demo/docs/screenshots/definitions.png +0 -0
  42. data/demo/docs/screenshots/show.png +0 -0
  43. data/demo/test/good_pipeline/test_bulk_enqueue.rb +54 -1
  44. data/demo/test/good_pipeline/test_cancellation.rb +312 -0
  45. data/demo/test/good_pipeline/test_cleanup.rb +31 -1
  46. data/demo/test/good_pipeline/test_coordinator.rb +39 -0
  47. data/demo/test/good_pipeline/test_dashboard.rb +667 -0
  48. data/demo/test/good_pipeline/test_dashboard_rerun.rb +244 -0
  49. data/demo/test/good_pipeline/test_pipeline_callback_job.rb +11 -0
  50. data/demo/test/good_pipeline/test_pipeline_record.rb +30 -0
  51. data/demo/test/good_pipeline/test_runner.rb +40 -1
  52. data/demo/test/good_pipeline/test_step_record.rb +37 -0
  53. data/demo/test/integration/test_barrier_execution.rb +502 -0
  54. data/demo/test/integration/test_chain_locking.rb +54 -0
  55. data/demo/test/integration/test_pipeline_chaining.rb +96 -0
  56. data/demo/test/test_helper.rb +20 -0
  57. data/docs/architecture.md +45 -29
  58. data/docs/branching.md +4 -0
  59. data/docs/callbacks.md +9 -7
  60. data/docs/cleanup.md +2 -2
  61. data/docs/dag-validation.md +16 -0
  62. data/docs/dashboard.md +84 -22
  63. data/docs/defining-pipelines.md +34 -1
  64. data/docs/failure-strategies.md +24 -1
  65. data/docs/getting-started.md +9 -1
  66. data/docs/index.md +1 -1
  67. data/docs/introduction.md +4 -4
  68. data/docs/monitoring.md +8 -3
  69. data/docs/pipeline-chaining.md +17 -9
  70. data/docs/public/screenshots/definitions.png +0 -0
  71. data/docs/public/screenshots/index.png +0 -0
  72. data/docs/public/screenshots/show.png +0 -0
  73. data/docs/screenshots/definitions.png +0 -0
  74. data/docs/screenshots/index.png +0 -0
  75. data/docs/screenshots/show.png +0 -0
  76. data/gemfiles/rails_7_2.gemfile +24 -0
  77. data/gemfiles/rails_7_2.gemfile.lock +412 -0
  78. data/gemfiles/rails_8_0.gemfile +24 -0
  79. data/gemfiles/rails_8_0.gemfile.lock +411 -0
  80. data/gemfiles/rails_8_1.gemfile +24 -0
  81. data/gemfiles/rails_8_1.gemfile.lock +415 -0
  82. data/lib/generators/good_pipeline/install/templates/create_good_pipeline_tables.rb.erb +9 -0
  83. data/lib/generators/good_pipeline/upgrade/templates/add_good_pipeline_dashboard_indexes.rb.erb +27 -0
  84. data/lib/generators/good_pipeline/upgrade/upgrade_generator.rb +32 -0
  85. data/lib/good_pipeline/barrier_compiler.rb +106 -0
  86. data/lib/good_pipeline/chain_coordinator.rb +9 -5
  87. data/lib/good_pipeline/constants.rb +2 -0
  88. data/lib/good_pipeline/coordinator.rb +269 -140
  89. data/lib/good_pipeline/dashboard/connection_info.rb +67 -0
  90. data/lib/good_pipeline/dashboard/definition_stages.rb +59 -0
  91. data/lib/good_pipeline/dashboard/filter_set.rb +77 -0
  92. data/lib/good_pipeline/dashboard/kpi_calculator.rb +143 -0
  93. data/lib/good_pipeline/dashboard/sparkline.rb +73 -0
  94. data/lib/good_pipeline/dashboard/stage_lanes.rb +307 -0
  95. data/lib/good_pipeline/dashboard/step_timings.rb +50 -0
  96. data/lib/good_pipeline/dashboard.rb +21 -0
  97. data/lib/good_pipeline/errors.rb +11 -0
  98. data/lib/good_pipeline/pipeline.rb +14 -2
  99. data/lib/good_pipeline/step_definition.rb +16 -1
  100. data/lib/good_pipeline/version.rb +1 -1
  101. data/lib/good_pipeline.rb +7 -1
  102. data/mise.toml +1 -1
  103. metadata +48 -13
  104. data/app/frontend/good_pipeline/style.css +0 -597
  105. data/app/views/good_pipeline/pipelines/_pagination.html.erb +0 -24
  106. data/app/views/good_pipeline/pipelines/_pipeline_row.html.erb +0 -7
@@ -0,0 +1,407 @@
1
+ :root[data-gp-theme="dark"] {
2
+ --bg:#0c0d10;--bg-1:#111317;--bg-2:#15181d;--bg-3:#1a1e24;
3
+ --bg-row-hover:#16191f;--bg-row-open:#14171c;--bg-detail:#0e1014;
4
+ --border:#20242c;--border-strong:#2a2f38;
5
+ --fg:#e6e9ef;--fg-1:#c2c8d2;--fg-2:#8a92a0;--fg-3:#5d6573;--fg-4:#3f4651;
6
+ --accent:#d6deea;--selected:#1d2530;--selected-bar:#2c3a52;--conn:#4ea36b;
7
+ --st-succeeded:#4ea36b;--st-running:#6694e8;--st-failed:#d96565;
8
+ --st-halted:#c98a3f;--st-canceling:#d79a4a;--st-canceled:#929baa;--st-skipped:#6b7384;--st-pending:#6b7384;
9
+ --strat-halt:#d96565;--strat-continue:#6694e8;--strat-ignore:#6b7384;
10
+ }
11
+
12
+ :root[data-gp-theme="light"] {
13
+ --bg:#f5f6f8;--bg-1:#ffffff;--bg-2:#fafbfc;--bg-3:#eef0f4;
14
+ --bg-row-hover:#f3f5f8;--bg-row-open:#f3f5f8;--bg-detail:#f9fafc;
15
+ --border:#e3e6eb;--border-strong:#cdd2da;
16
+ --fg:#0f1216;--fg-1:#2a2f38;--fg-2:#5b6471;--fg-3:#8a92a0;--fg-4:#b6bcc7;
17
+ --accent:#1a1d22;--selected:#e7eef9;--selected-bar:#c5d4ee;--conn:#2f8a52;
18
+ --st-succeeded:#2f8a52;--st-running:#2563d4;--st-failed:#c43a3a;
19
+ --st-halted:#b16a1f;--st-canceling:#874b0f;--st-canceled:#4f5867;--st-skipped:#6b7384;--st-pending:#6b7384;
20
+ --strat-halt:#c43a3a;--strat-continue:#2563d4;--strat-ignore:#6b7384;
21
+ }
22
+
23
+ html[data-gp-theme], html[data-gp-theme] body { height:100%; margin:0; }
24
+ html[data-gp-theme] body {
25
+ color:var(--fg); background:var(--bg);
26
+ font:12.5px/1.4 "Inter",system-ui,-apple-system,"Segoe UI",sans-serif;
27
+ -webkit-font-smoothing:antialiased; font-feature-settings:"cv11","ss01";
28
+ }
29
+
30
+ .gp-app, .gp-app * { box-sizing:border-box; }
31
+ .gp-app { width:100%; height:100vh; color:var(--fg); background:var(--bg); }
32
+ /* Element resets are wrapped in :where() so they carry zero specificity and can
33
+ never outrank a single-class component rule. Without this, `.gp-app button`
34
+ (0,1,1) silently beats every component font-size such as `.gp-action` (0,1,0). */
35
+ :where(.gp-app) a { color:var(--fg-1); text-decoration:none; }
36
+ :where(.gp-app) a:hover { color:var(--fg); }
37
+ :where(.gp-app) :where(button, input) { font:inherit; }
38
+ .gp-app .mono, .gp-app code, .gp-app pre, .gp-app input, .gp-app .data {
39
+ font-family:"JetBrains Mono",ui-monospace,"SFMono-Regular",Menlo,monospace;
40
+ }
41
+ .gp-app button { cursor:pointer; }
42
+ .gp-app button:disabled { cursor:not-allowed; opacity:.52; }
43
+ .gp-app table { border-spacing:0; }
44
+ .gp-app ::-webkit-scrollbar { width:8px; height:8px; }
45
+ .gp-app ::-webkit-scrollbar-thumb { background:var(--border-strong); border-radius:4px; }
46
+ .gp-app ::-webkit-scrollbar-track { background:transparent; }
47
+ .gp-sr-only {
48
+ position:absolute!important; width:1px!important; height:1px!important; padding:0!important;
49
+ margin:-1px!important; overflow:hidden!important; clip:rect(0,0,0,0)!important;
50
+ white-space:nowrap!important; border:0!important;
51
+ }
52
+ .gp-flash {
53
+ position:fixed; z-index:1000; top:10px; right:12px; max-width:min(420px,calc(100vw - 24px));
54
+ box-sizing:border-box;
55
+ padding:8px 12px; border:1px solid var(--border-strong); border-radius:4px;
56
+ color:var(--fg); background:var(--bg-2); box-shadow:0 6px 24px color-mix(in oklab,var(--bg) 75%,transparent);
57
+ font:11px/1.4 "JetBrains Mono",monospace;
58
+ }
59
+ .gp-flash--notice { border-color:color-mix(in oklab,var(--st-succeeded) 45%,var(--border)); }
60
+ .gp-flash--alert { border-color:color-mix(in oklab,var(--st-failed) 50%,var(--border)); color:var(--st-failed); }
61
+
62
+ /* Shell */
63
+ .gp-app--dashboard, .gp-app--definitions { display:grid; grid-template-columns:264px minmax(0,1fr); overflow:hidden; }
64
+ .gp-app--detail { display:flex; flex-direction:column; overflow:hidden; }
65
+ .gp-main { display:flex; flex-direction:column; min-width:0; min-height:0; overflow:hidden; }
66
+ #gp-dashboard, #gp-kpi, #gp-table { display:contents; }
67
+ #gp-filters { display:flex; align-items:center; gap:10px; min-width:0; flex-wrap:wrap; }
68
+ #gp-kpi.is-loading .gp-kpi-strip, #gp-filters.is-loading, #gp-table.is-loading .gp-table-wrap,
69
+ #gp-table.is-loading .gp-pager { opacity:.6; pointer-events:none; }
70
+ .gp-kpi-strip, #gp-filters, .gp-table-wrap, .gp-pager { transition:opacity 120ms ease-in; }
71
+
72
+ /* Sidebar */
73
+ .gp-sidebar {
74
+ display:flex; flex-direction:column; min-width:0; overflow:hidden;
75
+ background:var(--bg-1); border-right:1px solid var(--border);
76
+ }
77
+ .gp-sidebar-head { padding:12px 12px 10px; border-bottom:1px solid var(--border); }
78
+ .gp-brand { display:flex; align-items:center; gap:6px; min-width:0; }
79
+ .gp-brand-mark {
80
+ width:18px; height:18px; display:inline-flex; flex:none; align-items:center; justify-content:center;
81
+ border-radius:4px; color:var(--bg); background:var(--accent); font:500 9px/1 "JetBrains Mono",monospace;
82
+ }
83
+ .gp-brand-name { color:var(--fg); font-weight:600; letter-spacing:-.01em; line-height:1; }
84
+ .gp-brand-version { color:var(--fg-3); font:10px/1 "JetBrains Mono",monospace; }
85
+ .gp-sidebar-scroll { flex:1; overflow-y:auto; padding:4px 0 8px; }
86
+ .gp-sidebar-group { margin-top:10px; }
87
+ .gp-sidebar-group-head {
88
+ display:flex; align-items:center; justify-content:space-between; padding:4px 14px;
89
+ color:var(--fg-3); font-size:10px; letter-spacing:.06em; text-transform:uppercase;
90
+ }
91
+ .gp-sidebar-group-count { color:var(--fg-4); font-family:"JetBrains Mono",monospace; font-variant-numeric:tabular-nums; }
92
+ .gp-sidebar-row {
93
+ position:relative; display:flex; align-items:center; justify-content:space-between;
94
+ width:calc(100% - 12px); min-height:27px; margin:0 6px; padding:5px 8px;
95
+ overflow:hidden; border:0; border-radius:3px; color:var(--fg-1); background:transparent;
96
+ font-family:inherit; font-size:12px; line-height:1.4; text-align:left;
97
+ }
98
+ .gp-sidebar-all { margin-top:14px; }
99
+ .gp-sidebar-row:hover { color:var(--fg); background:var(--bg-row-hover); }
100
+ .gp-sidebar-row.is-active { color:var(--fg); background:var(--selected); }
101
+ .gp-sidebar-row.is-active::before {
102
+ content:""; position:absolute; z-index:2; left:0; top:4px; bottom:4px; width:2px;
103
+ border-radius:1px; background:var(--accent);
104
+ }
105
+ .gp-sidebar-bar {
106
+ position:absolute; inset:0 auto 0 0; z-index:0; width:var(--gp-width,0%);
107
+ pointer-events:none; opacity:.6; background:var(--bg-2);
108
+ }
109
+ .gp-sidebar-row.is-active .gp-sidebar-bar { opacity:.7; background:var(--selected-bar); }
110
+ .gp-sidebar-label, .gp-sidebar-count { position:relative; z-index:1; }
111
+ .gp-sidebar-label { display:flex; align-items:center; gap:6px; min-width:0; overflow:hidden; white-space:nowrap; }
112
+ .gp-sidebar-name { overflow:hidden; text-overflow:ellipsis; }
113
+ .gp-sidebar-count { color:var(--fg-3); font:11px "JetBrains Mono",monospace; font-variant-numeric:tabular-nums; }
114
+ .gp-sidebar-dot { width:6px; height:6px; flex:none; border-radius:50%; background:var(--accent); }
115
+ .gp-sidebar-dot.strategy-halt { background:var(--strat-halt); }
116
+ .gp-sidebar-dot.strategy-continue { background:var(--strat-continue); }
117
+ .gp-sidebar-dot.strategy-ignore { background:var(--strat-ignore); }
118
+ .gp-sidebar-tag {
119
+ display:inline-flex; align-items:center; height:14px; padding:0 4px; border:1px solid var(--border-strong);
120
+ border-radius:2px; color:var(--fg-3); background:var(--bg-2); font:9px "JetBrains Mono",monospace;
121
+ }
122
+ .gp-sidebar-tag--xl { color:var(--st-halted); border-color:color-mix(in oklab,var(--st-halted) 30%,var(--border)); }
123
+ .gp-sidebar-foot {
124
+ display:flex; flex-direction:column; gap:6px; padding:8px 12px;
125
+ border-top:1px solid var(--border); background:var(--bg-1);
126
+ }
127
+ .gp-legend { display:flex; gap:10px; color:var(--fg-3); font:10px "JetBrains Mono",monospace; }
128
+ .gp-legend span { display:inline-flex; align-items:center; gap:4px; }
129
+ .gp-legend i { width:6px; height:6px; border-radius:50%; }
130
+ .gp-retention { display:flex; align-items:baseline; justify-content:space-between; font-size:10.5px; }
131
+ .gp-retention-label { color:var(--fg-3); font-size:9.5px; letter-spacing:.06em; text-transform:uppercase; }
132
+ .gp-retention-value { color:var(--fg-2); font-family:"JetBrains Mono",monospace; }
133
+
134
+ /* Topbar */
135
+ .gp-topbar {
136
+ height:38px; flex:none; display:flex; align-items:center; justify-content:space-between;
137
+ gap:12px; padding:0 14px; border-bottom:1px solid var(--border); background:var(--bg-1);
138
+ }
139
+ .gp-crumbs { display:flex; align-items:center; gap:8px; min-width:0; overflow:hidden; white-space:nowrap; }
140
+ .gp-crumb { color:var(--fg-2); font-size:12px; }
141
+ .gp-crumb--active { color:var(--fg); font-weight:500; overflow:hidden; text-overflow:ellipsis; }
142
+ .gp-crumb-sep { color:var(--fg-4); font-size:12px; }
143
+ .gp-crumb-meta { color:var(--fg-3); font-size:11px; overflow:hidden; text-overflow:ellipsis; }
144
+ .gp-back {
145
+ display:inline-flex; align-items:center; height:22px; padding:0 8px; border:1px solid var(--border);
146
+ border-radius:3px; color:var(--fg-2); background:var(--bg-2); font-size:12px;
147
+ }
148
+ .gp-topbar-right { display:flex; align-items:center; gap:8px; flex:none; }
149
+ .gp-theme-btn, .gp-diagram-btn {
150
+ display:inline-flex; align-items:center; justify-content:center; height:22px;
151
+ border:1px solid var(--border); border-radius:3px; color:var(--fg-1); background:var(--bg-2);
152
+ }
153
+ .gp-theme-btn { width:22px; font-size:13px; line-height:1; }
154
+ .gp-theme-btn:hover, .gp-diagram-btn:hover { background:var(--bg-3); color:var(--fg); }
155
+ .gp-connection, .gp-clock {
156
+ display:inline-flex; align-items:center; gap:6px; height:22px; padding:0 8px;
157
+ border:1px solid var(--border); border-radius:3px; background:var(--bg-2); font-size:11px;
158
+ }
159
+ .gp-connection-dot { width:6px; height:6px; border-radius:50%; background:var(--conn); box-shadow:0 0 0 2px color-mix(in oklab,var(--conn) 22%,transparent); }
160
+ .gp-clock, .gp-connection-version { color:var(--fg-3); font-family:"JetBrains Mono",monospace; }
161
+
162
+ /* Pills */
163
+ /* text-transform:none keeps a pill identical wherever it is nested. Without it a
164
+ pill placed inside an uppercased section heading renders "ON_FAILURE: CONTINUE"
165
+ while the same component renders lowercase elsewhere. */
166
+ .gp-status, .gp-status-pill, .badge, .gp-strategy {
167
+ display:inline-flex; align-items:center; height:16px; padding:0 5px;
168
+ border:1px solid var(--border-strong); border-radius:2px; background:var(--bg-2);
169
+ font:10px "JetBrains Mono",monospace; letter-spacing:.02em; text-transform:none; white-space:nowrap;
170
+ }
171
+ .gp-status--pending,.gp-status-pending,.badge-pending { --gp-status:var(--st-pending); }
172
+ .gp-status--running,.gp-status--enqueued,.gp-status-running,.gp-status-enqueued,.badge-running,.badge-enqueued { --gp-status:var(--st-running); }
173
+ .gp-status--canceling,.gp-status-canceling,.badge-canceling { --gp-status:var(--st-canceling); }
174
+ .gp-status--canceled,.gp-status-canceled,.badge-canceled { --gp-status:var(--st-canceled); }
175
+ .gp-status--succeeded,.gp-status-succeeded,.badge-succeeded { --gp-status:var(--st-succeeded); }
176
+ .gp-status--failed,.gp-status-failed,.badge-failed { --gp-status:var(--st-failed); }
177
+ .gp-status--halted,.gp-status-halted,.badge-halted { --gp-status:var(--st-halted); }
178
+ .gp-status--skipped,.gp-status--skipped_by_branch,.gp-status-skipped,.gp-status-skipped_by_branch,.badge-skipped,.badge-skipped_by_branch { --gp-status:var(--st-skipped); }
179
+ .gp-status, .gp-status-pill, .badge { color:var(--gp-status,var(--fg-2)); border-color:color-mix(in oklab,var(--gp-status,var(--fg-2)) 35%,var(--border)); background:color-mix(in oklab,var(--gp-status,var(--fg-2)) 8%,var(--bg-2)); }
180
+ .gp-strategy--halt { --gp-strategy:var(--strat-halt); }
181
+ .gp-strategy--continue { --gp-strategy:var(--strat-continue); }
182
+ .gp-strategy--ignore { --gp-strategy:var(--strat-ignore); }
183
+ .gp-strategy { color:var(--gp-strategy,var(--fg-2)); border-color:color-mix(in oklab,var(--gp-strategy,var(--fg-2)) 35%,var(--border)); background:color-mix(in oklab,var(--gp-strategy,var(--fg-2)) 8%,var(--bg-2)); }
184
+
185
+ /* KPI strip */
186
+ .gp-kpi-strip { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)) minmax(120px,1.4fr); border-bottom:1px solid var(--border); background:var(--bg-1); }
187
+ .gp-kpi { display:flex; flex-direction:column; gap:2px; min-width:0; padding:10px 14px; border-right:1px solid var(--border); }
188
+ .gp-kpi:last-child { border-right:0; }
189
+ .gp-kpi-label { color:var(--fg-3); font-size:9.5px; letter-spacing:.07em; text-transform:uppercase; white-space:nowrap; }
190
+ .gp-kpi-value { color:var(--fg); font:500 18px "JetBrains Mono",monospace; font-variant-numeric:tabular-nums; white-space:nowrap; }
191
+ .gp-kpi-value--running { color:var(--st-running); }
192
+ .gp-kpi-value--failed { color:var(--st-failed); }
193
+ .gp-kpi-sub { color:var(--fg-3); font:11px "JetBrains Mono",monospace; white-space:nowrap; }
194
+ .gp-spark { display:flex; align-items:flex-end; gap:2px; height:28px; margin-top:4px; }
195
+ .gp-spark-bar { flex:1; min-height:1px; height:var(--gp-height,1%); border-radius:1px; background:var(--fg-3); opacity:.7; }
196
+ .gp-spark-bar:nth-last-child(-n+3) { background:var(--accent); opacity:1; }
197
+
198
+ /* Filters */
199
+ .gp-filter-bar { display:flex; align-items:center; gap:10px; padding:8px 14px; border-bottom:1px solid var(--border); background:var(--bg-1); flex-wrap:wrap; }
200
+ .gp-search-form { flex:1 1 260px; min-width:220px; }
201
+ .gp-search {
202
+ display:flex; align-items:center; gap:6px; width:100%; height:26px; padding:4px 8px;
203
+ border:1px solid var(--border); border-radius:4px; color:var(--fg-3); background:var(--bg-2);
204
+ }
205
+ .gp-search svg { width:12px; height:12px; flex:none; }
206
+ .gp-search input { flex:1; min-width:0; padding:0; border:0; outline:0; color:var(--fg); background:transparent; font-size:11.5px; }
207
+ .gp-status-filter-scroll { flex:0 1 auto; min-width:0; max-width:100%; overflow-x:auto; overflow-y:hidden; }
208
+ /* Segment children use inline layout, not flex centring: a flex row would centre
209
+ the 11.5px label box against the 9.5px count box and their baselines would
210
+ diverge. `--gp-segment-line` is the segment's inner height (26px - 2px border),
211
+ so line-height alone centres a single shared baseline. */
212
+ .gp-segment { --gp-segment-line:24px; display:inline-flex; align-items:stretch; height:26px; border:1px solid var(--border); border-radius:4px; overflow:hidden; background:var(--bg-2); }
213
+ .gp-segment-label { display:block; padding:0 8px; border-right:1px solid var(--border); color:var(--fg-3); background:var(--bg-3); font-size:10px; line-height:var(--gp-segment-line); letter-spacing:.06em; text-transform:uppercase; }
214
+ .gp-segment-link { display:block; padding:0 9px; border-right:1px solid var(--border); color:var(--fg-2); font-size:11.5px; line-height:var(--gp-segment-line); white-space:nowrap; }
215
+ .gp-segment-count { margin-left:5px; color:var(--fg-3); font-family:"JetBrains Mono",monospace; font-size:9.5px; font-variant-numeric:tabular-nums; }
216
+ .gp-segment-link.is-active .gp-segment-count { color:var(--fg-1); }
217
+ .gp-segment-link:last-child { border-right:0; }
218
+ .gp-segment-link:hover { color:var(--fg); background:var(--bg-row-hover); }
219
+ .gp-segment-link.is-active { color:var(--fg); background:var(--selected); }
220
+ .gp-filter-clear { display:inline-flex; align-items:center; height:26px; padding:0 10px; border:1px solid var(--border); border-radius:4px; color:var(--fg-2); font-size:11.5px; }
221
+ .gp-filter-clear:hover { color:var(--fg); border-color:var(--border-strong); }
222
+
223
+ /* Execution table */
224
+ .gp-table-wrap { flex:1; min-height:0; overflow:auto; background:var(--bg); }
225
+ .gp-table { width:100%; border-collapse:collapse; table-layout:fixed; font-size:12px; }
226
+ .gp-table thead th {
227
+ position:sticky; top:0; z-index:3; padding:7px 10px; border-bottom:1px solid var(--border);
228
+ color:var(--fg-3); background:var(--bg-1); text-align:left; font-size:10px; font-weight:500;
229
+ letter-spacing:.07em; text-transform:uppercase;
230
+ }
231
+ .gp-table .gp-th-expand { width:24px; padding-right:0; }
232
+ /* Fits "YYYY-MM-DD HH:MM:SSZ" (135px at 10.5px JetBrains Mono) plus the cell's
233
+ 20px horizontal padding, so the absolute timestamp is never ellipsised. */
234
+ .gp-table .gp-th-started { width:158px; }
235
+ .gp-table .gp-th-steps { width:160px; }
236
+ .gp-table .gp-th-status { width:110px; }
237
+ .gp-table .gp-th-duration { width:90px; }
238
+ .gp-row { cursor:pointer; }
239
+ .gp-row > td { padding:6px 10px; border-bottom:1px solid var(--border); vertical-align:middle; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
240
+ .gp-row > .gp-td-expand { padding-right:0; color:var(--fg-3); }
241
+ .gp-row:hover > td { background:var(--bg-row-hover); }
242
+ .gp-row.is-open > td { border-bottom-color:transparent; background:var(--bg-row-open); }
243
+ .gp-when-rel { color:var(--fg); font:11.5px "JetBrains Mono",monospace; font-variant-numeric:tabular-nums; }
244
+ .gp-when-abs { color:var(--fg-3); font:10.5px "JetBrains Mono",monospace; font-variant-numeric:tabular-nums; }
245
+ /* Only the pipeline name may truncate. The id and the two icon affordances are
246
+ flex:none so a long name can never clip the copy/open controls out of view. */
247
+ .gp-execution-cell { display:flex; align-items:center; min-width:0; }
248
+ .gp-execution-name { flex:0 1 auto; min-width:0; overflow:hidden; color:var(--fg); font-weight:500; text-overflow:ellipsis; white-space:nowrap; }
249
+ .gp-execution-id { flex:none; margin-left:6px; color:var(--fg-2); font:11.5px "JetBrains Mono",monospace; }
250
+ /* Both icon affordances share one fixed 14px box. Sizing them by their contents
251
+ gave the SVG an 11px box and the text glyph a 16.8px line box, so they sat on
252
+ visibly different centres. */
253
+ .gp-copy, .gp-open-detail {
254
+ display:inline-flex; align-items:center; justify-content:center; flex:none;
255
+ width:14px; height:14px; padding:0; margin-left:4px; line-height:1;
256
+ border:0; color:var(--fg-3); background:transparent; vertical-align:middle;
257
+ }
258
+ .gp-copy:hover, .gp-open-detail:hover { color:var(--fg); }
259
+ .gp-copy svg, .gp-open-detail svg { display:block; width:11px; height:11px; }
260
+ .gp-step-dots { display:inline-flex; align-items:center; gap:3px; margin-right:6px; vertical-align:middle; }
261
+ .gp-step-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--gp-status); opacity:var(--gp-opacity,1); }
262
+ .gp-step-dot.is-branch { border-radius:1px; transform:rotate(45deg) scale(.9); }
263
+ .gp-step-dot.is-barrier { border-radius:1px; box-shadow:inset 1px 0 var(--bg),inset -1px 0 var(--bg); }
264
+ .gp-step-stack { display:inline-flex; align-items:center; width:84px; height:8px; margin-right:6px; overflow:hidden; border-radius:2px; vertical-align:middle; background:var(--bg-3); }
265
+ .gp-step-stack > span { display:block; height:100%; width:var(--gp-width); background:var(--gp-status); opacity:var(--gp-opacity,1); }
266
+ .gp-step-summary { color:var(--fg-3); font:10.5px "JetBrains Mono",monospace; font-variant-numeric:tabular-nums; }
267
+ .gp-params-preview { color:var(--fg-2); font:11px "JetBrains Mono",monospace; }
268
+ .gp-table-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; padding:80px 20px; color:var(--fg-3); }
269
+ .gp-empty-mark { color:var(--fg-4); font:28px "JetBrains Mono",monospace; }
270
+ .gp-empty-title { color:var(--fg-1); font-weight:500; }
271
+ .gp-empty-sub { color:var(--fg-3); font:11px "JetBrains Mono",monospace; }
272
+
273
+ /* Expandable detail */
274
+ .gp-row-detail > td { padding:0; border-bottom:1px solid var(--border); background:var(--bg-detail); }
275
+ .gp-row-detail-inner { padding:12px 18px 14px; border-top:1px dashed var(--border-strong); }
276
+ .gp-detail-grid { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:14px 24px; }
277
+ .gp-detail-section { min-width:0; }
278
+ .gp-detail-wide { grid-column:1 / -1; }
279
+ .gp-section-heading { display:flex; align-items:center; gap:6px; min-height:21px; padding-bottom:6px; margin:0 0 6px; border-bottom:1px solid var(--border); color:var(--fg-3); font-size:10px; font-weight:400; letter-spacing:.08em; text-transform:uppercase; }
280
+ .gp-section-heading .gp-heading-meta { margin-left:auto; font-family:"JetBrains Mono",monospace; font-variant-numeric:tabular-nums; text-transform:none; letter-spacing:normal; }
281
+ .gp-section-heading .gp-strategy { margin-left:auto; }
282
+ .gp-scroll-steps { max-height:240px; overflow-y:auto; }
283
+ .gp-compact-table { width:100%; border-collapse:collapse; font-size:11.5px; }
284
+ .gp-compact-table td { padding:2.5px 0; vertical-align:top; }
285
+ .gp-compact-table .gp-step-key { color:var(--fg); font-family:"JetBrains Mono",monospace; white-space:nowrap; }
286
+ .gp-compact-table .gp-job-class { max-width:180px; padding-left:8px; padding-right:8px; overflow:hidden; color:var(--fg-3); font:10.5px "JetBrains Mono",monospace; text-overflow:ellipsis; white-space:nowrap; }
287
+ .gp-compact-table .gp-step-duration { padding-left:8px; color:var(--fg-2); font:10.5px "JetBrains Mono",monospace; text-align:right; white-space:nowrap; }
288
+ .gp-error { max-width:220px; overflow:hidden; color:var(--st-failed); text-overflow:ellipsis; white-space:nowrap; }
289
+ .gp-kv { width:100%; border-collapse:collapse; font-size:11.5px; }
290
+ .gp-kv td { padding:2px 0; vertical-align:top; }
291
+ .gp-kv-key { width:140px; color:var(--fg-3); font-family:"JetBrains Mono",monospace; white-space:nowrap; }
292
+ .gp-kv-value { color:var(--fg); font-family:"JetBrains Mono",monospace; overflow-wrap:anywhere; }
293
+ .gp-actions { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
294
+ .gp-action-form { display:inline-flex; margin:0; }
295
+ .gp-action {
296
+ display:inline-flex; align-items:center; height:22px; padding:0 10px; border:1px solid var(--border-strong);
297
+ border-radius:3px; color:var(--fg-1); background:var(--bg-2); font:11px "JetBrains Mono",monospace;
298
+ }
299
+ .gp-action--danger { border-color:color-mix(in oklab,var(--st-failed) 40%,var(--border)); color:var(--st-failed); background:transparent; }
300
+ .gp-action--ghost { border-color:var(--border); color:var(--fg-2); background:transparent; }
301
+ .gp-action:disabled { color:var(--fg-4); border-color:var(--border); }
302
+
303
+ /* Timeline */
304
+ .gp-timeline { display:flex; flex-direction:column; gap:3px; max-height:260px; overflow-y:auto; }
305
+ .gp-lane { display:flex; align-items:center; gap:8px; min-height:12px; }
306
+ .gp-lane-label { width:130px; flex:none; overflow:hidden; color:var(--fg-2); font:10.5px "JetBrains Mono",monospace; text-align:right; text-overflow:ellipsis; white-space:nowrap; }
307
+ .gp-lane-track { position:relative; flex:1; height:10px; overflow:hidden; border-radius:2px; background:var(--bg-2); }
308
+ .gp-lane-bar { position:absolute; inset:0 auto 0 var(--gp-left,0%); width:max(1.5%,var(--gp-width,1.5%)); border-radius:2px; background:var(--gp-status,var(--fg-3)); opacity:.85; }
309
+ .gp-lane-bar.is-running { background:repeating-linear-gradient(135deg,var(--gp-status,var(--st-running)) 0,var(--gp-status,var(--st-running)) 5px,color-mix(in oklab,var(--gp-status,var(--st-running)) 55%,var(--bg-2)) 5px,color-mix(in oklab,var(--gp-status,var(--st-running)) 55%,var(--bg-2)) 9px); }
310
+ .gp-lane-note { width:110px; flex:none; overflow:hidden; color:var(--fg-3); font:10.5px "JetBrains Mono",monospace; font-variant-numeric:tabular-nums; text-overflow:ellipsis; white-space:nowrap; }
311
+ .gp-stage-level { width:28px; flex:none; color:var(--fg-4); font:9.5px "JetBrains Mono",monospace; text-align:right; }
312
+ .gp-stage-role { width:var(--gp-width,12%); min-width:8px; height:10px; flex:none; border:1px solid var(--border-strong); border-radius:2px; background:var(--bg-3); }
313
+ .gp-stage-role--branch { background:color-mix(in oklab,var(--st-halted) 42%,var(--bg-3)); border-color:var(--st-halted); }
314
+ .gp-stage-role--barrier { background:var(--bg-2); border-width:2px; border-color:var(--fg-3); }
315
+ .gp-stage-role--terminal { background:var(--accent); border-color:var(--accent); }
316
+ .gp-stage-stack { display:inline-flex; flex:1; height:10px; overflow:hidden; border-radius:2px; background:var(--bg-2); }
317
+ .gp-stage-stack > span { display:block; width:var(--gp-width); height:100%; background:var(--gp-status); opacity:var(--gp-opacity,1); }
318
+
319
+ /* Code/requery */
320
+ .gp-code-wrap { position:relative; }
321
+ .gp-code {
322
+ margin:4px 0 0; padding:8px 36px 8px 10px; overflow-x:auto; border:1px solid var(--border);
323
+ border-radius:3px; color:var(--fg-1); background:var(--bg-2); font-size:11.5px; white-space:pre;
324
+ }
325
+ .gp-code-wrap > .gp-copy { position:absolute; z-index:1; top:6px; right:6px; width:22px; height:22px; margin:0; border:1px solid var(--border); border-radius:3px; background:var(--bg-3); }
326
+
327
+ /* Pager */
328
+ .gp-pager { height:36px; flex:none; display:flex; align-items:center; justify-content:space-between; padding:8px 14px; border-top:1px solid var(--border); background:var(--bg-1); font-size:11.5px; }
329
+ .gp-pager-info { color:var(--fg-2); }
330
+ .gp-pager-info .mono { color:var(--fg); font-variant-numeric:tabular-nums; }
331
+ .gp-pager-buttons { display:flex; align-items:center; gap:4px; }
332
+ .gp-page-button { width:24px; height:22px; display:inline-flex; align-items:center; justify-content:center; border:1px solid var(--border); border-radius:3px; color:var(--fg-1); background:var(--bg-2); }
333
+ .gp-page-button:hover { background:var(--bg-3); }
334
+ .gp-page-button:disabled { color:var(--fg-4); }
335
+ .gp-page-label { padding:0 8px; color:var(--fg-2); font-variant-numeric:tabular-nums; }
336
+
337
+ /* Detail screen and cards */
338
+ .gp-detail-header { flex:none; display:flex; align-items:center; gap:10px; padding:10px 16px; border-bottom:1px solid var(--border); background:var(--bg-1); flex-wrap:wrap; }
339
+ .gp-detail-title { color:var(--fg); font-size:14px; font-weight:600; }
340
+ .gp-detail-uuid { display:inline-flex; align-items:center; gap:2px; color:var(--fg-3); font:10.5px "JetBrains Mono",monospace; }
341
+ .gp-detail-header .gp-actions { margin:0 0 0 auto; }
342
+ .gp-detail-scroll { flex:1; min-height:0; overflow-y:auto; }
343
+ .gp-detail-columns { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.15fr); gap:14px; align-items:start; padding:14px 16px; }
344
+ .gp-detail-column { display:flex; flex-direction:column; gap:14px; min-width:0; }
345
+ .gp-card { min-width:0; padding:10px 12px; border:1px solid var(--border); border-radius:4px; background:var(--bg-1); }
346
+ .gp-card .gp-scroll-steps { max-height:340px; }
347
+ .gp-chain { display:flex; align-items:center; flex-wrap:wrap; gap:6px; }
348
+ .gp-chain-chip { display:inline-flex; align-items:center; gap:6px; height:22px; padding:0 7px; border:1px solid var(--border); border-radius:3px; color:var(--fg-2); background:var(--bg-2); font-size:10.5px; }
349
+ .gp-chain-arrow { color:var(--fg-4); }
350
+
351
+ /* Diagrams */
352
+ .gp-diagram-container { min-width:0; }
353
+ .gp-diagram-toolbar { margin-left:auto; display:inline-flex; align-items:center; gap:4px; }
354
+ .gp-diagram-btn { min-width:20px; height:20px; padding:0 6px; color:var(--fg-2); font-size:10px; }
355
+ .gp-diagram-btn--icon { width:20px; padding:0; font-size:11px; }
356
+ .gp-diagram-btn[aria-pressed="true"] { color:var(--fg); background:var(--selected); }
357
+ .gp-diagram-viewport { min-height:280px; max-height:560px; overflow:hidden; cursor:grab; touch-action:none; }
358
+ .gp-diagram-viewport:active { cursor:grabbing; }
359
+ .gp-diagram-canvas { display:flex; justify-content:center; min-width:max-content; padding:6px 0; transform-origin:0 0; will-change:transform; }
360
+ .gp-diagram-canvas.is-dragging { cursor:grabbing; user-select:none; }
361
+ .gp-diagram-canvas svg { max-width:100%; height:auto; }
362
+ .gp-diagram-error, .gp-diagram-fallback { display:flex; min-height:200px; align-items:center; justify-content:center; padding:20px; color:var(--st-failed); font:11px "JetBrains Mono",monospace; text-align:center; }
363
+ .gp-diagram-fallback { color:var(--fg-3); }
364
+ .gp-graph-panel[hidden], .gp-stage-panel[hidden] { display:none; }
365
+ .gp-edge-warning { margin:6px 0 0; color:var(--st-halted); font:10.5px "JetBrains Mono",monospace; text-transform:none; letter-spacing:normal; }
366
+ .gp-diagram-container:fullscreen { padding:14px; overflow:auto; background:var(--bg); }
367
+ .gp-diagram-container:fullscreen .gp-diagram-viewport { max-height:none; height:calc(100vh - 50px); }
368
+ .gp-card:fullscreen { padding:14px; overflow:auto; background:var(--bg); }
369
+ .gp-card:fullscreen .gp-diagram-viewport { max-height:none; height:calc(100vh - 58px); }
370
+
371
+ /* Definitions */
372
+ .gp-definitions-scroll { flex:1; min-height:0; overflow-y:auto; }
373
+ .gp-definitions-content { width:100%; max-width:1100px; display:flex; flex-direction:column; gap:14px; padding:14px 16px; }
374
+ .gp-definition-panel { display:none; }
375
+ .gp-definition-panel.is-active { display:contents; }
376
+ .gp-definition-summary { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
377
+ .gp-definition-summary .gp-action { margin-left:auto; }
378
+ .gp-definition-title { color:var(--fg); font-size:14px; font-weight:600; }
379
+ .gp-definition-metrics { display:flex; flex-wrap:wrap; gap:18px; margin-top:8px; color:var(--fg-2); font:11px "JetBrains Mono",monospace; }
380
+ .gp-definition-metrics b { color:var(--fg-3); font-weight:400; }
381
+ .gp-definition-list-overflow { max-height:280px; overflow:auto; }
382
+
383
+ @media (max-width:1000px) {
384
+ .gp-app--dashboard, .gp-app--definitions { grid-template-columns:220px minmax(0,1fr); }
385
+ .gp-kpi-strip { grid-template-columns:repeat(3,minmax(0,1fr)); }
386
+ .gp-kpi:nth-child(3) { border-right:0; }
387
+ .gp-detail-columns { grid-template-columns:1fr; }
388
+ .gp-clock { display:none; }
389
+ }
390
+
391
+ @media (max-width:720px) {
392
+ .gp-app--dashboard, .gp-app--definitions { grid-template-columns:1fr; overflow:auto; }
393
+ .gp-sidebar { display:none; }
394
+ .gp-main { min-height:100vh; overflow:visible; }
395
+ .gp-topbar { position:sticky; top:0; z-index:10; }
396
+ .gp-connection { display:none; }
397
+ .gp-kpi-strip { grid-template-columns:repeat(2,minmax(0,1fr)); }
398
+ .gp-kpi:nth-child(odd) { border-right:1px solid var(--border); }
399
+ .gp-kpi:nth-child(even) { border-right:0; }
400
+ .gp-filter-bar { align-items:stretch; }
401
+ #gp-filters { width:100%; flex-wrap:wrap; }
402
+ .gp-table-wrap { min-height:480px; }
403
+ .gp-detail-grid { grid-template-columns:1fr; }
404
+ .gp-detail-wide { grid-column:auto; }
405
+ .gp-detail-columns { padding:10px; }
406
+ .gp-detail-header .gp-actions { width:100%; margin-left:0; }
407
+ }