chrono_forge-dashboard 0.1.0 → 0.2.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +43 -24
  4. data/app/assets/chrono_forge/dashboard/cytoscape-dagre.js +397 -0
  5. data/app/assets/chrono_forge/dashboard/cytoscape.min.js +32 -0
  6. data/app/assets/chrono_forge/dashboard/dagre.min.js +3809 -0
  7. data/app/assets/chrono_forge/dashboard/dashboard.css +1 -1
  8. data/app/assets/chrono_forge/dashboard/dashboard.js +37 -0
  9. data/app/assets/chrono_forge/dashboard/definition_graph.js +161 -0
  10. data/app/controllers/chrono_forge/dashboard/assets_controller.rb +8 -1
  11. data/app/controllers/chrono_forge/dashboard/definitions_controller.rb +35 -0
  12. data/app/controllers/chrono_forge/dashboard/workflows_controller.rb +6 -2
  13. data/app/helpers/chrono_forge/dashboard/dashboard_helper.rb +33 -0
  14. data/app/presenters/chrono_forge/dashboard/branch_presenter.rb +38 -1
  15. data/app/presenters/chrono_forge/dashboard/branches_presenter.rb +59 -4
  16. data/app/presenters/chrono_forge/dashboard/cytoscape_graph.rb +48 -0
  17. data/app/presenters/chrono_forge/dashboard/definition_overlay.rb +128 -0
  18. data/app/queries/chrono_forge/dashboard/workflows_query.rb +10 -1
  19. data/app/views/chrono_forge/dashboard/branch_children/show.html.erb +56 -11
  20. data/app/views/chrono_forge/dashboard/definitions/show.html.erb +42 -0
  21. data/app/views/chrono_forge/dashboard/workflows/_branches.html.erb +14 -4
  22. data/app/views/chrono_forge/dashboard/workflows/_filters.html.erb +12 -1
  23. data/app/views/chrono_forge/dashboard/workflows/_stats.html.erb +5 -9
  24. data/app/views/chrono_forge/dashboard/workflows/index.html.erb +3 -3
  25. data/app/views/chrono_forge/dashboard/workflows/show.html.erb +1 -0
  26. data/app/views/layouts/chrono_forge/dashboard/application.html.erb +18 -10
  27. data/config/routes.rb +6 -1
  28. data/lib/chrono_forge/dashboard/configuration.rb +2 -2
  29. data/lib/chrono_forge/dashboard/version.rb +1 -1
  30. metadata +10 -2
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.3.1 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--font-mono:ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-900:oklch(41.4% .112 45.904);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-700:oklch(50% .134 242.749);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-700:oklch(49.1% .27 292.581);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-800:oklch(45.5% .188 13.697);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-white:#fff;--spacing:.25rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{.cf-pill{align-items:center;gap:calc(var(--spacing) * 1.5);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-width:1px;border-radius:3.40282e38px;display:inline-flex}.cf-pill-idle{border-color:var(--color-zinc-200);background-color:var(--color-zinc-100);color:var(--color-zinc-600)}.cf-pill-running{border-color:var(--color-sky-200);background-color:var(--color-sky-50);color:var(--color-sky-700)}.cf-pill-completed{border-color:var(--color-emerald-200);background-color:var(--color-emerald-50);color:var(--color-emerald-700)}.cf-pill-failed{border-color:var(--color-rose-200);background-color:var(--color-rose-50);color:var(--color-rose-700)}.cf-pill-stalled{border-color:var(--color-amber-200);background-color:var(--color-amber-50);color:var(--color-amber-700)}.cf-pill-scheduled{border-color:var(--color-violet-200);background-color:var(--color-violet-50);color:var(--color-violet-700)}.cf-dot{height:calc(var(--spacing) * 2);width:calc(var(--spacing) * 2);border-radius:3.40282e38px;display:inline-block}.cf-dot-idle{background-color:var(--color-zinc-400)}.cf-dot-running{background-color:var(--color-sky-500)}.cf-dot-completed{background-color:var(--color-emerald-500)}.cf-dot-failed{background-color:var(--color-rose-500)}.cf-dot-stalled{background-color:var(--color-amber-500)}.cf-dot-scheduled{background-color:var(--color-violet-500)}.cf-dot-pending{background-color:var(--color-zinc-300)}.cf-btn{align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-zinc-300);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-zinc-800);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}@media (hover:hover){.cf-btn:hover{background-color:var(--color-zinc-50)}}.cf-btn:focus{--tw-outline-style:none;outline-style:none}.cf-btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:#18181b26}@supports (color:color-mix(in lab, red, red)){.cf-btn:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-zinc-900) 15%, transparent)}}.cf-btn-primary{border-color:var(--color-zinc-900);background-color:var(--color-zinc-900);color:var(--color-white)}@media (hover:hover){.cf-btn-primary:hover{background-color:var(--color-zinc-700)}}.cf-btn-danger{border-color:var(--color-rose-300);color:var(--color-rose-700)}@media (hover:hover){.cf-btn-danger:hover{background-color:var(--color-rose-50)}}.cf-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-zinc-200);background-color:var(--color-white);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.cf-bar{height:100%}.cf-bar-0{width:0%}.cf-bar-5{width:5%}.cf-bar-10{width:10%}.cf-bar-15{width:15%}.cf-bar-20{width:20%}.cf-bar-25{width:25%}.cf-bar-30{width:30%}.cf-bar-35{width:35%}.cf-bar-40{width:40%}.cf-bar-45{width:45%}.cf-bar-50{width:50%}.cf-bar-55{width:55%}.cf-bar-60{width:60%}.cf-bar-65{width:65%}.cf-bar-70{width:70%}.cf-bar-75{width:75%}.cf-bar-80{width:80%}.cf-bar-85{width:85%}.cf-bar-90{width:90%}.cf-bar-95{width:95%}.cf-bar-100{width:100%}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-4{top:calc(var(--spacing) * 4)}.right-4{right:calc(var(--spacing) * 4)}.-left-\[5px\]{left:-5px}.z-50{z-index:50}.order-99{order:99}.order-1000{order:1000}.order-1001{order:1001}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-16{width:calc(var(--spacing) * 16)}.w-24{width:calc(var(--spacing) * 24)}.w-32{width:calc(var(--spacing) * 32)}.w-64{width:calc(var(--spacing) * 64)}.w-80{width:calc(var(--spacing) * 80)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.max-w-\[16rem\]{max-width:16rem}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-full{max-width:100%}.min-w-0{min-width:0}.min-w-\[36rem\]{min-width:36rem}.min-w-\[40rem\]{min-width:40rem}.min-w-\[44rem\]{min-width:44rem}.flex-1{flex:1}.shrink-0{flex-shrink:0}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}.gap-y-1{row-gap:var(--spacing)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-amber-100>:not(:last-child)){border-color:var(--color-amber-100)}:where(.divide-zinc-100>:not(:last-child)){border-color:var(--color-zinc-100)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-amber-200{border-color:var(--color-amber-200)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-rose-200{border-color:var(--color-rose-200)}.border-zinc-100{border-color:var(--color-zinc-100)}.border-zinc-200{border-color:var(--color-zinc-200)}.border-zinc-300{border-color:var(--color-zinc-300)}.border-zinc-900{border-color:var(--color-zinc-900)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-50\/40{background-color:#fffbeb66}@supports (color:color-mix(in lab, red, red)){.bg-amber-50\/40{background-color:color-mix(in oklab, var(--color-amber-50) 40%, transparent)}}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-300{background-color:var(--color-rose-300)}.bg-rose-400{background-color:var(--color-rose-400)}.bg-white{background-color:var(--color-white)}.bg-zinc-50{background-color:var(--color-zinc-50)}.bg-zinc-100{background-color:var(--color-zinc-100)}.bg-zinc-900{background-color:var(--color-zinc-900)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-900{color:var(--color-amber-900)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-800{color:var(--color-emerald-800)}.text-rose-500{color:var(--color-rose-500)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-white{color:var(--color-white)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.text-zinc-700{color:var(--color-zinc-700)}.text-zinc-800{color:var(--color-zinc-800)}.text-zinc-900{color:var(--color-zinc-900)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-white{--tw-ring-color:var(--color-white)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.placeholder\:text-zinc-400::placeholder{color:var(--color-zinc-400)}.last\:border-l-transparent:last-child{border-left-color:#0000}.last\:pb-0:last-child{padding-bottom:0}@media (hover:hover){.hover\:bg-zinc-50:hover{background-color:var(--color-zinc-50)}.hover\:text-zinc-900:hover{color:var(--color-zinc-900)}.hover\:underline:hover{text-decoration-line:underline}}@media (min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--font-mono:ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-700:oklch(50% .134 242.749);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-700:oklch(49.1% .27 292.581);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-800:oklch(45.5% .188 13.697);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-white:#fff;--spacing:.25rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{.cf-pill{align-items:center;gap:calc(var(--spacing) * 1.5);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-width:1px;border-radius:3.40282e38px;display:inline-flex}.cf-pill-idle{border-color:var(--color-zinc-200);background-color:var(--color-zinc-100);color:var(--color-zinc-600)}.cf-pill-running{border-color:var(--color-sky-200);background-color:var(--color-sky-50);color:var(--color-sky-700)}.cf-pill-completed{border-color:var(--color-emerald-200);background-color:var(--color-emerald-50);color:var(--color-emerald-700)}.cf-pill-failed{border-color:var(--color-rose-200);background-color:var(--color-rose-50);color:var(--color-rose-700)}.cf-pill-stalled{border-color:var(--color-amber-200);background-color:var(--color-amber-50);color:var(--color-amber-700)}.cf-pill-scheduled{border-color:var(--color-violet-200);background-color:var(--color-violet-50);color:var(--color-violet-700)}.cf-dot{height:calc(var(--spacing) * 2);width:calc(var(--spacing) * 2);border-radius:3.40282e38px;display:inline-block}.cf-dot-idle{background-color:var(--color-zinc-400)}.cf-dot-running{background-color:var(--color-sky-500)}.cf-dot-completed{background-color:var(--color-emerald-500)}.cf-dot-failed{background-color:var(--color-rose-500)}.cf-dot-stalled{background-color:var(--color-amber-500)}.cf-dot-scheduled{background-color:var(--color-violet-500)}.cf-dot-pending{background-color:var(--color-zinc-300)}.cf-btn{align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-zinc-300);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-zinc-800);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:inline-flex}@media (hover:hover){.cf-btn:hover{background-color:var(--color-zinc-50)}}.cf-btn:focus{--tw-outline-style:none;outline-style:none}.cf-btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:#18181b26}@supports (color:color-mix(in lab, red, red)){.cf-btn:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-zinc-900) 15%, transparent)}}.cf-btn-primary{border-color:var(--color-zinc-900);background-color:var(--color-zinc-900);color:var(--color-white)}@media (hover:hover){.cf-btn-primary:hover{background-color:var(--color-zinc-700)}}.cf-btn-danger{border-color:var(--color-rose-300);color:var(--color-rose-700)}@media (hover:hover){.cf-btn-danger:hover{background-color:var(--color-rose-50)}}.cf-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-zinc-200);background-color:var(--color-white);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.cf-bar{height:100%}.cf-bar-0{width:0%}.cf-bar-5{width:5%}.cf-bar-10{width:10%}.cf-bar-15{width:15%}.cf-bar-20{width:20%}.cf-bar-25{width:25%}.cf-bar-30{width:30%}.cf-bar-35{width:35%}.cf-bar-40{width:40%}.cf-bar-45{width:45%}.cf-bar-50{width:50%}.cf-bar-55{width:55%}.cf-bar-60{width:60%}.cf-bar-65{width:65%}.cf-bar-70{width:70%}.cf-bar-75{width:75%}.cf-bar-80{width:80%}.cf-bar-85{width:85%}.cf-bar-90{width:90%}.cf-bar-95{width:95%}.cf-bar-100{width:100%}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-4{top:calc(var(--spacing) * 4)}.right-4{right:calc(var(--spacing) * 4)}.-left-\[5px\]{left:-5px}.z-50{z-index:50}.order-99{order:99}.order-1000{order:1000}.order-1001{order:1001}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.ml-1{margin-left:var(--spacing)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-\[72vh\]{height:72vh}.min-h-\[3rem\]{min-height:3rem}.min-h-screen{min-height:100vh}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-16{width:calc(var(--spacing) * 16)}.w-24{width:calc(var(--spacing) * 24)}.w-32{width:calc(var(--spacing) * 32)}.w-64{width:calc(var(--spacing) * 64)}.w-80{width:calc(var(--spacing) * 80)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.max-w-\[16rem\]{max-width:16rem}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-full{max-width:100%}.min-w-0{min-width:0}.min-w-\[36rem\]{min-width:36rem}.min-w-\[40rem\]{min-width:40rem}.min-w-\[44rem\]{min-width:44rem}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}.gap-y-1{row-gap:var(--spacing)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-amber-100>:not(:last-child)){border-color:var(--color-amber-100)}:where(.divide-zinc-100>:not(:last-child)){border-color:var(--color-zinc-100)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-rose-200{border-color:var(--color-rose-200)}.border-zinc-100{border-color:var(--color-zinc-100)}.border-zinc-200{border-color:var(--color-zinc-200)}.border-zinc-300{border-color:var(--color-zinc-300)}.border-zinc-900{border-color:var(--color-zinc-900)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-50\/40{background-color:#fffbeb66}@supports (color:color-mix(in lab, red, red)){.bg-amber-50\/40{background-color:color-mix(in oklab, var(--color-amber-50) 40%, transparent)}}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-300{background-color:var(--color-rose-300)}.bg-rose-400{background-color:var(--color-rose-400)}.bg-white{background-color:var(--color-white)}.bg-zinc-50{background-color:var(--color-zinc-50)}.bg-zinc-100{background-color:var(--color-zinc-100)}.bg-zinc-900{background-color:var(--color-zinc-900)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-6{padding-left:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-wrap{text-wrap:wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-800{color:var(--color-emerald-800)}.text-rose-500{color:var(--color-rose-500)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-white{color:var(--color-white)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.text-zinc-700{color:var(--color-zinc-700)}.text-zinc-800{color:var(--color-zinc-800)}.text-zinc-900{color:var(--color-zinc-900)}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-white{--tw-ring-color:var(--color-white)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.placeholder\:text-zinc-400::placeholder{color:var(--color-zinc-400)}.last\:border-l-transparent:last-child{border-left-color:#0000}.last\:pb-0:last-child{padding-bottom:0}@media (hover:hover){.hover\:bg-rose-50:hover{background-color:var(--color-rose-50)}.hover\:bg-zinc-50:hover{background-color:var(--color-zinc-50)}.hover\:text-zinc-900:hover{color:var(--color-zinc-900)}.hover\:underline:hover{text-decoration-line:underline}}@media (min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
@@ -79,10 +79,47 @@
79
79
  // so polling doesn't yank a table back while it's being scrolled.
80
80
  var scrolls = Array.prototype.map.call(
81
81
  region.querySelectorAll(".overflow-x-auto"), function (el) { return el.scrollLeft; });
82
+
83
+ // Preserve in-progress text in the filter boxes. The swap replaces the
84
+ // inputs with server-rendered ones reflecting only the LAST SUBMITTED
85
+ // query, so without this every poll tick wipes whatever is being typed
86
+ // and drops focus. Capture each named text field's value, plus the caret
87
+ // of the focused one, and reapply them after the swap.
88
+ var isTextEntry = function (el) {
89
+ if (!el) return false;
90
+ if (el.tagName === "TEXTAREA") return true;
91
+ if (el.tagName !== "INPUT") return false;
92
+ return /^(text|search|email|url|tel|number|password)$/i.test(el.type || "text");
93
+ };
94
+ var values = {};
95
+ region.querySelectorAll("input, textarea").forEach(function (el) {
96
+ if (el.name && isTextEntry(el)) values[el.name] = el.value;
97
+ });
98
+ var active = document.activeElement;
99
+ var activeName = (active && region.contains(active) && isTextEntry(active) && active.name) ? active.name : null;
100
+ var caretStart = null, caretEnd = null;
101
+ if (activeName) {
102
+ try { caretStart = active.selectionStart; caretEnd = active.selectionEnd; } catch (e) {}
103
+ }
104
+
82
105
  region.innerHTML = fresh.innerHTML;
106
+
83
107
  region.querySelectorAll(".overflow-x-auto").forEach(function (el, i) {
84
108
  if (scrolls[i]) el.scrollLeft = scrolls[i];
85
109
  });
110
+ // Reapply preserved field values, then restore focus + caret.
111
+ region.querySelectorAll("input, textarea").forEach(function (el) {
112
+ if (el.name && isTextEntry(el) && Object.prototype.hasOwnProperty.call(values, el.name)) {
113
+ el.value = values[el.name];
114
+ }
115
+ });
116
+ if (activeName) {
117
+ var refocus = region.querySelector("[name='" + activeName + "']");
118
+ if (refocus) {
119
+ refocus.focus();
120
+ try { refocus.setSelectionRange(caretStart, caretEnd); } catch (e) {}
121
+ }
122
+ }
86
123
  }).catch(function () {});
87
124
  }, interval);
88
125
  }
@@ -0,0 +1,161 @@
1
+ // Renders the workflow definition DAG with Cytoscape + dagre. Reads the graph as
2
+ // JSON from #cf-graph[data-graph] (structured elements — no text grammar) and
3
+ // paints kind (node shape) + run status (fill/border). Interactive: pan/zoom,
4
+ // and tapping a node/edge shows its details and highlights its neighborhood.
5
+ (function () {
6
+ var el = document.getElementById("cf-graph");
7
+ if (!el || typeof cytoscape === "undefined") return;
8
+ if (window.cytoscapeDagre) {
9
+ try { cytoscape.use(window.cytoscapeDagre); } catch (e) { /* already registered */ }
10
+ }
11
+
12
+ var graph;
13
+ try { graph = JSON.parse(el.getAttribute("data-graph")); } catch (e) { return; }
14
+
15
+ // Escape before writing into innerHTML: labels, step names and guard text are
16
+ // author-controlled source strings (step-name validation only forbids "$"), and
17
+ // guards legitimately contain "<"/">", so they must not be treated as markup.
18
+ var ESC = {"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"};
19
+ function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return ESC[c]; }); }
20
+
21
+ // Node shape per durable kind (a legend in the view maps these back to names).
22
+ var SHAPE = {
23
+ execute: "round-rectangle", wait: "round-rectangle", wait_until: "round-rectangle",
24
+ continue_if: "round-rectangle", branch: "round-rectangle", merge: "round-rectangle",
25
+ repeat: "round-rectangle", join: "ellipse", dynamic: "round-rectangle",
26
+ endpoint: "round-tag"
27
+ };
28
+ // [fill, border, text] per run status. not_reached is intentionally muted so the
29
+ // path a run HASN'T reached recedes behind the path it has.
30
+ var COLOR = {
31
+ done: ["#ecfdf5", "#10b981", "#065f46"],
32
+ active: ["#eff6ff", "#3b82f6", "#1e40af"],
33
+ pending: ["#fafafa", "#d4d4d8", "#52525b"],
34
+ not_reached: ["#ffffff", "#e4e4e7", "#a1a1aa"],
35
+ failed: ["#fef2f2", "#ef4444", "#991b1b"],
36
+ stalled: ["#fff7ed", "#f59e0b", "#9a3412"],
37
+ unmapped: ["#fafaf9", "#d6d3d1", "#78716c"]
38
+ };
39
+
40
+ // Lead with the step's own name; the DSL word (kind) is carried by the shape +
41
+ // the small kind line, so "durably_execute send_payment_reminder" reads as the
42
+ // bold "send_payment_reminder" over a muted "execute".
43
+ var KIND_WORD = {
44
+ execute: "execute", wait: "wait", wait_until: "wait until", continue_if: "continue if",
45
+ branch: "branch", merge: "merge", repeat: "repeat", dynamic: "dynamic", join: "join"
46
+ };
47
+ graph.nodes.forEach(function (n) {
48
+ var kind = (n.classes.match(/kind-(\w+)/) || [])[1] || "";
49
+ if (kind === "endpoint") { n.data.display = n.data.label; return; }
50
+ var lbl = n.data.label || "";
51
+ var name = lbl.indexOf(" ") > -1 ? lbl.slice(lbl.indexOf(" ") + 1) : lbl;
52
+ // A repeat carries how many times it has run so far; show it inline (×N).
53
+ if (n.data.repetitions) name += " ×" + n.data.repetitions;
54
+ n.data.display = (KIND_WORD[kind] ? KIND_WORD[kind].toUpperCase() + "\n" : "") + name;
55
+ });
56
+
57
+ var style = [
58
+ {
59
+ selector: "node",
60
+ style: {
61
+ "label": "data(display)", "text-wrap": "wrap", "text-max-width": "150px",
62
+ "text-valign": "center", "text-halign": "center", "text-justification": "center",
63
+ "font-family": "ui-sans-serif, system-ui, sans-serif", "font-size": "11px",
64
+ "line-height": 1.35, "width": "170px", "height": "label",
65
+ "padding": "12px", "shape": "round-rectangle", "corner-radius": "10px",
66
+ "border-width": 1.5, "background-color": "#fff", "border-color": "#e4e4e7",
67
+ "color": "#3f3f46", "text-outline-width": 0
68
+ }
69
+ },
70
+ {selector: "edge", style: {
71
+ // Guard sits near the TARGET (not mid-edge), so several edges leaving the
72
+ // same node don't stack their labels on top of each other near the source.
73
+ "target-label": "data(label)", "target-text-offset": "42px",
74
+ "font-size": "9px", "font-family": "ui-sans-serif, system-ui, sans-serif",
75
+ "color": "#64748b", "curve-style": "taxi", "taxi-direction": "downward",
76
+ "taxi-turn": "40%", "taxi-turn-min-distance": "8px",
77
+ "width": 1.5, "line-color": "#cbd5e1", "target-arrow-shape": "triangle",
78
+ "target-arrow-color": "#94a3b8", "arrow-scale": 0.9,
79
+ "text-background-color": "#fff", "text-background-opacity": 1,
80
+ "text-background-padding": "3px", "text-background-shape": "round-rectangle",
81
+ "text-border-color": "#e2e8f0", "text-border-width": 1, "text-border-opacity": 1,
82
+ "text-max-width": "150px", "text-wrap": "ellipsis", "text-events": "yes"
83
+ }},
84
+ {selector: "edge.kind-terminal", style: {
85
+ "line-style": "dashed", "line-color": "#f87171", "target-arrow-color": "#ef4444", "color": "#b91c1c"
86
+ }},
87
+ {selector: "node.dim", style: {"opacity": 0.2}},
88
+ {selector: "edge.dim", style: {"opacity": 0.12}},
89
+ {selector: "node.focus", style: {"border-width": 3}},
90
+ {selector: "node.hover", style: {"border-width": 2.5}}
91
+ ];
92
+ Object.keys(SHAPE).forEach(function (k) {
93
+ style.push({selector: "node.kind-" + k, style: {"shape": SHAPE[k]}});
94
+ });
95
+ Object.keys(COLOR).forEach(function (s) {
96
+ style.push({selector: "node.status-" + s, style: {
97
+ "background-color": COLOR[s][0], "border-color": COLOR[s][1], "color": COLOR[s][2]
98
+ }});
99
+ });
100
+ style.push({selector: "node.kind-endpoint", style: {
101
+ "background-color": "#18181b", "border-color": "#18181b", "color": "#fff",
102
+ "width": "label", "font-size": "10px", "font-weight": "600", "padding": "8px"
103
+ }});
104
+
105
+ var cy = cytoscape({
106
+ container: el,
107
+ elements: graph,
108
+ style: style,
109
+ layout: {
110
+ name: "dagre", rankDir: "TB", nodeSep: 42, rankSep: 70, edgeSep: 18,
111
+ ranker: "network-simplex", padding: 30
112
+ },
113
+ minZoom: 0.25, maxZoom: 2.5, wheelSensitivity: 0.25, autoungrabify: true
114
+ });
115
+ cy.ready(function () { cy.fit(cy.elements(), 36); });
116
+
117
+ var detail = document.getElementById("cf-graph-detail");
118
+ function hint() {
119
+ if (detail) detail.innerHTML = '<span class="text-zinc-400">Tap a node or edge to inspect it. Scroll to zoom, drag to pan.</span>';
120
+ }
121
+ function clearFocus() { cy.elements().removeClass("dim focus"); hint(); }
122
+
123
+ cy.on("mouseover", "node", function (e) { e.target.addClass("hover"); });
124
+ cy.on("mouseout", "node", function (e) { e.target.removeClass("hover"); });
125
+
126
+ cy.on("tap", "node", function (evt) {
127
+ var n = evt.target, hood = n.closedNeighborhood();
128
+ cy.elements().addClass("dim");
129
+ hood.removeClass("dim");
130
+ cy.nodes().removeClass("focus");
131
+ n.addClass("focus");
132
+ if (detail) {
133
+ var d = n.data(), cls = n.classes() || [];
134
+ var kind = (cls.join(" ").match(/kind-(\w+)/) || [])[1] || "";
135
+ var status = (cls.join(" ").match(/status-(\w+)/) || [])[1] || "";
136
+ // Run aggregates: a repeat's execution count, and a fan-out's per-state
137
+ // child tally ("2 completed · 1 failed") — rendered only when present.
138
+ var extra = "";
139
+ if (d.repetitions) extra += '<div class="mt-0.5 text-zinc-500">' + esc(d.repetitions) + " repetition" + (d.repetitions === 1 ? "" : "s") + "</div>";
140
+ if (d.counts) {
141
+ var parts = Object.keys(d.counts).map(function (k) { return esc(d.counts[k]) + " " + esc(k); });
142
+ if (parts.length) extra += '<div class="mt-0.5 text-zinc-500">' + parts.join(" &middot; ") + "</div>";
143
+ }
144
+ detail.innerHTML =
145
+ '<div class="font-medium text-zinc-800">' + esc(d.label || d.id) + "</div>" +
146
+ '<div class="text-zinc-500">' + esc(kind) + (status ? ' &middot; <span class="font-medium">' + esc(status.replace("_", " ")) + "</span>" : "") + "</div>" +
147
+ (d.step_name ? '<div class="mt-0.5 font-mono text-[11px] text-zinc-500">' + esc(d.step_name) + "</div>" : "") +
148
+ extra;
149
+ }
150
+ });
151
+ cy.on("tap", "edge", function (evt) {
152
+ var g = evt.target.data("label");
153
+ if (detail) {
154
+ detail.innerHTML = g && g.length
155
+ ? '<div class="text-zinc-500">guard</div><div class="font-mono text-[11px] text-zinc-700">' + esc(g) + "</div>"
156
+ : '<span class="text-zinc-400">unconditional edge</span>';
157
+ }
158
+ });
159
+ cy.on("tap", function (evt) { if (evt.target === cy) clearFocus(); });
160
+ hint();
161
+ })();
@@ -4,7 +4,14 @@ module ChronoForge
4
4
  skip_before_action :authenticate!
5
5
  skip_forgery_protection
6
6
 
7
- TYPES = {"dashboard.css" => "text/css", "dashboard.js" => "application/javascript"}.freeze
7
+ TYPES = {
8
+ "dashboard.css" => "text/css",
9
+ "dashboard.js" => "application/javascript",
10
+ "cytoscape.min.js" => "application/javascript",
11
+ "dagre.min.js" => "application/javascript",
12
+ "cytoscape-dagre.js" => "application/javascript",
13
+ "definition_graph.js" => "application/javascript"
14
+ }.freeze
8
15
  ROOT = ChronoForge::Dashboard::Engine.root.join("app/assets/chrono_forge/dashboard")
9
16
 
10
17
  def show
@@ -0,0 +1,35 @@
1
+ module ChronoForge
2
+ module Dashboard
3
+ # Per-run view of a workflow's statically-analyzed definition graph, with the
4
+ # run's execution logs overlaid onto the nodes. Analysis is best-effort: an
5
+ # unknown/unloadable class or an unanalyzable perform yields a warning, not a
6
+ # 500, so the page always renders.
7
+ class DefinitionsController < BaseController
8
+ def show
9
+ # Opt out of the auto-refresh region swap: it replaces the page's HTML in
10
+ # place, which wipes the live Cytoscape canvas (injected <script>s don't
11
+ # re-run), leaving a blank graph. This page reloads fully instead.
12
+ @cf_disable_polling = true
13
+ @workflow = ChronoForge::Workflow.find(params[:id])
14
+ definition = analyze(@workflow)
15
+ overlay = DefinitionOverlay.new(definition, @workflow)
16
+ @nodes = overlay.nodes
17
+ @warnings = overlay.warnings
18
+ @graph = CytoscapeGraph.new(@nodes, definition.edges).to_h
19
+ end
20
+
21
+ private
22
+
23
+ # DefinitionAnalyzer.call always returns a Definition (an `unavailable` one on
24
+ # any analysis failure), so the only gap is a class that won't constantize.
25
+ def analyze(workflow)
26
+ ChronoForge::DefinitionAnalyzer.call(workflow.job_class.constantize)
27
+ rescue NameError
28
+ ChronoForge::Definition.new(
29
+ warnings: ["workflow class #{workflow.job_class} is not loadable and " \
30
+ "cannot be statically analyzed"]
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,10 +2,14 @@ module ChronoForge
2
2
  module Dashboard
3
3
  class WorkflowsController < BaseController
4
4
  def index
5
- @query = WorkflowsQuery.new(**list_params)
5
+ @hide_branches = params[:hide_branches] != "0" # on by default
6
+ @query = WorkflowsQuery.new(**list_params, exclude_branched: @hide_branches)
6
7
  @workflows = @query.records
7
8
  @waits = WaitStatePresenter.active_map(@workflows)
8
- stats = StatsQuery.new
9
+ # Stats track the toggle so the counts match the visible list — a large
10
+ # fan-out's children don't dominate the totals while hidden from the list.
11
+ stats_base = @hide_branches ? ChronoForge::Workflow.where(parent_execution_log_id: nil) : ChronoForge::Workflow.all
12
+ stats = StatsQuery.new(base: stats_base)
9
13
  @stats = stats.counts
10
14
  @stats_cap = stats.cap
11
15
  end
@@ -32,6 +32,23 @@ module ChronoForge
32
32
  tag.span(class: "cf-dot cf-dot-#{state}")
33
33
  end
34
34
 
35
+ # A filter chip: optional colored state dot, a label, and an optional capped
36
+ # count, with active styling. Shared by the index stats header and the
37
+ # branch-children filter row. The caller computes +href+ (the two views build
38
+ # different URLs); +dot+ is a state name for the colored dot, or nil for none
39
+ # (e.g. the "all" chip, or a composite like "blocked").
40
+ def cf_filter_chip(href, label:, active:, count: nil, cap: nil, dot: nil)
41
+ classes = "cf-stat flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition " +
42
+ (active ? "border-zinc-900 bg-zinc-50" : "border-zinc-200 bg-white hover:bg-zinc-50")
43
+ link_to href, class: classes do
44
+ safe_join([
45
+ dot ? cf_dot(dot) : "",
46
+ tag.span(label, class: "text-zinc-500"),
47
+ count ? tag.span(cf_capped(count, cap), class: "font-mono font-medium tabular-nums") : ""
48
+ ])
49
+ end
50
+ end
51
+
35
52
  def cf_time(t)
36
53
  t&.iso8601 || "—"
37
54
  end
@@ -61,6 +78,12 @@ module ChronoForge
61
78
  (secs % 60 == 0) ? "#{secs / 60}m" : "#{secs}s"
62
79
  end
63
80
 
81
+ # Whether the main region opts into auto-refresh. A page sets
82
+ # @cf_disable_polling to opt OUT (e.g. the definition graph, whose live
83
+ # Cytoscape canvas can't survive the poll's innerHTML region swap). Without a
84
+ # [data-poll-region] the JS never starts a poll timer for the page.
85
+ def cf_poll_region? = !@cf_disable_polling
86
+
64
87
  # A timestamp shown relative ("3 minutes ago") or absolute (raw ISO8601)
65
88
  # per the viewer's preference, with the other form available on hover.
66
89
  def cf_ago(t)
@@ -71,6 +94,16 @@ module ChronoForge
71
94
  tag.span(shown, title: hover, class: "cursor-help")
72
95
  end
73
96
 
97
+ # Like cf_ago but direction-aware: future times read "in 3 minutes", past
98
+ # times "3 minutes ago" — for things like a poller's next scheduled check.
99
+ def cf_when(t)
100
+ return "—" unless t
101
+ rel = t.future? ? "in #{time_ago_in_words(t)}" : "#{time_ago_in_words(t)} ago"
102
+ abs = t.iso8601
103
+ shown, hover = cf_absolute_time? ? [abs, rel] : [rel, abs]
104
+ tag.span(shown, title: hover, class: "cursor-help")
105
+ end
106
+
74
107
  # Human duration between two times (e.g. "1m 04s"); "—" if unfinished.
75
108
  def cf_duration(from, to)
76
109
  return "—" unless from && to
@@ -20,8 +20,10 @@ module ChronoForge
20
20
  # The branch is "sealed" once its block closed (done dispatching children).
21
21
  def sealed? = @log.completed?
22
22
 
23
- def dispatched = capped(children)
23
+ def spawned = capped(children)
24
+
24
25
  def pending = capped(children.where.not(state: ChronoForge::Workflow.states[:completed]))
26
+
25
27
  def blocked = capped(children.where(state: BLOCKED_STATES))
26
28
 
27
29
  def cap = CAP
@@ -35,8 +37,11 @@ module ChronoForge
35
37
  # The BranchMergeJob stamps its poll state onto the branch log's metadata
36
38
  # (it can't be queried from the backend; ActiveJob has no such API).
37
39
  def polled? = poll.present?
40
+
38
41
  def last_polled_at = parse_time(poll&.dig("last_polled_at"))
42
+
39
43
  def next_poll_at = parse_time(poll&.dig("next_poll_at"))
44
+
40
45
  def polls = poll&.dig("polls").to_i
41
46
 
42
47
  # next_poll_at is nil once the merge completes, so a finished merge never
@@ -46,6 +51,38 @@ module ChronoForge
46
51
  t.present? && t < Time.current - POLL_OVERDUE_GRACE
47
52
  end
48
53
 
54
+ # Live throughput/ETA from the last poll (the poller measures the branch's
55
+ # completion rate each pass). The wake poll records rate 0, so a positive rate
56
+ # means the branch is still draining — a merged/idle branch shows no gauge.
57
+ def rate = poll&.dig("rate").to_f
58
+
59
+ def eta_seconds = poll&.dig("eta_seconds")
60
+
61
+ def throughput? = rate > 0
62
+
63
+ # Children dispatched but not yet started (idle, started_at nil) — how much of
64
+ # this branch hasn't been picked up yet. Capped/index-only like the other counts.
65
+ def never_started = capped(children.where(state: ChronoForge::Workflow.states[:idle], started_at: nil))
66
+
67
+ # The FULL (uncapped) pending / never-started counts the poller ALREADY records
68
+ # each pass — so the dashboard can show the real number instead of the capped
69
+ # "CAP+", with NO new query. nil until the branch has been polled; callers fall
70
+ # back to the capped count. (The poll's "never_started" is the never-started count.)
71
+ def exact_pending = poll&.dig("pending")
72
+
73
+ def exact_never_started = poll&.dig("never_started")
74
+
75
+ # Total spawned, counted once and cached by the poller when the branch sealed
76
+ # (see BranchProbe#spawned) — exact and free. nil until sealed+polled; the
77
+ # "Spawned" column then falls back to the capped live count.
78
+ def exact_spawned = poll&.dig("spawned")
79
+
80
+ # Dropped-child recovery: how many children the poller has rekicked, and when
81
+ # it last did (nil if never).
82
+ def rekicks = poll&.dig("rekick_total").to_i
83
+
84
+ def last_rekick_at = parse_time(poll&.dig("last_rekick_at"))
85
+
49
86
  private
50
87
 
51
88
  def children = @log.spawned_workflows
@@ -6,8 +6,19 @@ module ChronoForge
6
6
  class BranchesPresenter
7
7
  # One merge join (a merge$<names> log = a BranchMergeJob's durable target).
8
8
  # state: :merging (pending — a poller is joining) | :merged (completed).
9
- Merge = Struct.new(:names, :state, :started_at) do
9
+ # The poll fields are the BranchMergeJob's observable cadence (it stamps them
10
+ # on the target branch logs each pass): when it last checked, when it's
11
+ # scheduled to check next, and how many times it has polled.
12
+ Merge = Struct.new(:names, :state, :started_at, :last_polled_at, :next_poll_at, :polls, :rate, :eta_seconds) do
10
13
  def merging? = state == :merging
14
+
15
+ # A next check scheduled in the past while still merging means the poller
16
+ # was dropped (or is overdue) — the join is stuck until it's re-armed.
17
+ def poll_overdue? = merging? && next_poll_at && next_poll_at.past?
18
+
19
+ # Throughput is a live gauge — only meaningful while merging and actually
20
+ # draining (rate 0.0 means idle; a merged join has no live rate).
21
+ def throughput? = merging? && rate.to_f > 0
11
22
  end
12
23
 
13
24
  def initialize(workflow) = @workflow = workflow
@@ -23,13 +34,57 @@ module ChronoForge
23
34
  # The merge joins on this workflow, in-progress first. A long-pending merge
24
35
  # with no blocked children is the sign of a dropped BranchMergeJob poller.
25
36
  def merges
26
- @merges ||= merge_logs
27
- .map { |log| Merge.new(StepNameParser.parse(log.step_name).name.split(","), log.completed? ? :merged : :merging, log.started_at) }
28
- .sort_by { |m| [m.merging? ? 0 : 1, m.started_at || Time.current] }
37
+ @merges ||= merge_logs.map { |log|
38
+ names = StepNameParser.parse(log.step_name).name.split(",")
39
+ poll = merge_poll(names)
40
+ rate, eta = merge_throughput(names)
41
+ Merge.new(
42
+ names,
43
+ log.completed? ? :merged : :merging,
44
+ log.started_at,
45
+ parse_time(poll&.dig("last_polled_at")),
46
+ parse_time(poll&.dig("next_poll_at")),
47
+ poll&.dig("polls"),
48
+ rate,
49
+ eta
50
+ )
51
+ }.sort_by { |m| [m.merging? ? 0 : 1, m.started_at || Time.current] }
29
52
  end
30
53
 
31
54
  private
32
55
 
56
+ # Shared cadence timestamps for a merge. BranchMergeJob writes the SAME
57
+ # last_polled_at/next_poll_at/polls to every target branch each pass, so any
58
+ # one works; take the latest by last_polled_at to be safe. (rate/eta_seconds
59
+ # are per-branch, so they are aggregated separately — see #merge_throughput.)
60
+ def merge_poll(names)
61
+ names.filter_map { |nm| branch_poll_by_name[nm] }.max_by { |p| p["last_polled_at"].to_s }
62
+ end
63
+
64
+ # Live throughput/ETA for the whole merge. A merge may join several branches
65
+ # (merge_branches(*names)), and each branch records its OWN rate/pending, so
66
+ # the merge's rate is their SUM and its ETA is the combined remaining over the
67
+ # combined rate — not any single branch's figure. Returns [rate, eta_seconds];
68
+ # rate 0.0 / eta nil when nothing is draining.
69
+ def merge_throughput(names)
70
+ polls = names.filter_map { |nm| branch_poll_by_name[nm] }
71
+ rate = polls.sum { |p| p["rate"].to_f }
72
+ return [0.0, nil] unless rate > 0
73
+ pending = polls.sum { |p| p["pending"].to_i }
74
+ [rate, (pending / rate).round]
75
+ end
76
+
77
+ def branch_poll_by_name
78
+ @branch_poll_by_name ||= branch_logs.each_with_object({}) do |log, h|
79
+ h[StepNameParser.parse(log.step_name).name] = log.metadata&.dig("poll")
80
+ end
81
+ end
82
+
83
+ def parse_time(value)
84
+ return nil if value.blank?
85
+ value.is_a?(Time) ? value : Time.zone.parse(value.to_s)
86
+ end
87
+
33
88
  def coordination_logs
34
89
  d = StepNameParser::DELIM
35
90
  @coordination_logs ||= @workflow.execution_logs