saga_forge-dashboard 0.1.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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +205 -0
  5. data/app/assets/saga_forge/dashboard/cytoscape-dagre.js +397 -0
  6. data/app/assets/saga_forge/dashboard/cytoscape.min.js +32 -0
  7. data/app/assets/saga_forge/dashboard/dagre.min.js +3809 -0
  8. data/app/assets/saga_forge/dashboard/dashboard.css +2 -0
  9. data/app/assets/saga_forge/dashboard/dashboard.js +132 -0
  10. data/app/assets/saga_forge/dashboard/saga_graph.js +143 -0
  11. data/app/assets/saga_forge/dashboard/tailwind.css +299 -0
  12. data/app/assets/saga_forge/dashboard/turbo.min.js +34 -0
  13. data/app/controllers/saga_forge/dashboard/actions_controller.rb +36 -0
  14. data/app/controllers/saga_forge/dashboard/assets_controller.rb +29 -0
  15. data/app/controllers/saga_forge/dashboard/base_controller.rb +29 -0
  16. data/app/controllers/saga_forge/dashboard/definitions_controller.rb +24 -0
  17. data/app/controllers/saga_forge/dashboard/overview_controller.rb +18 -0
  18. data/app/controllers/saga_forge/dashboard/sagas_controller.rb +25 -0
  19. data/app/controllers/saga_forge/dashboard/stalled_controller.rb +29 -0
  20. data/app/controllers/saga_forge/dashboard/suspended_controller.rb +33 -0
  21. data/app/helpers/saga_forge/dashboard/dashboard_helper.rb +77 -0
  22. data/app/jobs/saga_forge/dashboard/bulk_recovery_job.rb +20 -0
  23. data/app/presenters/saga_forge/dashboard/context_presenter.rb +50 -0
  24. data/app/presenters/saga_forge/dashboard/saga_graph.rb +59 -0
  25. data/app/presenters/saga_forge/dashboard/timeline_presenter.rb +68 -0
  26. data/app/queries/saga_forge/dashboard/overview_query.rb +32 -0
  27. data/app/queries/saga_forge/dashboard/sagas_query.rb +83 -0
  28. data/app/queries/saga_forge/dashboard/stats_query.rb +31 -0
  29. data/app/views/layouts/saga_forge/dashboard/application.html.erb +69 -0
  30. data/app/views/saga_forge/dashboard/definitions/missing.html.erb +7 -0
  31. data/app/views/saga_forge/dashboard/definitions/show.html.erb +48 -0
  32. data/app/views/saga_forge/dashboard/overview/_stat_card.html.erb +4 -0
  33. data/app/views/saga_forge/dashboard/overview/classes.html.erb +37 -0
  34. data/app/views/saga_forge/dashboard/overview/index.html.erb +33 -0
  35. data/app/views/saga_forge/dashboard/sagas/_actions.html.erb +27 -0
  36. data/app/views/saga_forge/dashboard/sagas/_compensation.html.erb +41 -0
  37. data/app/views/saga_forge/dashboard/sagas/_context_tree.html.erb +37 -0
  38. data/app/views/saga_forge/dashboard/sagas/_filters.html.erb +9 -0
  39. data/app/views/saga_forge/dashboard/sagas/_saga_row.html.erb +6 -0
  40. data/app/views/saga_forge/dashboard/sagas/_stats.html.erb +16 -0
  41. data/app/views/saga_forge/dashboard/sagas/_summary.html.erb +43 -0
  42. data/app/views/saga_forge/dashboard/sagas/_timeline.html.erb +48 -0
  43. data/app/views/saga_forge/dashboard/sagas/empty.html.erb +6 -0
  44. data/app/views/saga_forge/dashboard/sagas/index.html.erb +46 -0
  45. data/app/views/saga_forge/dashboard/sagas/show.html.erb +19 -0
  46. data/app/views/saga_forge/dashboard/stalled/index.html.erb +51 -0
  47. data/app/views/saga_forge/dashboard/suspended/index.html.erb +57 -0
  48. data/config/routes.rb +39 -0
  49. data/lib/saga_forge/dashboard/configuration.rb +30 -0
  50. data/lib/saga_forge/dashboard/engine.rb +9 -0
  51. data/lib/saga_forge/dashboard/version.rb +5 -0
  52. data/lib/saga_forge/dashboard.rb +33 -0
  53. metadata +142 -0
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.2 | 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-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-rose-50:oklch(96.9% .015 12.422);--color-rose-300:oklch(81% .117 11.638);--color-rose-700:oklch(51.4% .222 16.935);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--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-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--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);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--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{.sf-badge{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}.sf-badge-indigo{background-color:var(--sf-badge-indigo-bg);color:var(--sf-badge-indigo-fg);border-color:var(--sf-badge-indigo-border)}.sf-badge-green{background-color:var(--sf-badge-green-bg);color:var(--sf-badge-green-fg);border-color:var(--sf-badge-green-border)}.sf-badge-amber{background-color:var(--sf-badge-amber-bg);color:var(--sf-badge-amber-fg);border-color:var(--sf-badge-amber-border)}.sf-badge-red{background-color:var(--sf-badge-red-bg);color:var(--sf-badge-red-fg);border-color:var(--sf-badge-red-border)}.sf-badge-slate{background-color:var(--sf-badge-slate-bg);color:var(--sf-badge-slate-fg);border-color:var(--sf-badge-slate-border)}.sf-btn{align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-md);border-style:var(--tw-border-style);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);--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));border-width:1px;display:inline-flex}.sf-btn:focus{--tw-outline-style:none;outline-style:none}.sf-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)}.sf-btn{border-color:var(--sf-border);background-color:var(--sf-surface);color:var(--sf-fg)}.sf-btn:hover{background-color:var(--sf-bg)}.sf-btn-primary{border-color:var(--sf-accent);background-color:var(--sf-accent);color:var(--sf-accent-fg)}.sf-btn-primary:hover{opacity:.9}.sf-btn-danger{border-color:var(--color-rose-300);color:var(--color-rose-700)}.sf-btn-danger:hover{background-color:var(--color-rose-50)}.sf-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);--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);border-width:1px;border-color:var(--sf-border);background-color:var(--sf-surface)}.sf-bar{height:100%;display:block}.sf-bar-0{width:0%}.sf-bar-1{width:1%}.sf-bar-2{width:2%}.sf-bar-3{width:3%}.sf-bar-4{width:4%}.sf-bar-5{width:5%}.sf-bar-6{width:6%}.sf-bar-7{width:7%}.sf-bar-8{width:8%}.sf-bar-9{width:9%}.sf-bar-10{width:10%}.sf-bar-11{width:11%}.sf-bar-12{width:12%}.sf-bar-13{width:13%}.sf-bar-14{width:14%}.sf-bar-15{width:15%}.sf-bar-16{width:16%}.sf-bar-17{width:17%}.sf-bar-18{width:18%}.sf-bar-19{width:19%}.sf-bar-20{width:20%}.sf-bar-21{width:21%}.sf-bar-22{width:22%}.sf-bar-23{width:23%}.sf-bar-24{width:24%}.sf-bar-25{width:25%}.sf-bar-26{width:26%}.sf-bar-27{width:27%}.sf-bar-28{width:28%}.sf-bar-29{width:29%}.sf-bar-30{width:30%}.sf-bar-31{width:31%}.sf-bar-32{width:32%}.sf-bar-33{width:33%}.sf-bar-34{width:34%}.sf-bar-35{width:35%}.sf-bar-36{width:36%}.sf-bar-37{width:37%}.sf-bar-38{width:38%}.sf-bar-39{width:39%}.sf-bar-40{width:40%}.sf-bar-41{width:41%}.sf-bar-42{width:42%}.sf-bar-43{width:43%}.sf-bar-44{width:44%}.sf-bar-45{width:45%}.sf-bar-46{width:46%}.sf-bar-47{width:47%}.sf-bar-48{width:48%}.sf-bar-49{width:49%}.sf-bar-50{width:50%}.sf-bar-51{width:51%}.sf-bar-52{width:52%}.sf-bar-53{width:53%}.sf-bar-54{width:54%}.sf-bar-55{width:55%}.sf-bar-56{width:56%}.sf-bar-57{width:57%}.sf-bar-58{width:58%}.sf-bar-59{width:59%}.sf-bar-60{width:60%}.sf-bar-61{width:61%}.sf-bar-62{width:62%}.sf-bar-63{width:63%}.sf-bar-64{width:64%}.sf-bar-65{width:65%}.sf-bar-66{width:66%}.sf-bar-67{width:67%}.sf-bar-68{width:68%}.sf-bar-69{width:69%}.sf-bar-70{width:70%}.sf-bar-71{width:71%}.sf-bar-72{width:72%}.sf-bar-73{width:73%}.sf-bar-74{width:74%}.sf-bar-75{width:75%}.sf-bar-76{width:76%}.sf-bar-77{width:77%}.sf-bar-78{width:78%}.sf-bar-79{width:79%}.sf-bar-80{width:80%}.sf-bar-81{width:81%}.sf-bar-82{width:82%}.sf-bar-83{width:83%}.sf-bar-84{width:84%}.sf-bar-85{width:85%}.sf-bar-86{width:86%}.sf-bar-87{width:87%}.sf-bar-88{width:88%}.sf-bar-89{width:89%}.sf-bar-90{width:90%}.sf-bar-91{width:91%}.sf-bar-92{width:92%}.sf-bar-93{width:93%}.sf-bar-94{width:94%}.sf-bar-95{width:95%}.sf-bar-96{width:96%}.sf-bar-97{width:97%}.sf-bar-98{width:98%}.sf-bar-99{width:99%}.sf-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-4{top:calc(var(--spacing) * 4)}.right-4{right:calc(var(--spacing) * 4)}.z-50{z-index:50}.order-1001{order:1001}.order-1002{order:1002}.order-2001{order:2001}.order-2002{order:2002}.order-3001{order:3001}.order-4001{order:4001}.order-5001{order:5001}.order-6001{order:6001}.order-7001{order:7001}.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-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-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-\[68vh\]{height:68vh}.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-36{width:calc(var(--spacing) * 36)}.w-40{width:calc(var(--spacing) * 40)}.w-80{width:calc(var(--spacing) * 80)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.min-w-\[36rem\]{min-width:36rem}.min-w-\[40rem\]{min-width:40rem}.min-w-\[42rem\]{min-width:42rem}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.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-5{gap:calc(var(--spacing) * 5)}: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-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}: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-\[var\(--sf-border\)\]>:not(:last-child)){border-color:var(--sf-border)}.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-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-\[\#8b5cf6\]{border-color:#8b5cf6}.border-\[var\(--sf-badge-amber-border\)\]{border-color:var(--sf-badge-amber-border)}.border-\[var\(--sf-badge-red-border\)\]{border-color:var(--sf-badge-red-border)}.border-\[var\(--sf-border\)\]{border-color:var(--sf-border)}.border-\[var\(--sf-flash-err-border\)\]{border-color:var(--sf-flash-err-border)}.border-\[var\(--sf-flash-ok-border\)\]{border-color:var(--sf-flash-ok-border)}.border-\[var\(--sf-soft\)\]{border-color:var(--sf-soft)}.bg-\[var\(--sf-accent\)\]{background-color:var(--sf-accent)}.bg-\[var\(--sf-badge-amber-bg\)\],.bg-\[var\(--sf-badge-amber-bg\)\]\/40{background-color:var(--sf-badge-amber-bg)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--sf-badge-amber-bg\)\]\/40{background-color:color-mix(in oklab, var(--sf-badge-amber-bg) 40%, transparent)}}.bg-\[var\(--sf-badge-red-bg\)\],.bg-\[var\(--sf-badge-red-bg\)\]\/40{background-color:var(--sf-badge-red-bg)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--sf-badge-red-bg\)\]\/40{background-color:color-mix(in oklab, var(--sf-badge-red-bg) 40%, transparent)}}.bg-\[var\(--sf-bg\)\]{background-color:var(--sf-bg)}.bg-\[var\(--sf-flash-err-bg\)\]{background-color:var(--sf-flash-err-bg)}.bg-\[var\(--sf-flash-ok-bg\)\]{background-color:var(--sf-flash-ok-bg)}.bg-\[var\(--sf-surface\)\]{background-color:var(--sf-surface)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.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-4{padding-top:calc(var(--spacing) * 4)}.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-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-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-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)}.text-wrap{text-wrap:wrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[var\(--sf-accent-fg\)\]{color:var(--sf-accent-fg)}.text-\[var\(--sf-badge-amber-fg\)\]{color:var(--sf-badge-amber-fg)}.text-\[var\(--sf-badge-red-fg\)\]{color:var(--sf-badge-red-fg)}.text-\[var\(--sf-fg\)\]{color:var(--sf-fg)}.text-\[var\(--sf-flash-err-fg\)\]{color:var(--sf-flash-err-fg)}.text-\[var\(--sf-flash-ok-fg\)\]{color:var(--sf-flash-ok-fg)}.text-\[var\(--sf-soft\)\]{color:var(--sf-soft)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-700{color:var(--color-zinc-700)}.text-zinc-800{color:var(--color-zinc-800)}.lowercase{text-transform:lowercase}.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}.opacity-70{opacity:.7}.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)}.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)}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-\[var\(--sf-soft\)\]::placeholder{color:var(--sf-soft)}@media (hover:hover){.hover\:border-\[var\(--sf-accent\)\]:hover{border-color:var(--sf-accent)}.hover\:bg-\[var\(--sf-badge-amber-bg\)\]:hover{background-color:var(--sf-badge-amber-bg)}.hover\:bg-\[var\(--sf-badge-red-bg\)\]:hover{background-color:var(--sf-badge-red-bg)}.hover\:bg-\[var\(--sf-bg\)\]:hover{background-color:var(--sf-bg)}.hover\:text-\[var\(--sf-fg\)\]:hover{color:var(--sf-fg)}.hover\:underline:hover{text-decoration-line:underline}}@media (min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}:root{--sf-bg:#fafafa;--sf-surface:#fff;--sf-fg:#18181b;--sf-soft:#71717a;--sf-border:#e4e4e7;--sf-accent:#4650c8;--sf-accent-fg:#fff;--sf-badge-indigo-bg:#eef0fc;--sf-badge-indigo-fg:#4650c8;--sf-badge-indigo-border:#c7cbf0;--sf-badge-green-bg:#ecfdf5;--sf-badge-green-fg:#047857;--sf-badge-green-border:#a7f3d0;--sf-badge-amber-bg:#fffbeb;--sf-badge-amber-fg:#b45309;--sf-badge-amber-border:#fde68a;--sf-badge-red-bg:#fff1f2;--sf-badge-red-fg:#be123c;--sf-badge-red-border:#fecdd3;--sf-badge-slate-bg:#f4f4f5;--sf-badge-slate-fg:#52525b;--sf-badge-slate-border:#e4e4e7;--sf-flash-ok-bg:#ecfdf5;--sf-flash-ok-fg:#065f46;--sf-flash-ok-border:#a7f3d0;--sf-flash-err-bg:#fff1f2;--sf-flash-err-fg:#9f1239;--sf-flash-err-border:#fecdd3}@media (prefers-color-scheme:dark){:root{--sf-bg:#101317;--sf-surface:#16191f;--sf-fg:#e4e4e7;--sf-soft:#9a9aa2;--sf-border:#2a2e35;--sf-accent:#97a0f5;--sf-accent-fg:#14162b;--sf-badge-indigo-bg:#97a0f51f;--sf-badge-indigo-fg:#b7bdf7;--sf-badge-indigo-border:#97a0f54d;--sf-badge-green-bg:#10b9811f;--sf-badge-green-fg:#6ee7b7;--sf-badge-green-border:#10b9814d;--sf-badge-amber-bg:#f59e0b1f;--sf-badge-amber-fg:#fcd34d;--sf-badge-amber-border:#f59e0b4d;--sf-badge-red-bg:#f43f5e1f;--sf-badge-red-fg:#fda4af;--sf-badge-red-border:#f43f5e4d;--sf-badge-slate-bg:#a1a1aa1f;--sf-badge-slate-fg:#d4d4d8;--sf-badge-slate-border:#a1a1aa4d;--sf-flash-ok-bg:#10b9811f;--sf-flash-ok-fg:#6ee7b7;--sf-flash-ok-border:#10b9814d;--sf-flash-err-bg:#f43f5e1f;--sf-flash-err-fg:#fda4af;--sf-flash-err-border:#f43f5e4d}}:root[data-theme=light]{--sf-bg:#fafafa;--sf-surface:#fff;--sf-fg:#18181b;--sf-soft:#71717a;--sf-border:#e4e4e7;--sf-accent:#4650c8;--sf-accent-fg:#fff;--sf-badge-indigo-bg:#eef0fc;--sf-badge-indigo-fg:#4650c8;--sf-badge-indigo-border:#c7cbf0;--sf-badge-green-bg:#ecfdf5;--sf-badge-green-fg:#047857;--sf-badge-green-border:#a7f3d0;--sf-badge-amber-bg:#fffbeb;--sf-badge-amber-fg:#b45309;--sf-badge-amber-border:#fde68a;--sf-badge-red-bg:#fff1f2;--sf-badge-red-fg:#be123c;--sf-badge-red-border:#fecdd3;--sf-badge-slate-bg:#f4f4f5;--sf-badge-slate-fg:#52525b;--sf-badge-slate-border:#e4e4e7;--sf-flash-ok-bg:#ecfdf5;--sf-flash-ok-fg:#065f46;--sf-flash-ok-border:#a7f3d0;--sf-flash-err-bg:#fff1f2;--sf-flash-err-fg:#9f1239;--sf-flash-err-border:#fecdd3}:root[data-theme=dark]{--sf-bg:#101317;--sf-surface:#16191f;--sf-fg:#e4e4e7;--sf-soft:#9a9aa2;--sf-border:#2a2e35;--sf-accent:#97a0f5;--sf-accent-fg:#14162b;--sf-badge-indigo-bg:#97a0f51f;--sf-badge-indigo-fg:#b7bdf7;--sf-badge-indigo-border:#97a0f54d;--sf-badge-green-bg:#10b9811f;--sf-badge-green-fg:#6ee7b7;--sf-badge-green-border:#10b9814d;--sf-badge-amber-bg:#f59e0b1f;--sf-badge-amber-fg:#fcd34d;--sf-badge-amber-border:#f59e0b4d;--sf-badge-red-bg:#f43f5e1f;--sf-badge-red-fg:#fda4af;--sf-badge-red-border:#f43f5e4d;--sf-badge-slate-bg:#a1a1aa1f;--sf-badge-slate-fg:#d4d4d8;--sf-badge-slate-border:#a1a1aa4d;--sf-flash-ok-bg:#10b9811f;--sf-flash-ok-fg:#6ee7b7;--sf-flash-ok-border:#10b9814d;--sf-flash-err-bg:#f43f5e1f;--sf-flash-err-fg:#fda4af;--sf-flash-err-border:#f43f5e4d}@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}@keyframes pulse{50%{opacity:.5}}
@@ -0,0 +1,132 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ // Turbo Drive owns navigation and form submits, so the page is never fully
5
+ // reloaded on a normal click. Two consequences shape this file:
6
+ //
7
+ // 1. Interactive behaviors are delegated to `document` and attached ONCE.
8
+ // Per-element listeners would be lost on every body swap; `document` is
9
+ // never swapped, so one delegated listener keeps matching current and
10
+ // future rows.
11
+ // 2. Per-page setup (the poll timer, flash auto-dismiss) must re-run after
12
+ // every visit, so it lives in a `turbo:load` handler rather than at the
13
+ // top level, since top-level code runs only when the script first executes.
14
+ if (!window.__sagaForgeDashboard) {
15
+ window.__sagaForgeDashboard = true;
16
+
17
+ document.addEventListener("click", function (e) {
18
+ // Timestamp display toggle (relative vs absolute), persisted in a cookie.
19
+ // Re-visit the current URL so the server re-renders every timestamp.
20
+ var timeSet = e.target.closest("[data-time-set]");
21
+ if (timeSet) {
22
+ document.cookie = "sf_time_format=" + timeSet.getAttribute("data-time-set") +
23
+ ";path=/;max-age=31536000;samesite=lax";
24
+ Turbo.visit(window.location.href, { action: "replace" });
25
+ return;
26
+ }
27
+ // Collapsible context tree
28
+ var key = e.target.closest(".sf-context__key");
29
+ if (key && key.closest("[data-collapsible]")) {
30
+ var li = key.closest("li");
31
+ if (li) li.classList.toggle("sf-collapsed");
32
+ return;
33
+ }
34
+ // Whole-row navigation, except when the click landed on an interactive
35
+ // child. Turbo.visit keeps it an in-app visit (history + no full reload).
36
+ var row = e.target.closest("tr[data-href]");
37
+ if (row && !e.target.closest("a, button, input, form, summary")) {
38
+ Turbo.visit(row.getAttribute("data-href"));
39
+ }
40
+ });
41
+
42
+ document.addEventListener("change", function (e) {
43
+ // Auto-refresh interval control: persist and re-visit so the server
44
+ // re-renders the body's data-poll-interval (picked up in setupPage).
45
+ var poll = e.target.closest("[data-poll-select]");
46
+ if (poll) {
47
+ document.cookie = "sf_poll_interval=" + poll.value + ";path=/;max-age=31536000;samesite=lax";
48
+ Turbo.visit(window.location.href, { action: "replace" });
49
+ return;
50
+ }
51
+ var el = e.target.closest("[data-autosubmit]");
52
+ if (el && el.form) {
53
+ // A checkbox paired with a same-name hidden field (the "unchecked
54
+ // submits 0" trick) would otherwise put `name=0&name=1` in the query
55
+ // string when checked. Disable the hidden twin while checked so the URL
56
+ // carries a single value.
57
+ if (el.type === "checkbox") {
58
+ el.form.querySelectorAll('input[type="hidden"][name="' + el.name + '"]').forEach(function (h) {
59
+ h.disabled = el.checked;
60
+ });
61
+ }
62
+ el.form.requestSubmit();
63
+ }
64
+ });
65
+
66
+ // Confirm destructive actions: any form with data-confirm. preventDefault
67
+ // stops both the native submit and Turbo's.
68
+ document.addEventListener("submit", function (e) {
69
+ var form = e.target.closest("form[data-confirm]");
70
+ if (form && !window.confirm(form.getAttribute("data-confirm"))) e.preventDefault();
71
+ });
72
+
73
+ // Leave the filter inputs untouched during the polling morph refresh. Skipping
74
+ // the element (rather than letting idiomorph reconcile it) keeps it in place,
75
+ // so a value the user is typing, its caret, and focus all survive a tick
76
+ // instead of being reset to the last-submitted server value.
77
+ document.addEventListener("turbo:before-morph-element", function (e) {
78
+ if (e.target.hasAttribute && e.target.hasAttribute("data-sf-poll-preserve")) e.preventDefault();
79
+ });
80
+
81
+ // Runs on the initial load and after every Turbo visit render.
82
+ document.addEventListener("turbo:load", setupPage);
83
+ }
84
+
85
+ // Per-page setup: (re)arm the flash toasts and the polling timer for whatever
86
+ // page Turbo just rendered.
87
+ function setupPage() {
88
+ // Auto-dismiss floating flash toasts after a few seconds (fade, then remove).
89
+ document.querySelectorAll("[data-flash]").forEach(function (el, i) {
90
+ setTimeout(function () {
91
+ el.classList.add("opacity-0");
92
+ setTimeout(function () { el.remove(); }, 300);
93
+ }, 4000 + i * 150);
94
+ });
95
+
96
+ // Polling refresh of the list/stats region. Keep a single timer; clear any
97
+ // previous one so a navigation doesn't leave two running. Gate on the
98
+ // [data-poll-region] attribute, not the #sf-poll-region id: the id is always
99
+ // on <main> (it's the morph target), but the attribute is present only on
100
+ // pages that opt into polling; the definition graph opts out so its live
101
+ // Cytoscape canvas isn't morphed away.
102
+ if (window.__sagaForgePoll) clearInterval(window.__sagaForgePoll);
103
+ var body = document.body, interval = parseInt(body.getAttribute("data-poll-interval") || "0", 10) * 1000;
104
+ if (interval > 0 && document.querySelector("[data-poll-region]") && !body.hasAttribute("data-poll-paused")) {
105
+ window.__sagaForgePoll = setInterval(pollTick, interval);
106
+ }
107
+ }
108
+
109
+ // One polling tick: fetch the current page and morph the list/stats region.
110
+ //
111
+ // The refresh is a Turbo morph stream, not an innerHTML swap: idiomorph mutates
112
+ // the existing nodes in place instead of recreating them, so horizontal scroll,
113
+ // focus, caret, and in-progress filter text all survive the update for free,
114
+ // no manual preservation. `update` with method="morph" morphs the region's
115
+ // contents while leaving the <main id> wrapper (and its data-poll-region hook)
116
+ // untouched. Scoped to the region (not a whole-page refresh) so the header and
117
+ // flash toasts are left alone.
118
+ function pollTick() {
119
+ if (!document.querySelector("[data-poll-region]")) return;
120
+ fetch(window.location.href, { headers: { "X-Requested-With": "XMLHttpRequest" } })
121
+ .then(function (r) { return r.text(); })
122
+ .then(function (html) {
123
+ var doc = new DOMParser().parseFromString(html, "text/html");
124
+ var fresh = doc.getElementById("sf-poll-region");
125
+ if (!fresh || !window.Turbo) return;
126
+ Turbo.renderStreamMessage(
127
+ '<turbo-stream action="update" method="morph" target="sf-poll-region">' +
128
+ "<template>" + fresh.innerHTML + "</template></turbo-stream>"
129
+ );
130
+ }).catch(function () {});
131
+ }
132
+ })();
@@ -0,0 +1,143 @@
1
+ // Renders a saga's state-machine graph (Definition#to_graph: start/state/
2
+ // terminal nodes, chain/jump edges) with Cytoscape + dagre, optionally
3
+ // overlaid with one saga instance's status. Reads the graph as JSON from
4
+ // #sf-graph[data-graph] (structured elements, no text grammar) and paints
5
+ // kind (node shape) + overlay status (fill/border) + edge kind (chain solid /
6
+ // jump dashed). Interactive: pan/zoom, and tapping a
7
+ // node/edge shows its details and highlights its neighborhood.
8
+ (function () {
9
+ var el = document.getElementById("sf-graph");
10
+ if (!el || typeof cytoscape === "undefined") return;
11
+ if (window.cytoscapeDagre) {
12
+ try { cytoscape.use(window.cytoscapeDagre); } catch (e) { /* already registered */ }
13
+ }
14
+
15
+ var graph;
16
+ try { graph = JSON.parse(el.getAttribute("data-graph")); } catch (e) { return; }
17
+
18
+ // Escape before writing into innerHTML: node labels are state names and
19
+ // edge labels are event names (Ruby symbols/strings from the saga's
20
+ // declared DSL), harmless in practice, but tap details render them as
21
+ // HTML, so escape defensively regardless.
22
+ var ESC = {"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"};
23
+ function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return ESC[c]; }); }
24
+
25
+ // Node shape per graph-node kind (a legend in the view maps these back to
26
+ // names).
27
+ var SHAPE = {start: "ellipse", state: "round-rectangle", terminal: "round-tag"};
28
+
29
+ // [fill, border, text] per overlay status. none is muted so the states an
30
+ // instance hasn't touched recede behind the ones it has.
31
+ var COLOR = {
32
+ active: ["#eef0fc", "#4650c8", "#31358f"],
33
+ processed: ["#ecfdf5", "#10b981", "#065f46"],
34
+ stalled: ["#fffbeb", "#f59e0b", "#9a3412"],
35
+ failed: ["#fff1f2", "#f43f5e", "#9f1239"],
36
+ none: ["#fafafa", "#d4d4d8", "#52525b"]
37
+ };
38
+
39
+ graph.nodes.forEach(function (n) { n.data.display = n.data.label || n.data.id; });
40
+
41
+ var style = [
42
+ {
43
+ selector: "node",
44
+ style: {
45
+ "label": "data(display)", "text-wrap": "wrap", "text-max-width": "150px",
46
+ "text-valign": "center", "text-halign": "center", "text-justification": "center",
47
+ "font-family": "ui-sans-serif, system-ui, sans-serif", "font-size": "11px",
48
+ "line-height": 1.35, "width": "150px", "height": "label",
49
+ "padding": "12px", "shape": "round-rectangle", "corner-radius": "10px",
50
+ "border-width": 1.5, "background-color": "#fff", "border-color": "#e4e4e7",
51
+ "color": "#3f3f46", "text-outline-width": 0
52
+ }
53
+ },
54
+ {selector: "edge", style: {
55
+ // Label sits near the TARGET (not mid-edge), so several edges leaving
56
+ // the same node don't stack their labels on top of each other.
57
+ "target-label": "data(label)", "target-text-offset": "42px",
58
+ "font-size": "9px", "font-family": "ui-sans-serif, system-ui, sans-serif",
59
+ "color": "#64748b", "curve-style": "bezier",
60
+ "width": 1.5, "line-color": "#cbd5e1", "target-arrow-shape": "triangle",
61
+ "target-arrow-color": "#94a3b8", "arrow-scale": 0.9,
62
+ "text-background-color": "#fff", "text-background-opacity": 1,
63
+ "text-background-padding": "3px", "text-background-shape": "round-rectangle",
64
+ "text-border-color": "#e2e8f0", "text-border-width": 1, "text-border-opacity": 1,
65
+ "text-max-width": "120px", "text-wrap": "ellipsis", "text-events": "yes"
66
+ }},
67
+ {selector: "edge.kind-chain", style: {
68
+ "curve-style": "taxi", "taxi-direction": "downward", "taxi-turn": "40%", "taxi-turn-min-distance": "8px"
69
+ }},
70
+ {selector: "edge.kind-jump", style: {
71
+ "line-style": "dashed", "line-color": "#a78bfa", "target-arrow-color": "#8b5cf6", "color": "#7c3aed"
72
+ }},
73
+ {selector: "node.dim", style: {"opacity": 0.2}},
74
+ {selector: "edge.dim", style: {"opacity": 0.12}},
75
+ {selector: "node.focus", style: {"border-width": 3}},
76
+ {selector: "node.hover", style: {"border-width": 2.5}}
77
+ ];
78
+ Object.keys(SHAPE).forEach(function (k) {
79
+ style.push({selector: "node.kind-" + k, style: {"shape": SHAPE[k]}});
80
+ });
81
+ Object.keys(COLOR).forEach(function (s) {
82
+ style.push({selector: "node.status-" + s, style: {
83
+ "background-color": COLOR[s][0], "border-color": COLOR[s][1], "color": COLOR[s][2]
84
+ }});
85
+ });
86
+ // start is the machine's single entry point, visually distinct, like an
87
+ // endpoint marker rather than a state.
88
+ style.push({selector: "node.kind-start", style: {
89
+ "background-color": "#18181b", "border-color": "#18181b", "color": "#fff",
90
+ "width": "label", "font-size": "10px", "font-weight": "600", "padding": "8px"
91
+ }});
92
+ style.push({selector: "node.kind-terminal", style: {"font-weight": "600"}});
93
+
94
+ var cy = cytoscape({
95
+ container: el,
96
+ elements: graph,
97
+ style: style,
98
+ layout: {
99
+ name: "dagre", rankDir: "TB", nodeSep: 42, rankSep: 70, edgeSep: 18,
100
+ ranker: "network-simplex", padding: 30
101
+ },
102
+ minZoom: 0.25, maxZoom: 2.5, wheelSensitivity: 0.25, autoungrabify: true
103
+ });
104
+ cy.ready(function () { cy.fit(cy.elements(), 36); });
105
+
106
+ var detail = document.getElementById("sf-graph-detail");
107
+ function hint() {
108
+ if (detail) detail.innerHTML = '<span class="text-zinc-400">Tap a node or edge to inspect it. Scroll to zoom, drag to pan.</span>';
109
+ }
110
+ function clearFocus() { cy.elements().removeClass("dim focus"); hint(); }
111
+
112
+ cy.on("mouseover", "node", function (e) { e.target.addClass("hover"); });
113
+ cy.on("mouseout", "node", function (e) { e.target.removeClass("hover"); });
114
+
115
+ cy.on("tap", "node", function (evt) {
116
+ var n = evt.target, hood = n.closedNeighborhood();
117
+ cy.elements().addClass("dim");
118
+ hood.removeClass("dim");
119
+ cy.nodes().removeClass("focus");
120
+ n.addClass("focus");
121
+ if (detail) {
122
+ var d = n.data(), cls = n.classes() || [];
123
+ var kind = (cls.join(" ").match(/kind-(\w+)/) || [])[1] || "";
124
+ var status = (cls.join(" ").match(/status-(\w+)/) || [])[1] || "";
125
+ detail.innerHTML =
126
+ '<div class="font-medium text-zinc-800">' + esc(d.label || d.id) + "</div>" +
127
+ '<div class="text-zinc-500">' + esc(kind) +
128
+ (status && status !== "none" ? ' &middot; <span class="font-medium">' + esc(status) + "</span>" : "") + "</div>";
129
+ }
130
+ });
131
+ cy.on("tap", "edge", function (evt) {
132
+ var e = evt.target, cls = e.classes() || [];
133
+ var kind = (cls.join(" ").match(/kind-(\w+)/) || [])[1] || "";
134
+ var label = e.data("label");
135
+ if (detail) {
136
+ detail.innerHTML =
137
+ '<div class="text-zinc-500">' + esc(kind) + " edge</div>" +
138
+ (label ? '<div class="mt-0.5 font-mono text-[11px] text-zinc-700">' + esc(label) + "</div>" : "");
139
+ }
140
+ });
141
+ cy.on("tap", function (evt) { if (evt.target === cy) clearFocus(); });
142
+ hint();
143
+ })();
@@ -0,0 +1,299 @@
1
+ @import "tailwindcss";
2
+
3
+ /*
4
+ * SagaForge dashboard styles. Source for the compiled dashboard.css.
5
+ * Rebuild after editing this file or any view: `bundle exec rake tailwind:build`.
6
+ *
7
+ * Design rule: color means saga/event state; all other chrome is neutral,
8
+ * themed via CSS variables (see the --sf-* custom properties below) so a
9
+ * single build supports both light and dark without a Tailwind `dark:`
10
+ * variant. System fonts only (no external fonts, CSP-friendly).
11
+ */
12
+
13
+ @theme {
14
+ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
15
+ --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
16
+ }
17
+
18
+ /*
19
+ * Theme variables. Light values live on :root; a `prefers-color-scheme: dark`
20
+ * media query supplies the system-dark defaults, and the explicit
21
+ * `[data-theme]` attribute selectors let a future manual toggle override
22
+ * either direction, mirroring the marketing site's index.html so the same
23
+ * indigo accent (#4650c8 light / #97a0f5 dark) reads consistently everywhere.
24
+ */
25
+ :root {
26
+ --sf-bg: #fafafa;
27
+ --sf-surface: #ffffff;
28
+ --sf-fg: #18181b;
29
+ --sf-soft: #71717a;
30
+ --sf-border: #e4e4e7;
31
+
32
+ --sf-accent: #4650c8;
33
+ --sf-accent-fg: #ffffff;
34
+
35
+ --sf-badge-indigo-bg: #eef0fc;
36
+ --sf-badge-indigo-fg: #4650c8;
37
+ --sf-badge-indigo-border: #c7cbf0;
38
+
39
+ --sf-badge-green-bg: #ecfdf5;
40
+ --sf-badge-green-fg: #047857;
41
+ --sf-badge-green-border: #a7f3d0;
42
+
43
+ --sf-badge-amber-bg: #fffbeb;
44
+ --sf-badge-amber-fg: #b45309;
45
+ --sf-badge-amber-border: #fde68a;
46
+
47
+ --sf-badge-red-bg: #fff1f2;
48
+ --sf-badge-red-fg: #be123c;
49
+ --sf-badge-red-border: #fecdd3;
50
+
51
+ --sf-badge-slate-bg: #f4f4f5;
52
+ --sf-badge-slate-fg: #52525b;
53
+ --sf-badge-slate-border: #e4e4e7;
54
+
55
+ --sf-flash-ok-bg: #ecfdf5;
56
+ --sf-flash-ok-fg: #065f46;
57
+ --sf-flash-ok-border: #a7f3d0;
58
+
59
+ --sf-flash-err-bg: #fff1f2;
60
+ --sf-flash-err-fg: #9f1239;
61
+ --sf-flash-err-border: #fecdd3;
62
+ }
63
+
64
+ @media (prefers-color-scheme: dark) {
65
+ :root {
66
+ --sf-bg: #101317;
67
+ --sf-surface: #16191f;
68
+ --sf-fg: #e4e4e7;
69
+ --sf-soft: #9a9aa2;
70
+ --sf-border: #2a2e35;
71
+
72
+ --sf-accent: #97a0f5;
73
+ --sf-accent-fg: #14162b;
74
+
75
+ --sf-badge-indigo-bg: rgba(151, 160, 245, 0.12);
76
+ --sf-badge-indigo-fg: #b7bdf7;
77
+ --sf-badge-indigo-border: rgba(151, 160, 245, 0.3);
78
+
79
+ --sf-badge-green-bg: rgba(16, 185, 129, 0.12);
80
+ --sf-badge-green-fg: #6ee7b7;
81
+ --sf-badge-green-border: rgba(16, 185, 129, 0.3);
82
+
83
+ --sf-badge-amber-bg: rgba(245, 158, 11, 0.12);
84
+ --sf-badge-amber-fg: #fcd34d;
85
+ --sf-badge-amber-border: rgba(245, 158, 11, 0.3);
86
+
87
+ --sf-badge-red-bg: rgba(244, 63, 94, 0.12);
88
+ --sf-badge-red-fg: #fda4af;
89
+ --sf-badge-red-border: rgba(244, 63, 94, 0.3);
90
+
91
+ --sf-badge-slate-bg: rgba(161, 161, 170, 0.12);
92
+ --sf-badge-slate-fg: #d4d4d8;
93
+ --sf-badge-slate-border: rgba(161, 161, 170, 0.3);
94
+
95
+ --sf-flash-ok-bg: rgba(16, 185, 129, 0.12);
96
+ --sf-flash-ok-fg: #6ee7b7;
97
+ --sf-flash-ok-border: rgba(16, 185, 129, 0.3);
98
+
99
+ --sf-flash-err-bg: rgba(244, 63, 94, 0.12);
100
+ --sf-flash-err-fg: #fda4af;
101
+ --sf-flash-err-border: rgba(244, 63, 94, 0.3);
102
+ }
103
+ }
104
+
105
+ :root[data-theme="light"] {
106
+ --sf-bg: #fafafa;
107
+ --sf-surface: #ffffff;
108
+ --sf-fg: #18181b;
109
+ --sf-soft: #71717a;
110
+ --sf-border: #e4e4e7;
111
+ --sf-accent: #4650c8;
112
+ --sf-accent-fg: #ffffff;
113
+ --sf-badge-indigo-bg: #eef0fc;
114
+ --sf-badge-indigo-fg: #4650c8;
115
+ --sf-badge-indigo-border: #c7cbf0;
116
+ --sf-badge-green-bg: #ecfdf5;
117
+ --sf-badge-green-fg: #047857;
118
+ --sf-badge-green-border: #a7f3d0;
119
+ --sf-badge-amber-bg: #fffbeb;
120
+ --sf-badge-amber-fg: #b45309;
121
+ --sf-badge-amber-border: #fde68a;
122
+ --sf-badge-red-bg: #fff1f2;
123
+ --sf-badge-red-fg: #be123c;
124
+ --sf-badge-red-border: #fecdd3;
125
+ --sf-badge-slate-bg: #f4f4f5;
126
+ --sf-badge-slate-fg: #52525b;
127
+ --sf-badge-slate-border: #e4e4e7;
128
+ --sf-flash-ok-bg: #ecfdf5;
129
+ --sf-flash-ok-fg: #065f46;
130
+ --sf-flash-ok-border: #a7f3d0;
131
+ --sf-flash-err-bg: #fff1f2;
132
+ --sf-flash-err-fg: #9f1239;
133
+ --sf-flash-err-border: #fecdd3;
134
+ }
135
+
136
+ :root[data-theme="dark"] {
137
+ --sf-bg: #101317;
138
+ --sf-surface: #16191f;
139
+ --sf-fg: #e4e4e7;
140
+ --sf-soft: #9a9aa2;
141
+ --sf-border: #2a2e35;
142
+ --sf-accent: #97a0f5;
143
+ --sf-accent-fg: #14162b;
144
+ --sf-badge-indigo-bg: rgba(151, 160, 245, 0.12);
145
+ --sf-badge-indigo-fg: #b7bdf7;
146
+ --sf-badge-indigo-border: rgba(151, 160, 245, 0.3);
147
+ --sf-badge-green-bg: rgba(16, 185, 129, 0.12);
148
+ --sf-badge-green-fg: #6ee7b7;
149
+ --sf-badge-green-border: rgba(16, 185, 129, 0.3);
150
+ --sf-badge-amber-bg: rgba(245, 158, 11, 0.12);
151
+ --sf-badge-amber-fg: #fcd34d;
152
+ --sf-badge-amber-border: rgba(245, 158, 11, 0.3);
153
+ --sf-badge-red-bg: rgba(244, 63, 94, 0.12);
154
+ --sf-badge-red-fg: #fda4af;
155
+ --sf-badge-red-border: rgba(244, 63, 94, 0.3);
156
+ --sf-badge-slate-bg: rgba(161, 161, 170, 0.12);
157
+ --sf-badge-slate-fg: #d4d4d8;
158
+ --sf-badge-slate-border: rgba(161, 161, 170, 0.3);
159
+ --sf-flash-ok-bg: rgba(16, 185, 129, 0.12);
160
+ --sf-flash-ok-fg: #6ee7b7;
161
+ --sf-flash-ok-border: rgba(16, 185, 129, 0.3);
162
+ --sf-flash-err-bg: rgba(244, 63, 94, 0.12);
163
+ --sf-flash-err-fg: #fda4af;
164
+ --sf-flash-err-border: rgba(244, 63, 94, 0.3);
165
+ }
166
+
167
+ @layer components {
168
+ /* State/status badges (the one place color is allowed). Colors come from
169
+ the --sf-badge-*-{bg,fg,border} variables above so they adapt with the
170
+ theme without needing a separate dark ruleset per color. */
171
+ .sf-badge { @apply inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 font-mono text-xs font-medium; }
172
+ .sf-badge-indigo { background-color: var(--sf-badge-indigo-bg); color: var(--sf-badge-indigo-fg); border-color: var(--sf-badge-indigo-border); }
173
+ .sf-badge-green { background-color: var(--sf-badge-green-bg); color: var(--sf-badge-green-fg); border-color: var(--sf-badge-green-border); }
174
+ .sf-badge-amber { background-color: var(--sf-badge-amber-bg); color: var(--sf-badge-amber-fg); border-color: var(--sf-badge-amber-border); }
175
+ .sf-badge-red { background-color: var(--sf-badge-red-bg); color: var(--sf-badge-red-fg); border-color: var(--sf-badge-red-border); }
176
+ .sf-badge-slate { background-color: var(--sf-badge-slate-bg); color: var(--sf-badge-slate-fg); border-color: var(--sf-badge-slate-border); }
177
+
178
+ /* Buttons: ink chrome by default; -primary uses the indigo accent. */
179
+ .sf-btn { @apply inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium shadow-sm transition focus:outline-none focus-visible:ring-2; border-color: var(--sf-border); background-color: var(--sf-surface); color: var(--sf-fg); }
180
+ .sf-btn:hover { background-color: var(--sf-bg); }
181
+ .sf-btn-primary { border-color: var(--sf-accent); background-color: var(--sf-accent); color: var(--sf-accent-fg); }
182
+ .sf-btn-primary:hover { opacity: 0.9; }
183
+ .sf-btn-danger { @apply border-rose-300 text-rose-700; }
184
+ .sf-btn-danger:hover { @apply bg-rose-50; }
185
+
186
+ /* Surfaces */
187
+ .sf-card { @apply rounded-lg border shadow-sm; border-color: var(--sf-border); background-color: var(--sf-surface); }
188
+
189
+ /* Proportional meters (analytics bars, duration bars, etc). Widths are
190
+ pre-generated 1%-step classes so no inline style is ever needed (keeps a
191
+ strict style-src CSP intact); see .sf-bar-{0..100} below. */
192
+ .sf-bar { @apply h-full block; }
193
+ }
194
+
195
+ /* Width steps at 1% granularity so bar_width_class(pct) can render any
196
+ integer 0..100 without an inline style. */
197
+ @layer components {
198
+ .sf-bar-0 { width: 0%; }
199
+ .sf-bar-1 { width: 1%; }
200
+ .sf-bar-2 { width: 2%; }
201
+ .sf-bar-3 { width: 3%; }
202
+ .sf-bar-4 { width: 4%; }
203
+ .sf-bar-5 { width: 5%; }
204
+ .sf-bar-6 { width: 6%; }
205
+ .sf-bar-7 { width: 7%; }
206
+ .sf-bar-8 { width: 8%; }
207
+ .sf-bar-9 { width: 9%; }
208
+ .sf-bar-10 { width: 10%; }
209
+ .sf-bar-11 { width: 11%; }
210
+ .sf-bar-12 { width: 12%; }
211
+ .sf-bar-13 { width: 13%; }
212
+ .sf-bar-14 { width: 14%; }
213
+ .sf-bar-15 { width: 15%; }
214
+ .sf-bar-16 { width: 16%; }
215
+ .sf-bar-17 { width: 17%; }
216
+ .sf-bar-18 { width: 18%; }
217
+ .sf-bar-19 { width: 19%; }
218
+ .sf-bar-20 { width: 20%; }
219
+ .sf-bar-21 { width: 21%; }
220
+ .sf-bar-22 { width: 22%; }
221
+ .sf-bar-23 { width: 23%; }
222
+ .sf-bar-24 { width: 24%; }
223
+ .sf-bar-25 { width: 25%; }
224
+ .sf-bar-26 { width: 26%; }
225
+ .sf-bar-27 { width: 27%; }
226
+ .sf-bar-28 { width: 28%; }
227
+ .sf-bar-29 { width: 29%; }
228
+ .sf-bar-30 { width: 30%; }
229
+ .sf-bar-31 { width: 31%; }
230
+ .sf-bar-32 { width: 32%; }
231
+ .sf-bar-33 { width: 33%; }
232
+ .sf-bar-34 { width: 34%; }
233
+ .sf-bar-35 { width: 35%; }
234
+ .sf-bar-36 { width: 36%; }
235
+ .sf-bar-37 { width: 37%; }
236
+ .sf-bar-38 { width: 38%; }
237
+ .sf-bar-39 { width: 39%; }
238
+ .sf-bar-40 { width: 40%; }
239
+ .sf-bar-41 { width: 41%; }
240
+ .sf-bar-42 { width: 42%; }
241
+ .sf-bar-43 { width: 43%; }
242
+ .sf-bar-44 { width: 44%; }
243
+ .sf-bar-45 { width: 45%; }
244
+ .sf-bar-46 { width: 46%; }
245
+ .sf-bar-47 { width: 47%; }
246
+ .sf-bar-48 { width: 48%; }
247
+ .sf-bar-49 { width: 49%; }
248
+ .sf-bar-50 { width: 50%; }
249
+ .sf-bar-51 { width: 51%; }
250
+ .sf-bar-52 { width: 52%; }
251
+ .sf-bar-53 { width: 53%; }
252
+ .sf-bar-54 { width: 54%; }
253
+ .sf-bar-55 { width: 55%; }
254
+ .sf-bar-56 { width: 56%; }
255
+ .sf-bar-57 { width: 57%; }
256
+ .sf-bar-58 { width: 58%; }
257
+ .sf-bar-59 { width: 59%; }
258
+ .sf-bar-60 { width: 60%; }
259
+ .sf-bar-61 { width: 61%; }
260
+ .sf-bar-62 { width: 62%; }
261
+ .sf-bar-63 { width: 63%; }
262
+ .sf-bar-64 { width: 64%; }
263
+ .sf-bar-65 { width: 65%; }
264
+ .sf-bar-66 { width: 66%; }
265
+ .sf-bar-67 { width: 67%; }
266
+ .sf-bar-68 { width: 68%; }
267
+ .sf-bar-69 { width: 69%; }
268
+ .sf-bar-70 { width: 70%; }
269
+ .sf-bar-71 { width: 71%; }
270
+ .sf-bar-72 { width: 72%; }
271
+ .sf-bar-73 { width: 73%; }
272
+ .sf-bar-74 { width: 74%; }
273
+ .sf-bar-75 { width: 75%; }
274
+ .sf-bar-76 { width: 76%; }
275
+ .sf-bar-77 { width: 77%; }
276
+ .sf-bar-78 { width: 78%; }
277
+ .sf-bar-79 { width: 79%; }
278
+ .sf-bar-80 { width: 80%; }
279
+ .sf-bar-81 { width: 81%; }
280
+ .sf-bar-82 { width: 82%; }
281
+ .sf-bar-83 { width: 83%; }
282
+ .sf-bar-84 { width: 84%; }
283
+ .sf-bar-85 { width: 85%; }
284
+ .sf-bar-86 { width: 86%; }
285
+ .sf-bar-87 { width: 87%; }
286
+ .sf-bar-88 { width: 88%; }
287
+ .sf-bar-89 { width: 89%; }
288
+ .sf-bar-90 { width: 90%; }
289
+ .sf-bar-91 { width: 91%; }
290
+ .sf-bar-92 { width: 92%; }
291
+ .sf-bar-93 { width: 93%; }
292
+ .sf-bar-94 { width: 94%; }
293
+ .sf-bar-95 { width: 95%; }
294
+ .sf-bar-96 { width: 96%; }
295
+ .sf-bar-97 { width: 97%; }
296
+ .sf-bar-98 { width: 98%; }
297
+ .sf-bar-99 { width: 99%; }
298
+ .sf-bar-100 { width: 100%; }
299
+ }