dispatch_policy 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64abf7f210de4186efbafa1f50248534304e17d1c303236778a19f48dd869f3e
4
- data.tar.gz: 02c422a48fa69385a1ff7c5855a05726ec1889d8efe72a51e4cc510c4b6e26cd
3
+ metadata.gz: 24ab8c2fe85abc57507f84edc955c8263f59a96505522ecd9ceb6ce60e14bcba
4
+ data.tar.gz: 152dc560f5b1169d5ef6f4a27065ae629da426ceefb0725fa6bc7a8d13c62a3f
5
5
  SHA512:
6
- metadata.gz: 193a857e02025e3cc4b5a829d83a1f4d0ed89f22d4f4e04040e3a006459cf09714ffc270b3a7b622bdf96d921c1c6bda888fe4a1c3da5d3c0221fb86d79078ec
7
- data.tar.gz: 2acfb5fa96b23d94a4845643c2a2d2ea3e1c3e45cfae7e2fe522ad568f2d0dcce15e70d9abee58ed96bbdc709b91e7aa15df3ae539ebfa7c4d064efc553a2d07
6
+ metadata.gz: 88f5adb73f3e7bb1893eab32e098a5dda1d7b147c0cd000ee35eb6771b2d2c21f0fe1541c16725b537a1e3a1121166b2681331e7fcea79de26e3d3926137bea0
7
+ data.tar.gz: c2b17c50ccd765c95bb6d316a68f3e7dece5134ae9ca26fcb918356e0baf611ebbfba19f695e41b1b7402f0021f0b9d3d08111c7938448efb9fb5accda0a4131
data/CHANGELOG.md ADDED
@@ -0,0 +1,113 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ ### Added
6
+ - TX-atomic admission: the DELETE on `staged_jobs`, the pre-INSERT
7
+ in `inflight_jobs` and the adapter handoff (`good_job` /
8
+ `solid_queue`) all run inside the same transaction, so any failure
9
+ rolls everything back with no loss window between admission COMMIT
10
+ and adapter enqueue.
11
+ - `:adaptive_concurrency` gate that auto-tunes per-partition
12
+ `current_max` via AIMD against an EWMA of `queue_lag` (time from
13
+ admission to perform start), with a safety valve that floors
14
+ `remaining` at `initial_max` when `in_flight == 0` so idle
15
+ partitions can recover after a shrink.
16
+ - In-tick fairness layer: claimed partitions are reordered by
17
+ `decayed_admits` (EWMA, default `half_life = 60s`) and capped by
18
+ `fair_share = ceil(tick_cap / N)`. Composes with
19
+ `:adaptive_concurrency` — fairness writes
20
+ `partitions.decayed_admits`, adaptive writes
21
+ `dispatch_policy_adaptive_concurrency_stats.current_max`, no
22
+ shared locks.
23
+ - `shard_by` to split a policy's partitions across parallel tick
24
+ loops; the shard is pinned on first write so partitions don't jump
25
+ between tick workers.
26
+ - Policy-level `partition_by`: a single canonical scope shared by
27
+ the staged job's `partition_key` and the concurrency gate's
28
+ `inflight_partition_key`, so no gate suffers scope dilution.
29
+ - Gates are no longer required — a policy with `partition_by` and
30
+ no gates is valid and still benefits from in-tick fairness.
31
+ - `dispatch_policy_inflight_jobs` is populated for every admitted
32
+ job (not only concurrency-gated ones), with a heartbeat thread
33
+ refreshing `heartbeat_at` during perform.
34
+ - Bulk handoff via `ActiveJob.perform_all_later` and bulk-flush of
35
+ deny-path partition state at the end of a tick (single
36
+ `UPDATE…FROM(VALUES…)` instead of N per-partition statements).
37
+ - Per-tick metrics layer (`dispatch_policy_tick_samples`) feeding
38
+ the admin UI: throughput, P50/P95 round-trip ages, capacity
39
+ headroom, pending trend, fail %, and operator hints.
40
+ - Admin UI improvements: cursor-based pagination of `/partitions`,
41
+ sort + only-pending filter, auto-refresh control (off / 2s / 5s /
42
+ 10s) via Turbo Drive, per-partition and per-policy Drain action,
43
+ redesigned dummy demo page with cards + storm controls.
44
+ - `config.enabled` master switch for cutovers.
45
+ - `TickLoop` `busy_pause` to throttle busy iterations.
46
+ - `bin/release` wrapper around `rake release`.
47
+ - Manual benchmark suite, plus a real-adapter end-to-end bench
48
+ covering `good_job` and `solid_queue`.
49
+
50
+ ### Changed
51
+ - **Breaking:** `partition_by` is policy-level only. Per-gate
52
+ `partition_by:` was removed; if omitted, `Policy#validate!` raises
53
+ `InvalidPolicy: partition_by required`. For different per-gate
54
+ scopes, use separate policies.
55
+ - `partitions.context` is refreshed on every `perform_later` via
56
+ UPSERT, so changes in the host DB take effect on the next enqueue
57
+ without redeploys. Gates read this ctx, not the historical
58
+ `staged_jobs.context`.
59
+ - Tick-claim ordering kept at `last_checked_at NULLS FIRST, id`
60
+ (anti-stagnation): each partition with pending is processed every
61
+ ⌈N/B⌉ ticks. Fairness reorder happens after the claim, in memory.
62
+ - Non-PG adapters now warn at boot (`warn_unsupported_adapter`)
63
+ instead of hard-failing — a custom PG-backed adapter still works.
64
+ - `config.database_role` lets the admission TX target a specific
65
+ Rails multi-DB role (e.g. `solid_queue` on a separate DB).
66
+
67
+ ### Fixed
68
+ - `BulkEnqueue.perform_all_later` checks `Bypass.active?` and
69
+ delegates to `super` when active, breaking an infinite re-staging
70
+ loop on the deserialize + `perform_all_later` path under Bypass.
71
+ - `JobExtension.ensure_arguments_materialized!` is called before
72
+ reading `job.arguments` in both single and bulk paths — previously
73
+ the public `arguments` getter returned `[]` for deserialised jobs
74
+ until `perform_now` triggered private materialization, so the
75
+ context proc fell back to its defaults.
76
+ - `:adaptive_concurrency` updates `current_max` in a single SQL
77
+ statement that uses the post-update `ewma_latency_ms` value in
78
+ its CASE expression, removing read-modify-write races between
79
+ concurrent workers.
80
+ - Adaptive's feedback signal is measured in `InflightTracker.track`
81
+ before `block.call` so perform duration doesn't pollute the
82
+ `queue_lag` signal.
83
+ - Heartbeat thread refreshes `inflight_jobs.heartbeat_at` during
84
+ perform so long-running jobs aren't reaped as stale.
85
+ - Deny-only ticks persist `next_eligible_at`.
86
+ - Tick samples query no longer depends on `date_bin` (works on
87
+ Postgres 13).
88
+ - Admin UI preserves scroll position on auto-refresh, and skips
89
+ auto-refresh while a Turbo visit is in flight.
90
+ - P95/P50 round-trip ages were inverted in the metrics view.
91
+ - Railtie no longer auto-merges the gem's `db/migrate` into the
92
+ host's paths.
93
+ - "Pending is growing" hint silenced when the backlog has drained.
94
+
95
+ ### Removed
96
+ - Per-gate `partition_by:` declarations (see Changed).
97
+ - Denormalised `partitions.in_flight_count` counter — `inflight_jobs`
98
+ is the source of truth.
99
+ - `unclaim!` / `preinserted_inflight_ids` — TX rollback covers the
100
+ failure case.
101
+
102
+ ## 0.1.0
103
+
104
+ Initial release.
105
+
106
+ - Rails engine + ActiveJob integration intercepting `perform_later`
107
+ via `JobExtension`.
108
+ - Gates: `:throttle`, `:concurrency`.
109
+ - Staged jobs admitted by a periodic tick loop, per-partition
110
+ counters, and token-bucket throttle state.
111
+ - Admin UI showing partitions, pending counts, and recent ticks.
112
+ - PostgreSQL required (uses `FOR UPDATE SKIP LOCKED`, `ON CONFLICT`,
113
+ and `jsonb`).
data/README.md CHANGED
@@ -1,12 +1,15 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ceritium/dispatch_policy/master/arts/logo-lockup-dark.svg">
4
+ <img src="https://raw.githubusercontent.com/ceritium/dispatch_policy/master/arts/logo-lockup.svg" alt="dispatch_policy" width="360">
5
+ </picture>
6
+ </p>
7
+
1
8
  # DispatchPolicy
2
9
 
3
- > **⚠️ Experimental v2 branch.** This is the `v2` branch of
4
- > [ceritium/dispatch_policy](https://github.com/ceritium/dispatch_policy)
5
- > an alternative cut: TX-atomic admission, in-tick fairness as a
6
- > layer (not a gate), and a single canonical partition scope per
7
- > policy. API, schema, and defaults can change between any two
8
- > commits. The `master` branch of the same repo is the original
9
- > design and is what the published gem (when one ships) tracks.
10
+ > **Pre-1.0.** Published on RubyGems but the API, schema, and
11
+ > defaults can still shift between minor versions. See `CHANGELOG.md`
12
+ > before upgrading.
10
13
  >
11
14
  > **PostgreSQL only.** Staging, admission, and adaptive stats lean on
12
15
  > `jsonb`, partial indexes, `FOR UPDATE SKIP LOCKED`, `ON CONFLICT`,
@@ -57,14 +60,46 @@ The dummy ships ten purpose-built jobs covering throttle, concurrency,
57
60
  mixed gates, scheduling, retries, stress tests, sharding, fairness, and
58
61
  adaptive concurrency. See `test/dummy/app/jobs/`.
59
62
 
63
+ ## Screenshots
64
+
65
+ The admin UI lives at `/dispatch_policy` once the engine is mounted.
66
+ Live throughput, capacity hints, denial reasons, and per-partition
67
+ sparklines:
68
+
69
+ ![Admin index](https://raw.githubusercontent.com/ceritium/dispatch_policy/master/screenshots/admin-index.png)
70
+
71
+ A policy detail page — totals, EWMA queue-lag chart, throughput
72
+ window, and a searchable list of all partitions:
73
+
74
+ ![Policy detail](https://raw.githubusercontent.com/ceritium/dispatch_policy/master/screenshots/admin-policy-fairness_demo.png)
75
+
76
+ Other per-policy pages:
77
+ [adaptive_demo](https://raw.githubusercontent.com/ceritium/dispatch_policy/master/screenshots/admin-policy-adaptive_demo.png) ·
78
+ [high_throttle](https://raw.githubusercontent.com/ceritium/dispatch_policy/master/screenshots/admin-policy-high_throttle.png) ·
79
+ [high_concurrency](https://raw.githubusercontent.com/ceritium/dispatch_policy/master/screenshots/admin-policy-high_concurrency.png) ·
80
+ [mixed](https://raw.githubusercontent.com/ceritium/dispatch_policy/master/screenshots/admin-policy-mixed.png) ·
81
+ [policies index](https://raw.githubusercontent.com/ceritium/dispatch_policy/master/screenshots/policies-index.png) ·
82
+ [partitions index](https://raw.githubusercontent.com/ceritium/dispatch_policy/master/screenshots/partitions-index.png).
83
+
84
+ Regenerate everything against the dummy app with:
85
+
86
+ ```bash
87
+ bin/screenshots
88
+ ```
89
+
90
+ The script seeds realistic state (ticks admit some, GoodJob drains
91
+ inline, then a few fresh jobs are left pending) and drives Capybara
92
+ with headless Chrome through the admin pages. Stop `bin/dummy
93
+ good_job` (or any running tick loop) first so the seeding isn't
94
+ racing a live worker — Selenium Manager auto-downloads chromedriver,
95
+ you only need Chrome installed locally.
96
+
60
97
  ## Install
61
98
 
62
99
  Add to your `Gemfile`:
63
100
 
64
101
  ```ruby
65
- gem "dispatch_policy",
66
- git: "https://github.com/ceritium/dispatch_policy",
67
- branch: "v2"
102
+ gem "dispatch_policy", "~> 0.3"
68
103
  ```
69
104
 
70
105
  Generate the install bundle (migration + initializer + tick loop job):
@@ -574,22 +609,6 @@ The script:
574
609
  Prerequisites: a configured `~/.gem/credentials` for RubyGems push
575
610
  and `gh auth login` for the GitHub release.
576
611
 
577
- ## Status
578
-
579
- Published on RubyGems. API may still shift between minors until
580
- 1.0. The set of features that ship today:
581
-
582
- - Gates: `:throttle`, `:concurrency`, `:adaptive_concurrency`.
583
- - Fairness: in-tick EWMA reorder + optional `tick_admission_budget`.
584
- - Sharding: `shard_by` + per-shard tick loops.
585
- - Bulk handoff: `ActiveJob.perform_all_later` collapses to one
586
- adapter `INSERT` per tick when admissible.
587
- - Admin UI with capacity hints, pending trend, denial reasons.
588
- - Manual benchmark suite.
589
-
590
- Deferred ideas (with rationale) live in [`IDEAS.md`](IDEAS.md):
591
- `gate :global_cap`, smarter sweeper defaults, `sweep_every_seconds`
592
- instead of `sweep_every_ticks`.
593
612
 
594
613
  ## License
595
614
 
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" fill="none" style="color:#a78bfa">
2
+ <rect x="0" y="0" width="96" height="96" rx="23" fill="#0a0a0a"></rect>
3
+ <g transform="translate(10 6.200000000000003) scale(0.7917 0.8708)">
4
+ <path d="M14 28 L26 48 L14 68" stroke="#ffffff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" opacity="0.4" fill="none"></path>
5
+ <path d="M34 28 L46 48 L34 68" stroke="#ffffff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" opacity="0.7" fill="none"></path>
6
+ <path d="M54 28 L66 48 L54 68" stroke="currentColor" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" fill="none"></path>
7
+ <rect x="78" y="20" width="6" height="56" rx="1" fill="#ffffff"></rect>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" fill="none" style="color:#a78bfa">
2
+ <rect x="0" y="0" width="96" height="96" rx="22" fill="#0a0a0a"></rect>
3
+ <g transform="translate(10 10) scale(0.7917)">
4
+ <path d="M28 24 L52 48 L28 72" stroke="currentColor" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" fill="none"></path>
5
+ <rect x="68" y="18" width="10" height="60" rx="2" fill="#ffffff"></rect>
6
+ </g>
7
+ </svg>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Minified by jsDelivr using Terser v5.37.0.
3
+ * Original file: /npm/@hotwired/turbo@8.0.4/dist/turbo.es2017-umd.js
4
+ *
5
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6
+ */
7
+ /*!
8
+ Turbo 8.0.4
9
+ Copyright © 2024 37signals LLC
10
+ */
11
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Turbo={})}(this,(function(e){"use strict";!function(e){function t(e,t,s){throw new e("Failed to execute 'requestSubmit' on 'HTMLFormElement': "+t+".",s)}"function"!=typeof e.requestSubmit&&(e.requestSubmit=function(e){e?(!function(e,s){e instanceof HTMLElement||t(TypeError,"parameter 1 is not of type 'HTMLElement'"),"submit"==e.type||t(TypeError,"The specified element is not a submit button"),e.form==s||t(DOMException,"The specified element is not owned by this form element","NotFoundError")}(e,this),e.click()):((e=document.createElement("input")).type="submit",e.hidden=!0,this.appendChild(e),e.click(),this.removeChild(e))})}(HTMLFormElement.prototype);const t=new WeakMap;function s(e){const s=function(e){const t=e instanceof Element?e:e instanceof Node?e.parentElement:null,s=t?t.closest("input, button"):null;return"submit"==s?.type?s:null}(e.target);s&&s.form&&t.set(s.form,s)}!function(){if("submitter"in Event.prototype)return;let e=window.Event.prototype;if("SubmitEvent"in window){const t=window.SubmitEvent.prototype;if(!/Apple Computer/.test(navigator.vendor)||"submitter"in t)return;e=t}addEventListener("click",s,!0),Object.defineProperty(e,"submitter",{get(){if("submit"==this.type&&this.target instanceof HTMLFormElement)return t.get(this.target)}})}();const r={eager:"eager",lazy:"lazy"};class i extends HTMLElement{static delegateConstructor=void 0;loaded=Promise.resolve();static get observedAttributes(){return["disabled","loading","src"]}constructor(){super(),this.delegate=new i.delegateConstructor(this)}connectedCallback(){this.delegate.connect()}disconnectedCallback(){this.delegate.disconnect()}reload(){return this.delegate.sourceURLReloaded()}attributeChangedCallback(e){"loading"==e?this.delegate.loadingStyleChanged():"src"==e?this.delegate.sourceURLChanged():"disabled"==e&&this.delegate.disabledChanged()}get src(){return this.getAttribute("src")}set src(e){e?this.setAttribute("src",e):this.removeAttribute("src")}get refresh(){return this.getAttribute("refresh")}set refresh(e){e?this.setAttribute("refresh",e):this.removeAttribute("refresh")}get loading(){return function(e){if("lazy"===e.toLowerCase())return r.lazy;return r.eager}(this.getAttribute("loading")||"")}set loading(e){e?this.setAttribute("loading",e):this.removeAttribute("loading")}get disabled(){return this.hasAttribute("disabled")}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get autoscroll(){return this.hasAttribute("autoscroll")}set autoscroll(e){e?this.setAttribute("autoscroll",""):this.removeAttribute("autoscroll")}get complete(){return!this.delegate.isLoading}get isActive(){return this.ownerDocument===document&&!this.isPreview}get isPreview(){return this.ownerDocument?.documentElement?.hasAttribute("data-turbo-preview")}}function n(e){return new URL(e.toString(),document.baseURI)}function o(e){let t;return e.hash?e.hash.slice(1):(t=e.href.match(/#(.*)$/))?t[1]:void 0}function a(e,t){return n(t?.getAttribute("formaction")||e.getAttribute("action")||e.action)}function l(e){return(function(e){return function(e){return e.pathname.split("/").slice(1)}(e).slice(-1)[0]}(e).match(/\.[^.]*$/)||[])[0]||""}function c(e,t){const s=function(e){return t=e.origin+e.pathname,t.endsWith("/")?t:t+"/";var t}(t);return e.href===n(s).href||e.href.startsWith(s)}function h(e,t){return c(e,t)&&!!l(e).match(/^(?:|\.(?:htm|html|xhtml|php))$/)}function d(e){const t=o(e);return null!=t?e.href.slice(0,-(t.length+1)):e.href}function u(e){return d(e)}class m{constructor(e){this.response=e}get succeeded(){return this.response.ok}get failed(){return!this.succeeded}get clientError(){return this.statusCode>=400&&this.statusCode<=499}get serverError(){return this.statusCode>=500&&this.statusCode<=599}get redirected(){return this.response.redirected}get location(){return n(this.response.url)}get isHTML(){return this.contentType&&this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/)}get statusCode(){return this.response.status}get contentType(){return this.header("Content-Type")}get responseText(){return this.response.clone().text()}get responseHTML(){return this.isHTML?this.response.clone().text():Promise.resolve(void 0)}header(e){return this.response.headers.get(e)}}function p(e){if("false"==e.getAttribute("data-turbo-eval"))return e;{const t=document.createElement("script"),s=M("csp-nonce");return s&&(t.nonce=s),t.textContent=e.textContent,t.async=!1,function(e,t){for(const{name:s,value:r}of t.attributes)e.setAttribute(s,r)}(t,e),t}}function f(e,{target:t,cancelable:s,detail:r}={}){const i=new CustomEvent(e,{cancelable:s,bubbles:!0,composed:!0,detail:r});return t&&t.isConnected?t.dispatchEvent(i):document.documentElement.dispatchEvent(i),i}function g(){return"hidden"===document.visibilityState?v():b()}function b(){return new Promise((e=>requestAnimationFrame((()=>e()))))}function v(){return new Promise((e=>setTimeout((()=>e()),0)))}function S(e=""){return(new DOMParser).parseFromString(e,"text/html")}function w(e,...t){const s=function(e,t){return e.reduce(((e,s,r)=>e+s+(null==t[r]?"":t[r])),"")}(e,t).replace(/^\n/,"").split("\n"),r=s[0].match(/^\s+/),i=r?r[0].length:0;return s.map((e=>e.slice(i))).join("\n")}function E(){return Array.from({length:36}).map(((e,t)=>8==t||13==t||18==t||23==t?"-":14==t?"4":19==t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(15*Math.random()).toString(16))).join("")}function y(e,...t){for(const s of t.map((t=>t?.getAttribute(e))))if("string"==typeof s)return s;return null}function R(...e){for(const t of e)"turbo-frame"==t.localName&&t.setAttribute("busy",""),t.setAttribute("aria-busy","true")}function L(...e){for(const t of e)"turbo-frame"==t.localName&&t.removeAttribute("busy"),t.removeAttribute("aria-busy")}function A(e,t=2e3){return new Promise((s=>{const r=()=>{e.removeEventListener("error",r),e.removeEventListener("load",r),s()};e.addEventListener("load",r,{once:!0}),e.addEventListener("error",r,{once:!0}),setTimeout(s,t)}))}function T(e){switch(e){case"replace":return history.replaceState;case"advance":case"restore":return history.pushState}}function C(...e){const t=y("data-turbo-action",...e);return function(e){return"advance"==e||"replace"==e||"restore"==e}(t)?t:null}function P(e){return document.querySelector(`meta[name="${e}"]`)}function M(e){const t=P(e);return t&&t.content}function k(e,t){if(e instanceof Element)return e.closest(t)||k(e.assignedSlot||e.getRootNode()?.host,t)}function F(e){return!!e&&null==e.closest("[inert], :disabled, [hidden], details:not([open]), dialog:not([open])")&&"function"==typeof e.focus}function I(e){return Array.from(e.querySelectorAll("[autofocus]")).find(F)}function q(e){return n(e.getAttribute("href")||"")}class H extends Set{constructor(e){super(),this.maxSize=e}add(e){if(this.size>=this.maxSize){const e=this.values().next().value;this.delete(e)}super.add(e)}}const B=new H(20),O=window.fetch;function N(e,t={}){const s=new Headers(t.headers||{}),r=E();return B.add(r),s.append("X-Turbo-Request-Id",r),O(e,{...t,headers:s})}function V(e){switch(e.toLowerCase()){case"get":return x.get;case"post":return x.post;case"put":return x.put;case"patch":return x.patch;case"delete":return x.delete}}const x={get:"get",post:"post",put:"put",patch:"patch",delete:"delete"};function D(e){switch(e.toLowerCase()){case W.multipart:return W.multipart;case W.plain:return W.plain;default:return W.urlEncoded}}const W={urlEncoded:"application/x-www-form-urlencoded",multipart:"multipart/form-data",plain:"text/plain"};class U{abortController=new AbortController;#e=e=>{};constructor(e,t,s,r=new URLSearchParams,i=null,o=W.urlEncoded){const[a,l]=$(n(s),t,r,o);this.delegate=e,this.url=a,this.target=i,this.fetchOptions={credentials:"same-origin",redirect:"follow",method:t,headers:{...this.defaultHeaders},body:l,signal:this.abortSignal,referrer:this.delegate.referrer?.href},this.enctype=o}get method(){return this.fetchOptions.method}set method(e){const t=this.isSafe?this.url.searchParams:this.fetchOptions.body||new FormData,s=V(e)||x.get;this.url.search="";const[r,i]=$(this.url,s,t,this.enctype);this.url=r,this.fetchOptions.body=i,this.fetchOptions.method=s}get headers(){return this.fetchOptions.headers}set headers(e){this.fetchOptions.headers=e}get body(){return this.isSafe?this.url.searchParams:this.fetchOptions.body}set body(e){this.fetchOptions.body=e}get location(){return this.url}get params(){return this.url.searchParams}get entries(){return this.body?Array.from(this.body.entries()):[]}cancel(){this.abortController.abort()}async perform(){const{fetchOptions:e}=this;this.delegate.prepareRequest(this);const t=await this.#t(e);try{this.delegate.requestStarted(this),t.detail.fetchRequest?this.response=t.detail.fetchRequest.response:this.response=N(this.url.href,e);const s=await this.response;return await this.receive(s)}catch(e){if("AbortError"!==e.name)throw this.#s(e)&&this.delegate.requestErrored(this,e),e}finally{this.delegate.requestFinished(this)}}async receive(e){const t=new m(e);return f("turbo:before-fetch-response",{cancelable:!0,detail:{fetchResponse:t},target:this.target}).defaultPrevented?this.delegate.requestPreventedHandlingResponse(this,t):t.succeeded?this.delegate.requestSucceededWithResponse(this,t):this.delegate.requestFailedWithResponse(this,t),t}get defaultHeaders(){return{Accept:"text/html, application/xhtml+xml"}}get isSafe(){return j(this.method)}get abortSignal(){return this.abortController.signal}acceptResponseType(e){this.headers.Accept=[e,this.headers.Accept].join(", ")}async#t(e){const t=new Promise((e=>this.#e=e)),s=f("turbo:before-fetch-request",{cancelable:!0,detail:{fetchOptions:e,url:this.url,resume:this.#e},target:this.target});return this.url=s.detail.url,s.defaultPrevented&&await t,s}#s(e){return!f("turbo:fetch-request-error",{target:this.target,cancelable:!0,detail:{request:this,error:e}}).defaultPrevented}}function j(e){return V(e)==x.get}function $(e,t,s,r){const i=Array.from(s).length>0?new URLSearchParams(_(s)):e.searchParams;return j(t)?[z(e,i),null]:r==W.urlEncoded?[e,i]:[e,s]}function _(e){const t=[];for(const[s,r]of e)r instanceof File||t.push([s,r]);return t}function z(e,t){const s=new URLSearchParams(_(t));return e.search=s.toString(),e}class X{started=!1;constructor(e,t){this.delegate=e,this.element=t,this.intersectionObserver=new IntersectionObserver(this.intersect)}start(){this.started||(this.started=!0,this.intersectionObserver.observe(this.element))}stop(){this.started&&(this.started=!1,this.intersectionObserver.unobserve(this.element))}intersect=e=>{const t=e.slice(-1)[0];t?.isIntersecting&&this.delegate.elementAppearedInViewport(this.element)}}class K{static contentType="text/vnd.turbo-stream.html";static wrap(e){return"string"==typeof e?new this(function(e){const t=document.createElement("template");return t.innerHTML=e,t.content}(e)):e}constructor(e){this.fragment=function(e){for(const t of e.querySelectorAll("turbo-stream")){const e=document.importNode(t,!0);for(const t of e.templateElement.content.querySelectorAll("script"))t.replaceWith(p(t));t.replaceWith(e)}return e}(e)}}const Q=new class{#r=null;#i=null;get(e){if(this.#i&&this.#i.url===e&&this.#i.expire>Date.now())return this.#i.request}setLater(e,t,s){this.clear(),this.#r=setTimeout((()=>{t.perform(),this.set(e,t,s),this.#r=null}),100)}set(e,t,s){this.#i={url:e,request:t,expire:new Date((new Date).getTime()+s)}}clear(){this.#r&&clearTimeout(this.#r),this.#i=null}},Y={initialized:"initialized",requesting:"requesting",waiting:"waiting",receiving:"receiving",stopping:"stopping",stopped:"stopped"};class J{state=Y.initialized;static confirmMethod(e,t,s){return Promise.resolve(confirm(e))}constructor(e,t,s,r=!1){const i=function(e,t){const s=t?.getAttribute("formmethod")||e.getAttribute("method")||"";return V(s.toLowerCase())||x.get}(t,s),o=function(e,t){const s=n(e);j(t)&&(s.search="");return s}(function(e,t){const s="string"==typeof e.action?e.action:null;return t?.hasAttribute("formaction")?t.getAttribute("formaction")||"":e.getAttribute("action")||s||""}(t,s),i),a=function(e,t){const s=new FormData(e),r=t?.getAttribute("name"),i=t?.getAttribute("value");r&&s.append(r,i||"");return s}(t,s),l=function(e,t){return D(t?.getAttribute("formenctype")||e.enctype)}(t,s);this.delegate=e,this.formElement=t,this.submitter=s,this.fetchRequest=new U(this,i,o,a,t,l),this.mustRedirect=r}get method(){return this.fetchRequest.method}set method(e){this.fetchRequest.method=e}get action(){return this.fetchRequest.url.toString()}set action(e){this.fetchRequest.url=n(e)}get body(){return this.fetchRequest.body}get enctype(){return this.fetchRequest.enctype}get isSafe(){return this.fetchRequest.isSafe}get location(){return this.fetchRequest.url}async start(){const{initialized:e,requesting:t}=Y,s=y("data-turbo-confirm",this.submitter,this.formElement);if("string"==typeof s){if(!await J.confirmMethod(s,this.formElement,this.submitter))return}if(this.state==e)return this.state=t,this.fetchRequest.perform()}stop(){const{stopping:e,stopped:t}=Y;if(this.state!=e&&this.state!=t)return this.state=e,this.fetchRequest.cancel(),!0}prepareRequest(e){if(!e.isSafe){const t=function(e){if(null!=e){const t=(document.cookie?document.cookie.split("; "):[]).find((t=>t.startsWith(e)));if(t){const e=t.split("=").slice(1).join("=");return e?decodeURIComponent(e):void 0}}}(M("csrf-param"))||M("csrf-token");t&&(e.headers["X-CSRF-Token"]=t)}this.requestAcceptsTurboStreamResponse(e)&&e.acceptResponseType(K.contentType)}requestStarted(e){this.state=Y.waiting,this.submitter?.setAttribute("disabled",""),this.setSubmitsWith(),R(this.formElement),f("turbo:submit-start",{target:this.formElement,detail:{formSubmission:this}}),this.delegate.formSubmissionStarted(this)}requestPreventedHandlingResponse(e,t){Q.clear(),this.result={success:t.succeeded,fetchResponse:t}}requestSucceededWithResponse(e,t){if(t.clientError||t.serverError)this.delegate.formSubmissionFailedWithResponse(this,t);else if(Q.clear(),this.requestMustRedirect(e)&&function(e){return 200==e.statusCode&&!e.redirected}(t)){const e=new Error("Form responses must redirect to another location");this.delegate.formSubmissionErrored(this,e)}else this.state=Y.receiving,this.result={success:!0,fetchResponse:t},this.delegate.formSubmissionSucceededWithResponse(this,t)}requestFailedWithResponse(e,t){this.result={success:!1,fetchResponse:t},this.delegate.formSubmissionFailedWithResponse(this,t)}requestErrored(e,t){this.result={success:!1,error:t},this.delegate.formSubmissionErrored(this,t)}requestFinished(e){this.state=Y.stopped,this.submitter?.removeAttribute("disabled"),this.resetSubmitterText(),L(this.formElement),f("turbo:submit-end",{target:this.formElement,detail:{formSubmission:this,...this.result}}),this.delegate.formSubmissionFinished(this)}setSubmitsWith(){if(this.submitter&&this.submitsWith)if(this.submitter.matches("button"))this.originalSubmitText=this.submitter.innerHTML,this.submitter.innerHTML=this.submitsWith;else if(this.submitter.matches("input")){const e=this.submitter;this.originalSubmitText=e.value,e.value=this.submitsWith}}resetSubmitterText(){if(this.submitter&&this.originalSubmitText)if(this.submitter.matches("button"))this.submitter.innerHTML=this.originalSubmitText;else if(this.submitter.matches("input")){this.submitter.value=this.originalSubmitText}}requestMustRedirect(e){return!e.isSafe&&this.mustRedirect}requestAcceptsTurboStreamResponse(e){return!e.isSafe||function(e,...t){return t.some((t=>t&&t.hasAttribute(e)))}("data-turbo-stream",this.submitter,this.formElement)}get submitsWith(){return this.submitter?.getAttribute("data-turbo-submits-with")}}class G{constructor(e){this.element=e}get activeElement(){return this.element.ownerDocument.activeElement}get children(){return[...this.element.children]}hasAnchor(e){return null!=this.getElementForAnchor(e)}getElementForAnchor(e){return e?this.element.querySelector(`[id='${e}'], a[name='${e}']`):null}get isConnected(){return this.element.isConnected}get firstAutofocusableElement(){return I(this.element)}get permanentElements(){return ee(this.element)}getPermanentElementById(e){return Z(this.element,e)}getPermanentElementMapForSnapshot(e){const t={};for(const s of this.permanentElements){const{id:r}=s,i=e.getPermanentElementById(r);i&&(t[r]=[s,i])}return t}}function Z(e,t){return e.querySelector(`#${t}[data-turbo-permanent]`)}function ee(e){return e.querySelectorAll("[id][data-turbo-permanent]")}class te{started=!1;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||(this.eventTarget.addEventListener("submit",this.submitCaptured,!0),this.started=!0)}stop(){this.started&&(this.eventTarget.removeEventListener("submit",this.submitCaptured,!0),this.started=!1)}submitCaptured=()=>{this.eventTarget.removeEventListener("submit",this.submitBubbled,!1),this.eventTarget.addEventListener("submit",this.submitBubbled,!1)};submitBubbled=e=>{if(!e.defaultPrevented){const t=e.target instanceof HTMLFormElement?e.target:void 0,s=e.submitter||void 0;t&&function(e,t){const s=t?.getAttribute("formmethod")||e.getAttribute("method");return"dialog"!=s}(t,s)&&function(e,t){if(t?.hasAttribute("formtarget")||e.hasAttribute("target")){const s=t?.getAttribute("formtarget")||e.target;for(const e of document.getElementsByName(s))if(e instanceof HTMLIFrameElement)return!1;return!0}return!0}(t,s)&&this.delegate.willSubmitForm(t,s)&&(e.preventDefault(),e.stopImmediatePropagation(),this.delegate.formSubmitted(t,s))}}}class se{#n=e=>{};#o=e=>{};constructor(e,t){this.delegate=e,this.element=t}scrollToAnchor(e){const t=this.snapshot.getElementForAnchor(e);t?(this.scrollToElement(t),this.focusElement(t)):this.scrollToPosition({x:0,y:0})}scrollToAnchorFromLocation(e){this.scrollToAnchor(o(e))}scrollToElement(e){e.scrollIntoView()}focusElement(e){e instanceof HTMLElement&&(e.hasAttribute("tabindex")?e.focus():(e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")))}scrollToPosition({x:e,y:t}){this.scrollRoot.scrollTo(e,t)}scrollToTop(){this.scrollToPosition({x:0,y:0})}get scrollRoot(){return window}async render(e){const{isPreview:t,shouldRender:s,willRender:r,newSnapshot:i}=e,n=r;if(s)try{this.renderPromise=new Promise((e=>this.#n=e)),this.renderer=e,await this.prepareToRenderSnapshot(e);const s=new Promise((e=>this.#o=e)),r={resume:this.#o,render:this.renderer.renderElement,renderMethod:this.renderer.renderMethod};this.delegate.allowsImmediateRender(i,r)||await s,await this.renderSnapshot(e),this.delegate.viewRenderedSnapshot(i,t,this.renderer.renderMethod),this.delegate.preloadOnLoadLinksForView(this.element),this.finishRenderingSnapshot(e)}finally{delete this.renderer,this.#n(void 0),delete this.renderPromise}else n&&this.invalidate(e.reloadReason)}invalidate(e){this.delegate.viewInvalidated(e)}async prepareToRenderSnapshot(e){this.markAsPreview(e.isPreview),await e.prepareToRender()}markAsPreview(e){e?this.element.setAttribute("data-turbo-preview",""):this.element.removeAttribute("data-turbo-preview")}markVisitDirection(e){this.element.setAttribute("data-turbo-visit-direction",e)}unmarkVisitDirection(){this.element.removeAttribute("data-turbo-visit-direction")}async renderSnapshot(e){await e.render()}finishRenderingSnapshot(e){e.finishRendering()}}class re extends se{missing(){this.element.innerHTML='<strong class="turbo-frame-error">Content missing</strong>'}get snapshot(){return new G(this.element)}}class ie{constructor(e,t){this.delegate=e,this.element=t}start(){this.element.addEventListener("click",this.clickBubbled),document.addEventListener("turbo:click",this.linkClicked),document.addEventListener("turbo:before-visit",this.willVisit)}stop(){this.element.removeEventListener("click",this.clickBubbled),document.removeEventListener("turbo:click",this.linkClicked),document.removeEventListener("turbo:before-visit",this.willVisit)}clickBubbled=e=>{this.respondsToEventTarget(e.target)?this.clickEvent=e:delete this.clickEvent};linkClicked=e=>{this.clickEvent&&this.respondsToEventTarget(e.target)&&e.target instanceof Element&&this.delegate.shouldInterceptLinkClick(e.target,e.detail.url,e.detail.originalEvent)&&(this.clickEvent.preventDefault(),e.preventDefault(),this.delegate.linkClickIntercepted(e.target,e.detail.url,e.detail.originalEvent)),delete this.clickEvent};willVisit=e=>{delete this.clickEvent};respondsToEventTarget(e){const t=e instanceof Element?e:e instanceof Node?e.parentElement:null;return t&&t.closest("turbo-frame, html")==this.element}}class ne{started=!1;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||(this.eventTarget.addEventListener("click",this.clickCaptured,!0),this.started=!0)}stop(){this.started&&(this.eventTarget.removeEventListener("click",this.clickCaptured,!0),this.started=!1)}clickCaptured=()=>{this.eventTarget.removeEventListener("click",this.clickBubbled,!1),this.eventTarget.addEventListener("click",this.clickBubbled,!1)};clickBubbled=e=>{if(e instanceof MouseEvent&&this.clickEventIsSignificant(e)){const t=function(e){return k(e,"a[href]:not([target^=_]):not([download])")}(e.composedPath&&e.composedPath()[0]||e.target);if(t&&function(e){if(e.hasAttribute("target"))for(const t of document.getElementsByName(e.target))if(t instanceof HTMLIFrameElement)return!1;return!0}(t)){const s=q(t);this.delegate.willFollowLinkToLocation(t,s,e)&&(e.preventDefault(),this.delegate.followedLinkToLocation(t,s))}}};clickEventIsSignificant(e){return!(e.target&&e.target.isContentEditable||e.defaultPrevented||e.which>1||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey)}}class oe{constructor(e,t){this.delegate=e,this.linkInterceptor=new ne(this,t)}start(){this.linkInterceptor.start()}stop(){this.linkInterceptor.stop()}canPrefetchRequestToLocation(e,t){return!1}prefetchAndCacheRequestToLocation(e,t){}willFollowLinkToLocation(e,t,s){return this.delegate.willSubmitFormLinkToLocation(e,t,s)&&(e.hasAttribute("data-turbo-method")||e.hasAttribute("data-turbo-stream"))}followedLinkToLocation(e,t){const s=document.createElement("form");for(const[e,r]of t.searchParams)s.append(Object.assign(document.createElement("input"),{type:"hidden",name:e,value:r}));const r=Object.assign(t,{search:""});s.setAttribute("data-turbo","true"),s.setAttribute("action",r.href),s.setAttribute("hidden","");const i=e.getAttribute("data-turbo-method");i&&s.setAttribute("method",i);const n=e.getAttribute("data-turbo-frame");n&&s.setAttribute("data-turbo-frame",n);const o=C(e);o&&s.setAttribute("data-turbo-action",o);const a=e.getAttribute("data-turbo-confirm");a&&s.setAttribute("data-turbo-confirm",a);e.hasAttribute("data-turbo-stream")&&s.setAttribute("data-turbo-stream",""),this.delegate.submittedFormLinkToLocation(e,t,s),document.body.appendChild(s),s.addEventListener("turbo:submit-end",(()=>s.remove()),{once:!0}),requestAnimationFrame((()=>s.requestSubmit()))}}class ae{static async preservingPermanentElements(e,t,s){const r=new this(e,t);r.enter(),await s(),r.leave()}constructor(e,t){this.delegate=e,this.permanentElementMap=t}enter(){for(const e in this.permanentElementMap){const[t,s]=this.permanentElementMap[e];this.delegate.enteringBardo(t,s),this.replaceNewPermanentElementWithPlaceholder(s)}}leave(){for(const e in this.permanentElementMap){const[t]=this.permanentElementMap[e];this.replaceCurrentPermanentElementWithClone(t),this.replacePlaceholderWithPermanentElement(t),this.delegate.leavingBardo(t)}}replaceNewPermanentElementWithPlaceholder(e){const t=function(e){const t=document.createElement("meta");return t.setAttribute("name","turbo-permanent-placeholder"),t.setAttribute("content",e.id),t}(e);e.replaceWith(t)}replaceCurrentPermanentElementWithClone(e){const t=e.cloneNode(!0);e.replaceWith(t)}replacePlaceholderWithPermanentElement(e){const t=this.getPlaceholderById(e.id);t?.replaceWith(e)}getPlaceholderById(e){return this.placeholders.find((t=>t.content==e))}get placeholders(){return[...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]")]}}class le{#a=null;constructor(e,t,s,r,i=!0){this.currentSnapshot=e,this.newSnapshot=t,this.isPreview=r,this.willRender=i,this.renderElement=s,this.promise=new Promise(((e,t)=>this.resolvingFunctions={resolve:e,reject:t}))}get shouldRender(){return!0}get reloadReason(){}prepareToRender(){}render(){}finishRendering(){this.resolvingFunctions&&(this.resolvingFunctions.resolve(),delete this.resolvingFunctions)}async preservingPermanentElements(e){await ae.preservingPermanentElements(this,this.permanentElementMap,e)}focusFirstAutofocusableElement(){const e=this.connectedSnapshot.firstAutofocusableElement;e&&e.focus()}enteringBardo(e){this.#a||e.contains(this.currentSnapshot.activeElement)&&(this.#a=this.currentSnapshot.activeElement)}leavingBardo(e){e.contains(this.#a)&&this.#a instanceof HTMLElement&&(this.#a.focus(),this.#a=null)}get connectedSnapshot(){return this.newSnapshot.isConnected?this.newSnapshot:this.currentSnapshot}get currentElement(){return this.currentSnapshot.element}get newElement(){return this.newSnapshot.element}get permanentElementMap(){return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot)}get renderMethod(){return"replace"}}class ce extends le{static renderElement(e,t){const s=document.createRange();s.selectNodeContents(e),s.deleteContents();const r=t,i=r.ownerDocument?.createRange();i&&(i.selectNodeContents(r),e.appendChild(i.extractContents()))}constructor(e,t,s,r,i,n=!0){super(t,s,r,i,n),this.delegate=e}get shouldRender(){return!0}async render(){await g(),this.preservingPermanentElements((()=>{this.loadFrameElement()})),this.scrollFrameIntoView(),await g(),this.focusFirstAutofocusableElement(),await g(),this.activateScriptElements()}loadFrameElement(){this.delegate.willRenderFrame(this.currentElement,this.newElement),this.renderElement(this.currentElement,this.newElement)}scrollFrameIntoView(){if(this.currentElement.autoscroll||this.newElement.autoscroll){const s=this.currentElement.firstElementChild,r=(e=this.currentElement.getAttribute("data-autoscroll-block"),t="end","end"==e||"start"==e||"center"==e||"nearest"==e?e:t),i=function(e,t){return"auto"==e||"smooth"==e?e:t}(this.currentElement.getAttribute("data-autoscroll-behavior"),"auto");if(s)return s.scrollIntoView({block:r,behavior:i}),!0}var e,t;return!1}activateScriptElements(){for(const e of this.newScriptElements){const t=p(e);e.replaceWith(t)}}get newScriptElements(){return this.currentElement.querySelectorAll("script")}}class he{static animationDuration=300;static get defaultCSS(){return w`
12
+ .turbo-progress-bar {
13
+ position: fixed;
14
+ display: block;
15
+ top: 0;
16
+ left: 0;
17
+ height: 3px;
18
+ background: #0076ff;
19
+ z-index: 2147483647;
20
+ transition:
21
+ width ${he.animationDuration}ms ease-out,
22
+ opacity ${he.animationDuration/2}ms ${he.animationDuration/2}ms ease-in;
23
+ transform: translate3d(0, 0, 0);
24
+ }
25
+ `}hiding=!1;value=0;visible=!1;constructor(){this.stylesheetElement=this.createStylesheetElement(),this.progressElement=this.createProgressElement(),this.installStylesheetElement(),this.setValue(0)}show(){this.visible||(this.visible=!0,this.installProgressElement(),this.startTrickling())}hide(){this.visible&&!this.hiding&&(this.hiding=!0,this.fadeProgressElement((()=>{this.uninstallProgressElement(),this.stopTrickling(),this.visible=!1,this.hiding=!1})))}setValue(e){this.value=e,this.refresh()}installStylesheetElement(){document.head.insertBefore(this.stylesheetElement,document.head.firstChild)}installProgressElement(){this.progressElement.style.width="0",this.progressElement.style.opacity="1",document.documentElement.insertBefore(this.progressElement,document.body),this.refresh()}fadeProgressElement(e){this.progressElement.style.opacity="0",setTimeout(e,1.5*he.animationDuration)}uninstallProgressElement(){this.progressElement.parentNode&&document.documentElement.removeChild(this.progressElement)}startTrickling(){this.trickleInterval||(this.trickleInterval=window.setInterval(this.trickle,he.animationDuration))}stopTrickling(){window.clearInterval(this.trickleInterval),delete this.trickleInterval}trickle=()=>{this.setValue(this.value+Math.random()/100)};refresh(){requestAnimationFrame((()=>{this.progressElement.style.width=10+90*this.value+"%"}))}createStylesheetElement(){const e=document.createElement("style");return e.type="text/css",e.textContent=he.defaultCSS,this.cspNonce&&(e.nonce=this.cspNonce),e}createProgressElement(){const e=document.createElement("div");return e.className="turbo-progress-bar",e}get cspNonce(){return M("csp-nonce")}}class de extends G{detailsByOuterHTML=this.children.filter((e=>!function(e){const t=e.localName;return"noscript"==t}(e))).map((e=>function(e){e.hasAttribute("nonce")&&e.setAttribute("nonce","");return e}(e))).reduce(((e,t)=>{const{outerHTML:s}=t,r=s in e?e[s]:{type:ue(t),tracked:me(t),elements:[]};return{...e,[s]:{...r,elements:[...r.elements,t]}}}),{});get trackedElementSignature(){return Object.keys(this.detailsByOuterHTML).filter((e=>this.detailsByOuterHTML[e].tracked)).join("")}getScriptElementsNotInSnapshot(e){return this.getElementsMatchingTypeNotInSnapshot("script",e)}getStylesheetElementsNotInSnapshot(e){return this.getElementsMatchingTypeNotInSnapshot("stylesheet",e)}getElementsMatchingTypeNotInSnapshot(e,t){return Object.keys(this.detailsByOuterHTML).filter((e=>!(e in t.detailsByOuterHTML))).map((e=>this.detailsByOuterHTML[e])).filter((({type:t})=>t==e)).map((({elements:[e]})=>e))}get provisionalElements(){return Object.keys(this.detailsByOuterHTML).reduce(((e,t)=>{const{type:s,tracked:r,elements:i}=this.detailsByOuterHTML[t];return null!=s||r?i.length>1?[...e,...i.slice(1)]:e:[...e,...i]}),[])}getMetaValue(e){const t=this.findMetaElementByName(e);return t?t.getAttribute("content"):null}findMetaElementByName(e){return Object.keys(this.detailsByOuterHTML).reduce(((t,s)=>{const{elements:[r]}=this.detailsByOuterHTML[s];return function(e,t){const s=e.localName;return"meta"==s&&e.getAttribute("name")==t}(r,e)?r:t}),0)}}function ue(e){return function(e){const t=e.localName;return"script"==t}(e)?"script":function(e){const t=e.localName;return"style"==t||"link"==t&&"stylesheet"==e.getAttribute("rel")}(e)?"stylesheet":void 0}function me(e){return"reload"==e.getAttribute("data-turbo-track")}class pe extends G{static fromHTMLString(e=""){return this.fromDocument(S(e))}static fromElement(e){return this.fromDocument(e.ownerDocument)}static fromDocument({documentElement:e,body:t,head:s}){return new this(e,t,new de(s))}constructor(e,t,s){super(t),this.documentElement=e,this.headSnapshot=s}clone(){const e=this.element.cloneNode(!0),t=this.element.querySelectorAll("select"),s=e.querySelectorAll("select");for(const[e,r]of t.entries()){const t=s[e];for(const e of t.selectedOptions)e.selected=!1;for(const e of r.selectedOptions)t.options[e.index].selected=!0}for(const t of e.querySelectorAll('input[type="password"]'))t.value="";return new pe(this.documentElement,e,this.headSnapshot)}get lang(){return this.documentElement.getAttribute("lang")}get headElement(){return this.headSnapshot.element}get rootLocation(){return n(this.getSetting("root")??"/")}get cacheControlValue(){return this.getSetting("cache-control")}get isPreviewable(){return"no-preview"!=this.cacheControlValue}get isCacheable(){return"no-cache"!=this.cacheControlValue}get isVisitable(){return"reload"!=this.getSetting("visit-control")}get prefersViewTransitions(){return"same-origin"===this.headSnapshot.getMetaValue("view-transition")}get shouldMorphPage(){return"morph"===this.getSetting("refresh-method")}get shouldPreserveScrollPosition(){return"preserve"===this.getSetting("refresh-scroll")}getSetting(e){return this.headSnapshot.getMetaValue(`turbo-${e}`)}}class fe{#l=!1;#c=Promise.resolve();renderChange(e,t){return e&&this.viewTransitionsAvailable&&!this.#l?(this.#l=!0,this.#c=this.#c.then((async()=>{await document.startViewTransition(t).finished}))):this.#c=this.#c.then(t),this.#c}get viewTransitionsAvailable(){return document.startViewTransition}}const ge={action:"advance",historyChanged:!1,visitCachedSnapshot:()=>{},willRender:!0,updateHistory:!0,shouldCacheSnapshot:!0,acceptsStreamResponse:!1},be="visitStart",ve="requestStart",Se="requestEnd",we="visitEnd",Ee="initialized",ye="started",Re="canceled",Le="failed",Ae="completed",Te=0,Ce=-1,Pe=-2,Me={advance:"forward",restore:"back",replace:"none"};class ke{identifier=E();timingMetrics={};followedRedirect=!1;historyChanged=!1;scrolled=!1;shouldCacheSnapshot=!0;acceptsStreamResponse=!1;snapshotCached=!1;state=Ee;viewTransitioner=new fe;constructor(e,t,s,r={}){this.delegate=e,this.location=t,this.restorationIdentifier=s||E();const{action:i,historyChanged:n,referrer:o,snapshot:a,snapshotHTML:l,response:c,visitCachedSnapshot:h,willRender:d,updateHistory:u,shouldCacheSnapshot:m,acceptsStreamResponse:p,direction:f}={...ge,...r};this.action=i,this.historyChanged=n,this.referrer=o,this.snapshot=a,this.snapshotHTML=l,this.response=c,this.isSamePage=this.delegate.locationWithActionIsSamePage(this.location,this.action),this.isPageRefresh=this.view.isPageRefresh(this),this.visitCachedSnapshot=h,this.willRender=d,this.updateHistory=u,this.scrolled=!d,this.shouldCacheSnapshot=m,this.acceptsStreamResponse=p,this.direction=f||Me[i]}get adapter(){return this.delegate.adapter}get view(){return this.delegate.view}get history(){return this.delegate.history}get restorationData(){return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)}get silent(){return this.isSamePage}start(){this.state==Ee&&(this.recordTimingMetric(be),this.state=ye,this.adapter.visitStarted(this),this.delegate.visitStarted(this))}cancel(){this.state==ye&&(this.request&&this.request.cancel(),this.cancelRender(),this.state=Re)}complete(){this.state==ye&&(this.recordTimingMetric(we),this.adapter.visitCompleted(this),this.state=Ae,this.followRedirect(),this.followedRedirect||this.delegate.visitCompleted(this))}fail(){this.state==ye&&(this.state=Le,this.adapter.visitFailed(this),this.delegate.visitCompleted(this))}changeHistory(){if(!this.historyChanged&&this.updateHistory){const e=T(this.location.href===this.referrer?.href?"replace":this.action);this.history.update(e,this.location,this.restorationIdentifier),this.historyChanged=!0}}issueRequest(){this.hasPreloadedResponse()?this.simulateRequest():this.shouldIssueRequest()&&!this.request&&(this.request=new U(this,x.get,this.location),this.request.perform())}simulateRequest(){this.response&&(this.startRequest(),this.recordResponse(),this.finishRequest())}startRequest(){this.recordTimingMetric(ve),this.adapter.visitRequestStarted(this)}recordResponse(e=this.response){if(this.response=e,e){const{statusCode:t}=e;Fe(t)?this.adapter.visitRequestCompleted(this):this.adapter.visitRequestFailedWithStatusCode(this,t)}}finishRequest(){this.recordTimingMetric(Se),this.adapter.visitRequestFinished(this)}loadResponse(){if(this.response){const{statusCode:e,responseHTML:t}=this.response;this.render((async()=>{if(this.shouldCacheSnapshot&&this.cacheSnapshot(),this.view.renderPromise&&await this.view.renderPromise,Fe(e)&&null!=t){const e=pe.fromHTMLString(t);await this.renderPageSnapshot(e,!1),this.adapter.visitRendered(this),this.complete()}else await this.view.renderError(pe.fromHTMLString(t),this),this.adapter.visitRendered(this),this.fail()}))}}getCachedSnapshot(){const e=this.view.getCachedSnapshotForLocation(this.location)||this.getPreloadedSnapshot();if(e&&(!o(this.location)||e.hasAnchor(o(this.location)))&&("restore"==this.action||e.isPreviewable))return e}getPreloadedSnapshot(){if(this.snapshotHTML)return pe.fromHTMLString(this.snapshotHTML)}hasCachedSnapshot(){return null!=this.getCachedSnapshot()}loadCachedSnapshot(){const e=this.getCachedSnapshot();if(e){const t=this.shouldIssueRequest();this.render((async()=>{this.cacheSnapshot(),this.isSamePage||this.isPageRefresh?this.adapter.visitRendered(this):(this.view.renderPromise&&await this.view.renderPromise,await this.renderPageSnapshot(e,t),this.adapter.visitRendered(this),t||this.complete())}))}}followRedirect(){this.redirectedToLocation&&!this.followedRedirect&&this.response?.redirected&&(this.adapter.visitProposedToLocation(this.redirectedToLocation,{action:"replace",response:this.response,shouldCacheSnapshot:!1,willRender:!1}),this.followedRedirect=!0)}goToSamePageAnchor(){this.isSamePage&&this.render((async()=>{this.cacheSnapshot(),this.performScroll(),this.changeHistory(),this.adapter.visitRendered(this)}))}prepareRequest(e){this.acceptsStreamResponse&&e.acceptResponseType(K.contentType)}requestStarted(){this.startRequest()}requestPreventedHandlingResponse(e,t){}async requestSucceededWithResponse(e,t){const s=await t.responseHTML,{redirected:r,statusCode:i}=t;null==s?this.recordResponse({statusCode:Pe,redirected:r}):(this.redirectedToLocation=t.redirected?t.location:void 0,this.recordResponse({statusCode:i,responseHTML:s,redirected:r}))}async requestFailedWithResponse(e,t){const s=await t.responseHTML,{redirected:r,statusCode:i}=t;null==s?this.recordResponse({statusCode:Pe,redirected:r}):this.recordResponse({statusCode:i,responseHTML:s,redirected:r})}requestErrored(e,t){this.recordResponse({statusCode:Te,redirected:!1})}requestFinished(){this.finishRequest()}performScroll(){this.scrolled||this.view.forceReloaded||this.view.shouldPreserveScrollPosition(this)||("restore"==this.action?this.scrollToRestoredPosition()||this.scrollToAnchor()||this.view.scrollToTop():this.scrollToAnchor()||this.view.scrollToTop(),this.isSamePage&&this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation,this.location),this.scrolled=!0)}scrollToRestoredPosition(){const{scrollPosition:e}=this.restorationData;if(e)return this.view.scrollToPosition(e),!0}scrollToAnchor(){const e=o(this.location);if(null!=e)return this.view.scrollToAnchor(e),!0}recordTimingMetric(e){this.timingMetrics[e]=(new Date).getTime()}getTimingMetrics(){return{...this.timingMetrics}}getHistoryMethodForAction(e){switch(e){case"replace":return history.replaceState;case"advance":case"restore":return history.pushState}}hasPreloadedResponse(){return"object"==typeof this.response}shouldIssueRequest(){return!this.isSamePage&&("restore"==this.action?!this.hasCachedSnapshot():this.willRender)}cacheSnapshot(){this.snapshotCached||(this.view.cacheSnapshot(this.snapshot).then((e=>e&&this.visitCachedSnapshot(e))),this.snapshotCached=!0)}async render(e){this.cancelRender(),this.frame=await g(),await e(),delete this.frame}async renderPageSnapshot(e,t){await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(e),(async()=>{await this.view.renderPage(e,t,this.willRender,this),this.performScroll()}))}cancelRender(){this.frame&&(cancelAnimationFrame(this.frame),delete this.frame)}}function Fe(e){return e>=200&&e<300}class Ie{progressBar=new he;constructor(e){this.session=e}visitProposedToLocation(e,t){h(e,this.navigator.rootLocation)?this.navigator.startVisit(e,t?.restorationIdentifier||E(),t):window.location.href=e.toString()}visitStarted(e){this.location=e.location,e.loadCachedSnapshot(),e.issueRequest(),e.goToSamePageAnchor()}visitRequestStarted(e){this.progressBar.setValue(0),e.hasCachedSnapshot()||"restore"!=e.action?this.showVisitProgressBarAfterDelay():this.showProgressBar()}visitRequestCompleted(e){e.loadResponse()}visitRequestFailedWithStatusCode(e,t){switch(t){case Te:case Ce:case Pe:return this.reload({reason:"request_failed",context:{statusCode:t}});default:return e.loadResponse()}}visitRequestFinished(e){}visitCompleted(e){this.progressBar.setValue(1),this.hideVisitProgressBar()}pageInvalidated(e){this.reload(e)}visitFailed(e){this.progressBar.setValue(1),this.hideVisitProgressBar()}visitRendered(e){}formSubmissionStarted(e){this.progressBar.setValue(0),this.showFormProgressBarAfterDelay()}formSubmissionFinished(e){this.progressBar.setValue(1),this.hideFormProgressBar()}showVisitProgressBarAfterDelay(){this.visitProgressBarTimeout=window.setTimeout(this.showProgressBar,this.session.progressBarDelay)}hideVisitProgressBar(){this.progressBar.hide(),null!=this.visitProgressBarTimeout&&(window.clearTimeout(this.visitProgressBarTimeout),delete this.visitProgressBarTimeout)}showFormProgressBarAfterDelay(){null==this.formProgressBarTimeout&&(this.formProgressBarTimeout=window.setTimeout(this.showProgressBar,this.session.progressBarDelay))}hideFormProgressBar(){this.progressBar.hide(),null!=this.formProgressBarTimeout&&(window.clearTimeout(this.formProgressBarTimeout),delete this.formProgressBarTimeout)}showProgressBar=()=>{this.progressBar.show()};reload(e){f("turbo:reload",{detail:e}),window.location.href=this.location?.toString()||window.location.href}get navigator(){return this.session.navigator}}class qe{selector="[data-turbo-temporary]";deprecatedSelector="[data-turbo-cache=false]";started=!1;start(){this.started||(this.started=!0,addEventListener("turbo:before-cache",this.removeTemporaryElements,!1))}stop(){this.started&&(this.started=!1,removeEventListener("turbo:before-cache",this.removeTemporaryElements,!1))}removeTemporaryElements=e=>{for(const e of this.temporaryElements)e.remove()};get temporaryElements(){return[...document.querySelectorAll(this.selector),...this.temporaryElementsWithDeprecation]}get temporaryElementsWithDeprecation(){const e=document.querySelectorAll(this.deprecatedSelector);return e.length&&console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`),[...e]}}class He{constructor(e,t){this.session=e,this.element=t,this.linkInterceptor=new ie(this,t),this.formSubmitObserver=new te(this,t)}start(){this.linkInterceptor.start(),this.formSubmitObserver.start()}stop(){this.linkInterceptor.stop(),this.formSubmitObserver.stop()}shouldInterceptLinkClick(e,t,s){return this.#h(e)}linkClickIntercepted(e,t,s){const r=this.#d(e);r&&r.delegate.linkClickIntercepted(e,t,s)}willSubmitForm(e,t){return null==e.closest("turbo-frame")&&this.#u(e,t)&&this.#h(e,t)}formSubmitted(e,t){const s=this.#d(e,t);s&&s.delegate.formSubmitted(e,t)}#u(e,t){const s=a(e,t),r=this.element.ownerDocument.querySelector('meta[name="turbo-root"]'),i=n(r?.content??"/");return this.#h(e,t)&&h(s,i)}#h(e,t){if(e instanceof HTMLFormElement?this.session.submissionIsNavigatable(e,t):this.session.elementIsNavigatable(e)){const s=this.#d(e,t);return!!s&&s!=e.closest("turbo-frame")}return!1}#d(e,t){const s=t?.getAttribute("data-turbo-frame")||e.getAttribute("data-turbo-frame");if(s&&"_top"!=s){const e=this.element.querySelector(`#${s}:not([disabled])`);if(e instanceof i)return e}}}class Be{location;restorationIdentifier=E();restorationData={};started=!1;pageLoaded=!1;currentIndex=0;constructor(e){this.delegate=e}start(){this.started||(addEventListener("popstate",this.onPopState,!1),addEventListener("load",this.onPageLoad,!1),this.currentIndex=history.state?.turbo?.restorationIndex||0,this.started=!0,this.replace(new URL(window.location.href)))}stop(){this.started&&(removeEventListener("popstate",this.onPopState,!1),removeEventListener("load",this.onPageLoad,!1),this.started=!1)}push(e,t){this.update(history.pushState,e,t)}replace(e,t){this.update(history.replaceState,e,t)}update(e,t,s=E()){e===history.pushState&&++this.currentIndex;const r={turbo:{restorationIdentifier:s,restorationIndex:this.currentIndex}};e.call(history,r,"",t.href),this.location=t,this.restorationIdentifier=s}getRestorationDataForIdentifier(e){return this.restorationData[e]||{}}updateRestorationData(e){const{restorationIdentifier:t}=this,s=this.restorationData[t];this.restorationData[t]={...s,...e}}assumeControlOfScrollRestoration(){this.previousScrollRestoration||(this.previousScrollRestoration=history.scrollRestoration??"auto",history.scrollRestoration="manual")}relinquishControlOfScrollRestoration(){this.previousScrollRestoration&&(history.scrollRestoration=this.previousScrollRestoration,delete this.previousScrollRestoration)}onPopState=e=>{if(this.shouldHandlePopState()){const{turbo:t}=e.state||{};if(t){this.location=new URL(window.location.href);const{restorationIdentifier:e,restorationIndex:s}=t;this.restorationIdentifier=e;const r=s>this.currentIndex?"forward":"back";this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location,e,r),this.currentIndex=s}}};onPageLoad=async e=>{await Promise.resolve(),this.pageLoaded=!0};shouldHandlePopState(){return this.pageIsLoaded()}pageIsLoaded(){return this.pageLoaded||"complete"==document.readyState}}class Oe{started=!1;#m=null;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||("loading"===this.eventTarget.readyState?this.eventTarget.addEventListener("DOMContentLoaded",this.#p,{once:!0}):this.#p())}stop(){this.started&&(this.eventTarget.removeEventListener("mouseenter",this.#f,{capture:!0,passive:!0}),this.eventTarget.removeEventListener("mouseleave",this.#g,{capture:!0,passive:!0}),this.eventTarget.removeEventListener("turbo:before-fetch-request",this.#b,!0),this.started=!1)}#p=()=>{this.eventTarget.addEventListener("mouseenter",this.#f,{capture:!0,passive:!0}),this.eventTarget.addEventListener("mouseleave",this.#g,{capture:!0,passive:!0}),this.eventTarget.addEventListener("turbo:before-fetch-request",this.#b,!0),this.started=!0};#f=e=>{if("false"===M("turbo-prefetch"))return;const t=e.target;if(t.matches&&t.matches("a[href]:not([target^=_]):not([download])")&&this.#v(t)){const e=t,s=q(e);if(this.delegate.canPrefetchRequestToLocation(e,s)){this.#m=e;const r=new U(this,x.get,s,new URLSearchParams,t);Q.setLater(s.toString(),r,this.#S)}}};#g=e=>{e.target===this.#m&&this.#w()};#w=()=>{Q.clear(),this.#m=null};#b=e=>{if("FORM"!==e.target.tagName&&"get"===e.detail.fetchOptions.method){const t=Q.get(e.detail.url.toString());t&&(e.detail.fetchRequest=t),Q.clear()}};prepareRequest(e){const t=e.target;e.headers["X-Sec-Purpose"]="prefetch";const s=t.closest("turbo-frame"),r=t.getAttribute("data-turbo-frame")||s?.getAttribute("target")||s?.id;r&&"_top"!==r&&(e.headers["Turbo-Frame"]=r)}requestSucceededWithResponse(){}requestStarted(e){}requestErrored(e){}requestFinished(e){}requestPreventedHandlingResponse(e,t){}requestFailedWithResponse(e,t){}get#S(){return Number(M("turbo-prefetch-cache-time"))||1e4}#v(e){return!!e.getAttribute("href")&&(!Ne(e)&&(!Ve(e)&&(!xe(e)&&(!De(e)&&!Ue(e)))))}}const Ne=e=>e.origin!==document.location.origin||!["http:","https:"].includes(e.protocol)||e.hasAttribute("target"),Ve=e=>e.pathname+e.search===document.location.pathname+document.location.search||e.href.startsWith("#"),xe=e=>{if("false"===e.getAttribute("data-turbo-prefetch"))return!0;if("false"===e.getAttribute("data-turbo"))return!0;const t=k(e,"[data-turbo-prefetch]");return!(!t||"false"!==t.getAttribute("data-turbo-prefetch"))},De=e=>{const t=e.getAttribute("data-turbo-method");return!(!t||"get"===t.toLowerCase())||(!!We(e)||(!!e.hasAttribute("data-turbo-confirm")||!!e.hasAttribute("data-turbo-stream")))},We=e=>e.hasAttribute("data-remote")||e.hasAttribute("data-behavior")||e.hasAttribute("data-confirm")||e.hasAttribute("data-method"),Ue=e=>f("turbo:before-prefetch",{target:e,cancelable:!0}).defaultPrevented;class je{constructor(e){this.delegate=e}proposeVisit(e,t={}){this.delegate.allowsVisitingLocationWithAction(e,t.action)&&this.delegate.visitProposedToLocation(e,t)}startVisit(e,t,s={}){this.stop(),this.currentVisit=new ke(this,n(e),t,{referrer:this.location,...s}),this.currentVisit.start()}submitForm(e,t){this.stop(),this.formSubmission=new J(this,e,t,!0),this.formSubmission.start()}stop(){this.formSubmission&&(this.formSubmission.stop(),delete this.formSubmission),this.currentVisit&&(this.currentVisit.cancel(),delete this.currentVisit)}get adapter(){return this.delegate.adapter}get view(){return this.delegate.view}get rootLocation(){return this.view.snapshot.rootLocation}get history(){return this.delegate.history}formSubmissionStarted(e){"function"==typeof this.adapter.formSubmissionStarted&&this.adapter.formSubmissionStarted(e)}async formSubmissionSucceededWithResponse(e,t){if(e==this.formSubmission){const s=await t.responseHTML;if(s){const r=e.isSafe;r||this.view.clearSnapshotCache();const{statusCode:i,redirected:n}=t,o={action:this.#E(e,t),shouldCacheSnapshot:r,response:{statusCode:i,responseHTML:s,redirected:n}};this.proposeVisit(t.location,o)}}}async formSubmissionFailedWithResponse(e,t){const s=await t.responseHTML;if(s){const e=pe.fromHTMLString(s);t.serverError?await this.view.renderError(e,this.currentVisit):await this.view.renderPage(e,!1,!0,this.currentVisit),e.shouldPreserveScrollPosition||this.view.scrollToTop(),this.view.clearSnapshotCache()}}formSubmissionErrored(e,t){console.error(t)}formSubmissionFinished(e){"function"==typeof this.adapter.formSubmissionFinished&&this.adapter.formSubmissionFinished(e)}visitStarted(e){this.delegate.visitStarted(e)}visitCompleted(e){this.delegate.visitCompleted(e)}locationWithActionIsSamePage(e,t){const s=o(e),r=o(this.view.lastRenderedLocation),i="restore"===t&&void 0===s;return"replace"!==t&&d(e)===d(this.view.lastRenderedLocation)&&(i||null!=s&&s!==r)}visitScrolledToSamePageLocation(e,t){this.delegate.visitScrolledToSamePageLocation(e,t)}get location(){return this.history.location}get restorationIdentifier(){return this.history.restorationIdentifier}#E(e,t){const{submitter:s,formElement:r}=e;return C(s,r)||this.#y(t)}#y(e){return e.redirected&&e.location.href===this.location?.href?"replace":"advance"}}const $e=0,_e=1,ze=2,Xe=3;class Ke{stage=$e;started=!1;constructor(e){this.delegate=e}start(){this.started||(this.stage==$e&&(this.stage=_e),document.addEventListener("readystatechange",this.interpretReadyState,!1),addEventListener("pagehide",this.pageWillUnload,!1),this.started=!0)}stop(){this.started&&(document.removeEventListener("readystatechange",this.interpretReadyState,!1),removeEventListener("pagehide",this.pageWillUnload,!1),this.started=!1)}interpretReadyState=()=>{const{readyState:e}=this;"interactive"==e?this.pageIsInteractive():"complete"==e&&this.pageIsComplete()};pageIsInteractive(){this.stage==_e&&(this.stage=ze,this.delegate.pageBecameInteractive())}pageIsComplete(){this.pageIsInteractive(),this.stage==ze&&(this.stage=Xe,this.delegate.pageLoaded())}pageWillUnload=()=>{this.delegate.pageWillUnload()};get readyState(){return document.readyState}}class Qe{started=!1;constructor(e){this.delegate=e}start(){this.started||(addEventListener("scroll",this.onScroll,!1),this.onScroll(),this.started=!0)}stop(){this.started&&(removeEventListener("scroll",this.onScroll,!1),this.started=!1)}onScroll=()=>{this.updatePosition({x:window.pageXOffset,y:window.pageYOffset})};updatePosition(e){this.delegate.scrollPositionChanged(e)}}class Ye{render({fragment:e}){ae.preservingPermanentElements(this,function(e){const t=ee(document.documentElement),s={};for(const r of t){const{id:t}=r;for(const i of e.querySelectorAll("turbo-stream")){const e=Z(i.templateElement.content,t);e&&(s[t]=[r,e])}}return s}(e),(()=>{!async function(e,t){const s=`turbo-stream-autofocus-${E()}`,r=e.querySelectorAll("turbo-stream"),i=function(e){for(const t of e){const e=I(t.templateElement.content);if(e)return e}return null}(r);let n=null;i&&(n=i.id?i.id:s,i.id=n);t(),await g();if((null==document.activeElement||document.activeElement==document.body)&&n){const e=document.getElementById(n);F(e)&&e.focus(),e&&e.id==s&&e.removeAttribute("id")}}(e,(()=>{!async function(e){const[t,s]=await async function(e,t){const s=t();return e(),await b(),[s,t()]}(e,(()=>document.activeElement)),r=t&&t.id;if(r){const e=document.getElementById(r);F(e)&&e!=s&&e.focus()}}((()=>{document.documentElement.appendChild(e)}))}))}))}enteringBardo(e,t){t.replaceWith(e.cloneNode(!0))}leavingBardo(){}}class Je{sources=new Set;#R=!1;constructor(e){this.delegate=e}start(){this.#R||(this.#R=!0,addEventListener("turbo:before-fetch-response",this.inspectFetchResponse,!1))}stop(){this.#R&&(this.#R=!1,removeEventListener("turbo:before-fetch-response",this.inspectFetchResponse,!1))}connectStreamSource(e){this.streamSourceIsConnected(e)||(this.sources.add(e),e.addEventListener("message",this.receiveMessageEvent,!1))}disconnectStreamSource(e){this.streamSourceIsConnected(e)&&(this.sources.delete(e),e.removeEventListener("message",this.receiveMessageEvent,!1))}streamSourceIsConnected(e){return this.sources.has(e)}inspectFetchResponse=e=>{const t=function(e){const t=e.detail?.fetchResponse;if(t instanceof m)return t}(e);t&&function(e){const t=e.contentType??"";return t.startsWith(K.contentType)}(t)&&(e.preventDefault(),this.receiveMessageResponse(t))};receiveMessageEvent=e=>{this.#R&&"string"==typeof e.data&&this.receiveMessageHTML(e.data)};async receiveMessageResponse(e){const t=await e.responseHTML;t&&this.receiveMessageHTML(t)}receiveMessageHTML(e){this.delegate.receivedMessageFromStream(K.wrap(e))}}class Ge extends le{static renderElement(e,t){const{documentElement:s,body:r}=document;s.replaceChild(t,r)}async render(){this.replaceHeadAndBody(),this.activateScriptElements()}replaceHeadAndBody(){const{documentElement:e,head:t}=document;e.replaceChild(this.newHead,t),this.renderElement(this.currentElement,this.newElement)}activateScriptElements(){for(const e of this.scriptElements){const t=e.parentNode;if(t){const s=p(e);t.replaceChild(s,e)}}}get newHead(){return this.newSnapshot.headSnapshot.element}get scriptElements(){return document.documentElement.querySelectorAll("script")}}var Ze=function(){let e=new Set,t={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:c,afterNodeAdded:c,beforeNodeMorphed:c,afterNodeMorphed:c,beforeNodeRemoved:c,afterNodeRemoved:c,beforeAttributeUpdated:c},head:{style:"merge",shouldPreserve:function(e){return"true"===e.getAttribute("im-preserve")},shouldReAppend:function(e){return"true"===e.getAttribute("im-re-append")},shouldRemove:c,afterHeadMorphed:c}};function s(e,t,r){if(r.head.block){let i=e.querySelector("head"),n=t.querySelector("head");if(i&&n){let o=l(n,i,r);return void Promise.all(o).then((function(){s(e,t,Object.assign(r,{head:{block:!1,ignore:!0}}))}))}}if("innerHTML"===r.morphStyle)return n(t,e,r),e.children;if("outerHTML"===r.morphStyle||null==r.morphStyle){let s=function(e,t,s){let r;r=e.firstChild;let i=r,n=0;for(;r;){let e=f(r,t,s);e>n&&(i=r,n=e),r=r.nextSibling}return i}(t,e,r),n=s?.previousSibling,o=s?.nextSibling,a=i(e,s,r);return s?function(e,t,s){let r=[],i=[];for(;null!=e;)r.push(e),e=e.previousSibling;for(;r.length>0;){let e=r.pop();i.push(e),t.parentElement.insertBefore(e,t)}i.push(t);for(;null!=s;)r.push(s),i.push(s),s=s.nextSibling;for(;r.length>0;)t.parentElement.insertBefore(r.pop(),t.nextSibling);return i}(n,a,o):[]}throw"Do not understand how to morph style "+r.morphStyle}function r(e,t){return t.ignoreActiveValue&&e===document.activeElement&&e!==document.body}function i(e,t,s){if(!s.ignoreActive||e!==document.activeElement)return null==t?!1===s.callbacks.beforeNodeRemoved(e)?e:(e.remove(),s.callbacks.afterNodeRemoved(e),null):d(e,t)?(!1===s.callbacks.beforeNodeMorphed(e,t)||(e instanceof HTMLHeadElement&&s.head.ignore||(e instanceof HTMLHeadElement&&"morph"!==s.head.style?l(t,e,s):(!function(e,t,s){let i=e.nodeType;if(1===i){const r=e.attributes,i=t.attributes;for(const e of r)o(e.name,t,"update",s)||t.getAttribute(e.name)!==e.value&&t.setAttribute(e.name,e.value);for(let r=i.length-1;0<=r;r--){const n=i[r];o(n.name,t,"remove",s)||(e.hasAttribute(n.name)||t.removeAttribute(n.name))}}8!==i&&3!==i||t.nodeValue!==e.nodeValue&&(t.nodeValue=e.nodeValue);r(t,s)||function(e,t,s){if(e instanceof HTMLInputElement&&t instanceof HTMLInputElement&&"file"!==e.type){let r=e.value,i=t.value;a(e,t,"checked",s),a(e,t,"disabled",s),e.hasAttribute("value")?r!==i&&(o("value",t,"update",s)||(t.setAttribute("value",r),t.value=r)):o("value",t,"remove",s)||(t.value="",t.removeAttribute("value"))}else if(e instanceof HTMLOptionElement)a(e,t,"selected",s);else if(e instanceof HTMLTextAreaElement&&t instanceof HTMLTextAreaElement){let r=e.value,i=t.value;if(o("value",t,"update",s))return;r!==i&&(t.value=r),t.firstChild&&t.firstChild.nodeValue!==r&&(t.firstChild.nodeValue=r)}}(e,t,s)}(t,e,s),r(e,s)||n(t,e,s))),s.callbacks.afterNodeMorphed(e,t)),e):!1===s.callbacks.beforeNodeRemoved(e)||!1===s.callbacks.beforeNodeAdded(t)?e:(e.parentElement.replaceChild(t,e),s.callbacks.afterNodeAdded(t),s.callbacks.afterNodeRemoved(e),t)}function n(e,t,s){let r,n=e.firstChild,o=t.firstChild;for(;n;){if(r=n,n=r.nextSibling,null==o){if(!1===s.callbacks.beforeNodeAdded(r))return;t.appendChild(r),s.callbacks.afterNodeAdded(r),S(s,r);continue}if(h(r,o,s)){i(o,r,s),o=o.nextSibling,S(s,r);continue}let a=m(e,t,r,o,s);if(a){o=u(o,a,s),i(a,r,s),S(s,r);continue}let l=p(e,t,r,o,s);if(l)o=u(o,l,s),i(l,r,s),S(s,r);else{if(!1===s.callbacks.beforeNodeAdded(r))return;t.insertBefore(r,o),s.callbacks.afterNodeAdded(r),S(s,r)}}for(;null!==o;){let e=o;o=o.nextSibling,g(e,s)}}function o(e,t,s,r){return!("value"!==e||!r.ignoreActiveValue||t!==document.activeElement)||!1===r.callbacks.beforeAttributeUpdated(e,t,s)}function a(e,t,s,r){if(e[s]!==t[s]){let i=o(s,t,"update",r);i||(t[s]=e[s]),e[s]?i||t.setAttribute(s,e[s]):o(s,t,"remove",r)||t.removeAttribute(s)}}function l(e,t,s){let r=[],i=[],n=[],o=[],a=s.head.style,l=new Map;for(const t of e.children)l.set(t.outerHTML,t);for(const e of t.children){let t=l.has(e.outerHTML),r=s.head.shouldReAppend(e),c=s.head.shouldPreserve(e);t||c?r?i.push(e):(l.delete(e.outerHTML),n.push(e)):"append"===a?r&&(i.push(e),o.push(e)):!1!==s.head.shouldRemove(e)&&i.push(e)}o.push(...l.values());let c=[];for(const e of o){let i=document.createRange().createContextualFragment(e.outerHTML).firstChild;if(!1!==s.callbacks.beforeNodeAdded(i)){if(i.href||i.src){let e=null,t=new Promise((function(t){e=t}));i.addEventListener("load",(function(){e()})),c.push(t)}t.appendChild(i),s.callbacks.afterNodeAdded(i),r.push(i)}}for(const e of i)!1!==s.callbacks.beforeNodeRemoved(e)&&(t.removeChild(e),s.callbacks.afterNodeRemoved(e));return s.head.afterHeadMorphed(t,{added:r,kept:n,removed:i}),c}function c(){}function h(e,t,s){return null!=e&&null!=t&&(e.nodeType===t.nodeType&&e.tagName===t.tagName&&(""!==e.id&&e.id===t.id||w(s,e,t)>0))}function d(e,t){return null!=e&&null!=t&&(e.nodeType===t.nodeType&&e.tagName===t.tagName)}function u(e,t,s){for(;e!==t;){let t=e;e=e.nextSibling,g(t,s)}return S(s,t),t.nextSibling}function m(e,t,s,r,i){let n=w(i,s,t);if(n>0){let t=r,o=0;for(;null!=t;){if(h(s,t,i))return t;if(o+=w(i,t,e),o>n)return null;t=t.nextSibling}}return null}function p(e,t,s,r,i){let n=r,o=s.nextSibling,a=0;for(;null!=n;){if(w(i,n,e)>0)return null;if(d(s,n))return n;if(d(o,n)&&(a++,o=o.nextSibling,a>=2))return null;n=n.nextSibling}return n}function f(e,t,s){return d(e,t)?.5+w(s,e,t):0}function g(e,t){S(t,e),!1!==t.callbacks.beforeNodeRemoved(e)&&(e.remove(),t.callbacks.afterNodeRemoved(e))}function b(e,t){return!e.deadIds.has(t)}function v(t,s,r){return(t.idMap.get(r)||e).has(s)}function S(t,s){let r=t.idMap.get(s)||e;for(const e of r)t.deadIds.add(e)}function w(t,s,r){let i=t.idMap.get(s)||e,n=0;for(const e of i)b(t,e)&&v(t,e,r)&&++n;return n}function E(e,t){let s=e.parentElement,r=e.querySelectorAll("[id]");for(const e of r){let r=e;for(;r!==s&&null!=r;){let s=t.get(r);null==s&&(s=new Set,t.set(r,s)),s.add(e.id),r=r.parentElement}}}function y(e,t){let s=new Map;return E(e,s),E(t,s),s}return{morph:function(e,r,i={}){e instanceof Document&&(e=e.documentElement),"string"==typeof r&&(r=function(e){let t=new DOMParser,s=e.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(s.match(/<\/html>/)||s.match(/<\/head>/)||s.match(/<\/body>/)){let r=t.parseFromString(e,"text/html");if(s.match(/<\/html>/))return r.generatedByIdiomorph=!0,r;{let e=r.firstChild;return e?(e.generatedByIdiomorph=!0,e):null}}{let s=t.parseFromString("<body><template>"+e+"</template></body>","text/html").body.querySelector("template").content;return s.generatedByIdiomorph=!0,s}}(r));let n=function(e){if(null==e){return document.createElement("div")}if(e.generatedByIdiomorph)return e;if(e instanceof Node){const t=document.createElement("div");return t.append(e),t}{const t=document.createElement("div");for(const s of[...e])t.append(s);return t}}(r),o=function(e,s,r){return r=function(e){let s={};return Object.assign(s,t),Object.assign(s,e),s.callbacks={},Object.assign(s.callbacks,t.callbacks),Object.assign(s.callbacks,e.callbacks),s.head={},Object.assign(s.head,t.head),Object.assign(s.head,e.head),s}(r),{target:e,newContent:s,config:r,morphStyle:r.morphStyle,ignoreActive:r.ignoreActive,ignoreActiveValue:r.ignoreActiveValue,idMap:y(e,s),deadIds:new Set,callbacks:r.callbacks,head:r.head}}(e,n,i);return s(e,n,o)},defaults:t}}();class et extends le{static renderElement(e,t){document.body&&t instanceof HTMLBodyElement?document.body.replaceWith(t):document.documentElement.appendChild(t)}get shouldRender(){return this.newSnapshot.isVisitable&&this.trackedElementsAreIdentical}get reloadReason(){return this.newSnapshot.isVisitable?this.trackedElementsAreIdentical?void 0:{reason:"tracked_element_mismatch"}:{reason:"turbo_visit_control_is_reload"}}async prepareToRender(){this.#L(),await this.mergeHead()}async render(){this.willRender&&await this.replaceBody()}finishRendering(){super.finishRendering(),this.isPreview||this.focusFirstAutofocusableElement()}get currentHeadSnapshot(){return this.currentSnapshot.headSnapshot}get newHeadSnapshot(){return this.newSnapshot.headSnapshot}get newElement(){return this.newSnapshot.element}#L(){const{documentElement:e}=this.currentSnapshot,{lang:t}=this.newSnapshot;t?e.setAttribute("lang",t):e.removeAttribute("lang")}async mergeHead(){const e=this.mergeProvisionalElements(),t=this.copyNewHeadStylesheetElements();this.copyNewHeadScriptElements(),await e,await t,this.willRender&&this.removeUnusedDynamicStylesheetElements()}async replaceBody(){await this.preservingPermanentElements((async()=>{this.activateNewBody(),await this.assignNewBody()}))}get trackedElementsAreIdentical(){return this.currentHeadSnapshot.trackedElementSignature==this.newHeadSnapshot.trackedElementSignature}async copyNewHeadStylesheetElements(){const e=[];for(const t of this.newHeadStylesheetElements)e.push(A(t)),document.head.appendChild(t);await Promise.all(e)}copyNewHeadScriptElements(){for(const e of this.newHeadScriptElements)document.head.appendChild(p(e))}removeUnusedDynamicStylesheetElements(){for(const e of this.unusedDynamicStylesheetElements)document.head.removeChild(e)}async mergeProvisionalElements(){const e=[...this.newHeadProvisionalElements];for(const t of this.currentHeadProvisionalElements)this.isCurrentElementInElementList(t,e)||document.head.removeChild(t);for(const t of e)document.head.appendChild(t)}isCurrentElementInElementList(e,t){for(const[s,r]of t.entries()){if("TITLE"==e.tagName){if("TITLE"!=r.tagName)continue;if(e.innerHTML==r.innerHTML)return t.splice(s,1),!0}if(r.isEqualNode(e))return t.splice(s,1),!0}return!1}removeCurrentHeadProvisionalElements(){for(const e of this.currentHeadProvisionalElements)document.head.removeChild(e)}copyNewHeadProvisionalElements(){for(const e of this.newHeadProvisionalElements)document.head.appendChild(e)}activateNewBody(){document.adoptNode(this.newElement),this.activateNewBodyScriptElements()}activateNewBodyScriptElements(){for(const e of this.newBodyScriptElements){const t=p(e);e.replaceWith(t)}}async assignNewBody(){await this.renderElement(this.currentElement,this.newElement)}get unusedDynamicStylesheetElements(){return this.oldHeadStylesheetElements.filter((e=>"dynamic"===e.getAttribute("data-turbo-track")))}get oldHeadStylesheetElements(){return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)}get newHeadStylesheetElements(){return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)}get newHeadScriptElements(){return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot)}get currentHeadProvisionalElements(){return this.currentHeadSnapshot.provisionalElements}get newHeadProvisionalElements(){return this.newHeadSnapshot.provisionalElements}get newBodyScriptElements(){return this.newElement.querySelectorAll("script")}}class tt extends et{async render(){this.willRender&&await this.#A()}get renderMethod(){return"morph"}async#A(){this.#T(this.currentElement,this.newElement),this.#C(),f("turbo:morph",{detail:{currentElement:this.currentElement,newElement:this.newElement}})}#T(e,t,s="outerHTML"){this.isMorphingTurboFrame=this.#P(e),Ze.morph(e,t,{morphStyle:s,callbacks:{beforeNodeAdded:this.#M,beforeNodeMorphed:this.#k,beforeAttributeUpdated:this.#F,beforeNodeRemoved:this.#I,afterNodeMorphed:this.#q}})}#M=e=>!(e.id&&e.hasAttribute("data-turbo-permanent")&&document.getElementById(e.id));#k=(e,t)=>{if(e instanceof HTMLElement){if(e.hasAttribute("data-turbo-permanent")||!this.isMorphingTurboFrame&&this.#P(e))return!1;return!f("turbo:before-morph-element",{cancelable:!0,target:e,detail:{newElement:t}}).defaultPrevented}};#F=(e,t,s)=>!f("turbo:before-morph-attribute",{cancelable:!0,target:t,detail:{attributeName:e,mutationType:s}}).defaultPrevented;#q=(e,t)=>{t instanceof HTMLElement&&f("turbo:morph-element",{target:e,detail:{newElement:t}})};#I=e=>this.#k(e);#C(){this.#H().forEach((e=>{this.#P(e)&&(this.#B(e),e.reload())}))}#B(e){e.addEventListener("turbo:before-frame-render",(e=>{e.detail.render=this.#O}),{once:!0})}#O=(e,t)=>{f("turbo:before-frame-morph",{target:e,detail:{currentElement:e,newElement:t}}),this.#T(e,t.children,"innerHTML")};#P(e){return e.src&&"morph"===e.refresh}#H(){return Array.from(document.querySelectorAll("turbo-frame[src]")).filter((e=>!e.closest("[data-turbo-permanent]")))}}class st{keys=[];snapshots={};constructor(e){this.size=e}has(e){return u(e)in this.snapshots}get(e){if(this.has(e)){const t=this.read(e);return this.touch(e),t}}put(e,t){return this.write(e,t),this.touch(e),t}clear(){this.snapshots={}}read(e){return this.snapshots[u(e)]}write(e,t){this.snapshots[u(e)]=t}touch(e){const t=u(e),s=this.keys.indexOf(t);s>-1&&this.keys.splice(s,1),this.keys.unshift(t),this.trim()}trim(){for(const e of this.keys.splice(this.size))delete this.snapshots[e]}}class rt extends se{snapshotCache=new st(10);lastRenderedLocation=new URL(location.href);forceReloaded=!1;shouldTransitionTo(e){return this.snapshot.prefersViewTransitions&&e.prefersViewTransitions}renderPage(e,t=!1,s=!0,r){const i=new(this.isPageRefresh(r)&&this.snapshot.shouldMorphPage?tt:et)(this.snapshot,e,et.renderElement,t,s);return i.shouldRender?r?.changeHistory():this.forceReloaded=!0,this.render(i)}renderError(e,t){t?.changeHistory();const s=new Ge(this.snapshot,e,Ge.renderElement,!1);return this.render(s)}clearSnapshotCache(){this.snapshotCache.clear()}async cacheSnapshot(e=this.snapshot){if(e.isCacheable){this.delegate.viewWillCacheSnapshot();const{lastRenderedLocation:t}=this;await v();const s=e.clone();return this.snapshotCache.put(t,s),s}}getCachedSnapshotForLocation(e){return this.snapshotCache.get(e)}isPageRefresh(e){return!e||this.lastRenderedLocation.pathname===e.location.pathname&&"replace"===e.action}shouldPreserveScrollPosition(e){return this.isPageRefresh(e)&&this.snapshot.shouldPreserveScrollPosition}get snapshot(){return pe.fromElement(this.element)}}class it{selector="a[data-turbo-preload]";constructor(e,t){this.delegate=e,this.snapshotCache=t}start(){"loading"===document.readyState?document.addEventListener("DOMContentLoaded",this.#N):this.preloadOnLoadLinksForView(document.body)}stop(){document.removeEventListener("DOMContentLoaded",this.#N)}preloadOnLoadLinksForView(e){for(const t of e.querySelectorAll(this.selector))this.delegate.shouldPreloadLink(t)&&this.preloadURL(t)}async preloadURL(e){const t=new URL(e.href);if(this.snapshotCache.has(t))return;const s=new U(this,x.get,t,new URLSearchParams,e);await s.perform()}prepareRequest(e){e.headers["X-Sec-Purpose"]="prefetch"}async requestSucceededWithResponse(e,t){try{const s=await t.responseHTML,r=pe.fromHTMLString(s);this.snapshotCache.put(e.url,r)}catch(e){}}requestStarted(e){}requestErrored(e){}requestFinished(e){}requestPreventedHandlingResponse(e,t){}requestFailedWithResponse(e,t){}#N=()=>{this.preloadOnLoadLinksForView(document.body)}}class nt{constructor(e){this.session=e}clear(){this.session.clearCache()}resetCacheControl(){this.#V("")}exemptPageFromCache(){this.#V("no-cache")}exemptPageFromPreview(){this.#V("no-preview")}#V(e){!function(e,t){let s=P(e);s||(s=document.createElement("meta"),s.setAttribute("name",e),document.head.appendChild(s)),s.setAttribute("content",t)}("turbo-cache-control",e)}}function ot(e){Object.defineProperties(e,at)}const at={absoluteURL:{get(){return this.toString()}}},lt=new class{navigator=new je(this);history=new Be(this);view=new rt(this,document.documentElement);adapter=new Ie(this);pageObserver=new Ke(this);cacheObserver=new qe;linkPrefetchObserver=new Oe(this,document);linkClickObserver=new ne(this,window);formSubmitObserver=new te(this,document);scrollObserver=new Qe(this);streamObserver=new Je(this);formLinkClickObserver=new oe(this,document.documentElement);frameRedirector=new He(this,document.documentElement);streamMessageRenderer=new Ye;cache=new nt(this);drive=!0;enabled=!0;progressBarDelay=500;started=!1;formMode="on";#x=150;constructor(e){this.recentRequests=e,this.preloader=new it(this,this.view.snapshotCache),this.debouncedRefresh=this.refresh,this.pageRefreshDebouncePeriod=this.pageRefreshDebouncePeriod}start(){this.started||(this.pageObserver.start(),this.cacheObserver.start(),this.linkPrefetchObserver.start(),this.formLinkClickObserver.start(),this.linkClickObserver.start(),this.formSubmitObserver.start(),this.scrollObserver.start(),this.streamObserver.start(),this.frameRedirector.start(),this.history.start(),this.preloader.start(),this.started=!0,this.enabled=!0)}disable(){this.enabled=!1}stop(){this.started&&(this.pageObserver.stop(),this.cacheObserver.stop(),this.linkPrefetchObserver.stop(),this.formLinkClickObserver.stop(),this.linkClickObserver.stop(),this.formSubmitObserver.stop(),this.scrollObserver.stop(),this.streamObserver.stop(),this.frameRedirector.stop(),this.history.stop(),this.preloader.stop(),this.started=!1)}registerAdapter(e){this.adapter=e}visit(e,t={}){const s=t.frame?document.getElementById(t.frame):null;if(s instanceof i){const r=t.action||C(s);s.delegate.proposeVisitIfNavigatedWithAction(s,r),s.src=e.toString()}else this.navigator.proposeVisit(n(e),t)}refresh(e,t){t&&this.recentRequests.has(t)||this.visit(e,{action:"replace",shouldCacheSnapshot:!1})}connectStreamSource(e){this.streamObserver.connectStreamSource(e)}disconnectStreamSource(e){this.streamObserver.disconnectStreamSource(e)}renderStreamMessage(e){this.streamMessageRenderer.render(K.wrap(e))}clearCache(){this.view.clearSnapshotCache()}setProgressBarDelay(e){this.progressBarDelay=e}setFormMode(e){this.formMode=e}get location(){return this.history.location}get restorationIdentifier(){return this.history.restorationIdentifier}get pageRefreshDebouncePeriod(){return this.#x}set pageRefreshDebouncePeriod(e){this.refresh=function(e,t){let s=null;return(...r)=>{clearTimeout(s),s=setTimeout((()=>e.apply(this,r)),t)}}(this.debouncedRefresh.bind(this),e),this.#x=e}shouldPreloadLink(e){const t=e.hasAttribute("data-turbo-method"),s=e.hasAttribute("data-turbo-stream"),r=e.getAttribute("data-turbo-frame"),n="_top"==r?null:document.getElementById(r)||k(e,"turbo-frame:not([disabled])");if(t||s||n instanceof i)return!1;{const t=new URL(e.href);return this.elementIsNavigatable(e)&&h(t,this.snapshot.rootLocation)}}historyPoppedToLocationWithRestorationIdentifierAndDirection(e,t,s){this.enabled?this.navigator.startVisit(e,t,{action:"restore",historyChanged:!0,direction:s}):this.adapter.pageInvalidated({reason:"turbo_disabled"})}scrollPositionChanged(e){this.history.updateRestorationData({scrollPosition:e})}willSubmitFormLinkToLocation(e,t){return this.elementIsNavigatable(e)&&h(t,this.snapshot.rootLocation)}submittedFormLinkToLocation(){}canPrefetchRequestToLocation(e,t){return this.elementIsNavigatable(e)&&h(t,this.snapshot.rootLocation)}willFollowLinkToLocation(e,t,s){return this.elementIsNavigatable(e)&&h(t,this.snapshot.rootLocation)&&this.applicationAllowsFollowingLinkToLocation(e,t,s)}followedLinkToLocation(e,t){const s=this.getActionForLink(e),r=e.hasAttribute("data-turbo-stream");this.visit(t.href,{action:s,acceptsStreamResponse:r})}allowsVisitingLocationWithAction(e,t){return this.locationWithActionIsSamePage(e,t)||this.applicationAllowsVisitingLocation(e)}visitProposedToLocation(e,t){ot(e),this.adapter.visitProposedToLocation(e,t)}visitStarted(e){e.acceptsStreamResponse||(R(document.documentElement),this.view.markVisitDirection(e.direction)),ot(e.location),e.silent||this.notifyApplicationAfterVisitingLocation(e.location,e.action)}visitCompleted(e){this.view.unmarkVisitDirection(),L(document.documentElement),this.notifyApplicationAfterPageLoad(e.getTimingMetrics())}locationWithActionIsSamePage(e,t){return this.navigator.locationWithActionIsSamePage(e,t)}visitScrolledToSamePageLocation(e,t){this.notifyApplicationAfterVisitingSamePageLocation(e,t)}willSubmitForm(e,t){const s=a(e,t);return this.submissionIsNavigatable(e,t)&&h(n(s),this.snapshot.rootLocation)}formSubmitted(e,t){this.navigator.submitForm(e,t)}pageBecameInteractive(){this.view.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()}pageLoaded(){this.history.assumeControlOfScrollRestoration()}pageWillUnload(){this.history.relinquishControlOfScrollRestoration()}receivedMessageFromStream(e){this.renderStreamMessage(e)}viewWillCacheSnapshot(){this.navigator.currentVisit?.silent||this.notifyApplicationBeforeCachingSnapshot()}allowsImmediateRender({element:e},t){const s=this.notifyApplicationBeforeRender(e,t),{defaultPrevented:r,detail:{render:i}}=s;return this.view.renderer&&i&&(this.view.renderer.renderElement=i),!r}viewRenderedSnapshot(e,t,s){this.view.lastRenderedLocation=this.history.location,this.notifyApplicationAfterRender(s)}preloadOnLoadLinksForView(e){this.preloader.preloadOnLoadLinksForView(e)}viewInvalidated(e){this.adapter.pageInvalidated(e)}frameLoaded(e){this.notifyApplicationAfterFrameLoad(e)}frameRendered(e,t){this.notifyApplicationAfterFrameRender(e,t)}applicationAllowsFollowingLinkToLocation(e,t,s){return!this.notifyApplicationAfterClickingLinkToLocation(e,t,s).defaultPrevented}applicationAllowsVisitingLocation(e){return!this.notifyApplicationBeforeVisitingLocation(e).defaultPrevented}notifyApplicationAfterClickingLinkToLocation(e,t,s){return f("turbo:click",{target:e,detail:{url:t.href,originalEvent:s},cancelable:!0})}notifyApplicationBeforeVisitingLocation(e){return f("turbo:before-visit",{detail:{url:e.href},cancelable:!0})}notifyApplicationAfterVisitingLocation(e,t){return f("turbo:visit",{detail:{url:e.href,action:t}})}notifyApplicationBeforeCachingSnapshot(){return f("turbo:before-cache")}notifyApplicationBeforeRender(e,t){return f("turbo:before-render",{detail:{newBody:e,...t},cancelable:!0})}notifyApplicationAfterRender(e){return f("turbo:render",{detail:{renderMethod:e}})}notifyApplicationAfterPageLoad(e={}){return f("turbo:load",{detail:{url:this.location.href,timing:e}})}notifyApplicationAfterVisitingSamePageLocation(e,t){dispatchEvent(new HashChangeEvent("hashchange",{oldURL:e.toString(),newURL:t.toString()}))}notifyApplicationAfterFrameLoad(e){return f("turbo:frame-load",{target:e})}notifyApplicationAfterFrameRender(e,t){return f("turbo:frame-render",{detail:{fetchResponse:e},target:t,cancelable:!0})}submissionIsNavigatable(e,t){if("off"==this.formMode)return!1;{const s=!t||this.elementIsNavigatable(t);return"optin"==this.formMode?s&&null!=e.closest('[data-turbo="true"]'):s&&this.elementIsNavigatable(e)}}elementIsNavigatable(e){const t=k(e,"[data-turbo]"),s=k(e,"turbo-frame");return this.drive||s?!t||"false"!=t.getAttribute("data-turbo"):!!t&&"true"==t.getAttribute("data-turbo")}getActionForLink(e){return C(e)||"advance"}get snapshot(){return this.view.snapshot}}(B),{cache:ct,navigator:ht}=lt;function dt(){lt.start()}function ut(e){lt.registerAdapter(e)}function mt(e,t){lt.visit(e,t)}function pt(e){lt.connectStreamSource(e)}function ft(e){lt.disconnectStreamSource(e)}function gt(e){lt.renderStreamMessage(e)}function bt(){console.warn("Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`"),lt.clearCache()}function vt(e){lt.setProgressBarDelay(e)}function St(e){J.confirmMethod=e}function wt(e){lt.setFormMode(e)}var Et=Object.freeze({__proto__:null,navigator:ht,session:lt,cache:ct,PageRenderer:et,PageSnapshot:pe,FrameRenderer:ce,fetch:N,start:dt,registerAdapter:ut,visit:mt,connectStreamSource:pt,disconnectStreamSource:ft,renderStreamMessage:gt,clearCache:bt,setProgressBarDelay:vt,setConfirmMethod:St,setFormMode:wt});class yt extends Error{}function Rt(e){if(null!=e){const t=document.getElementById(e);if(t instanceof i)return t}}function Lt(e,t){if(e){const r=e.getAttribute("src");if(null!=r&&null!=t&&(s=t,n(r).href==n(s).href))throw new Error(`Matching <turbo-frame id="${e.id}"> element has a source URL which references itself`);if(e.ownerDocument!==document&&(e=document.importNode(e,!0)),e instanceof i)return e.connectedCallback(),e.disconnectedCallback(),e}var s}const At={after(){this.targetElements.forEach((e=>e.parentElement?.insertBefore(this.templateContent,e.nextSibling)))},append(){this.removeDuplicateTargetChildren(),this.targetElements.forEach((e=>e.append(this.templateContent)))},before(){this.targetElements.forEach((e=>e.parentElement?.insertBefore(this.templateContent,e)))},prepend(){this.removeDuplicateTargetChildren(),this.targetElements.forEach((e=>e.prepend(this.templateContent)))},remove(){this.targetElements.forEach((e=>e.remove()))},replace(){this.targetElements.forEach((e=>e.replaceWith(this.templateContent)))},update(){this.targetElements.forEach((e=>{e.innerHTML="",e.append(this.templateContent)}))},refresh(){lt.refresh(this.baseURI,this.requestId)}};class Tt extends HTMLElement{static async renderElement(e){await e.performAction()}async connectedCallback(){try{await this.render()}catch(e){console.error(e)}finally{this.disconnect()}}async render(){return this.renderPromise??=(async()=>{const e=this.beforeRenderEvent;this.dispatchEvent(e)&&(await g(),await e.detail.render(this))})()}disconnect(){try{this.remove()}catch{}}removeDuplicateTargetChildren(){this.duplicateChildren.forEach((e=>e.remove()))}get duplicateChildren(){const e=this.targetElements.flatMap((e=>[...e.children])).filter((e=>!!e.id)),t=[...this.templateContent?.children||[]].filter((e=>!!e.id)).map((e=>e.id));return e.filter((e=>t.includes(e.id)))}get performAction(){if(this.action){const e=At[this.action];if(e)return e;this.#D("unknown action")}this.#D("action attribute is missing")}get targetElements(){return this.target?this.targetElementsById:this.targets?this.targetElementsByQuery:void this.#D("target or targets attribute is missing")}get templateContent(){return this.templateElement.content.cloneNode(!0)}get templateElement(){if(null===this.firstElementChild){const e=this.ownerDocument.createElement("template");return this.appendChild(e),e}if(this.firstElementChild instanceof HTMLTemplateElement)return this.firstElementChild;this.#D("first child element must be a <template> element")}get action(){return this.getAttribute("action")}get target(){return this.getAttribute("target")}get targets(){return this.getAttribute("targets")}get requestId(){return this.getAttribute("request-id")}#D(e){throw new Error(`${this.description}: ${e}`)}get description(){return(this.outerHTML.match(/<[^>]+>/)??[])[0]??"<turbo-stream>"}get beforeRenderEvent(){return new CustomEvent("turbo:before-stream-render",{bubbles:!0,cancelable:!0,detail:{newStream:this,render:Tt.renderElement}})}get targetElementsById(){const e=this.ownerDocument?.getElementById(this.target);return null!==e?[e]:[]}get targetElementsByQuery(){const e=this.ownerDocument?.querySelectorAll(this.targets);return 0!==e.length?Array.prototype.slice.call(e):[]}}class Ct extends HTMLElement{streamSource=null;connectedCallback(){this.streamSource=this.src.match(/^ws{1,2}:/)?new WebSocket(this.src):new EventSource(this.src),pt(this.streamSource)}disconnectedCallback(){this.streamSource&&(this.streamSource.close(),ft(this.streamSource))}get src(){return this.getAttribute("src")||""}}i.delegateConstructor=class{fetchResponseLoaded=e=>Promise.resolve();#W=null;#U=()=>{};#j=!1;#$=!1;#_=new Set;action=null;constructor(e){this.element=e,this.view=new re(this,this.element),this.appearanceObserver=new X(this,this.element),this.formLinkClickObserver=new oe(this,this.element),this.linkInterceptor=new ie(this,this.element),this.restorationIdentifier=E(),this.formSubmitObserver=new te(this,this.element)}connect(){this.#j||(this.#j=!0,this.loadingStyle==r.lazy?this.appearanceObserver.start():this.#z(),this.formLinkClickObserver.start(),this.linkInterceptor.start(),this.formSubmitObserver.start())}disconnect(){this.#j&&(this.#j=!1,this.appearanceObserver.stop(),this.formLinkClickObserver.stop(),this.linkInterceptor.stop(),this.formSubmitObserver.stop())}disabledChanged(){this.loadingStyle==r.eager&&this.#z()}sourceURLChanged(){this.#X("src")||(this.element.isConnected&&(this.complete=!1),(this.loadingStyle==r.eager||this.#$)&&this.#z())}sourceURLReloaded(){const{src:e}=this.element;return this.element.removeAttribute("complete"),this.element.src=null,this.element.src=e,this.element.loaded}loadingStyleChanged(){this.loadingStyle==r.lazy?this.appearanceObserver.start():(this.appearanceObserver.stop(),this.#z())}async#z(){this.enabled&&this.isActive&&!this.complete&&this.sourceURL&&(this.element.loaded=this.#K(n(this.sourceURL)),this.appearanceObserver.stop(),await this.element.loaded,this.#$=!0)}async loadResponse(e){(e.redirected||e.succeeded&&e.isHTML)&&(this.sourceURL=e.response.url);try{const t=await e.responseHTML;if(t){const s=S(t);pe.fromDocument(s).isVisitable?await this.#Q(e,s):await this.#Y(e)}}finally{this.fetchResponseLoaded=()=>Promise.resolve()}}elementAppearedInViewport(e){this.proposeVisitIfNavigatedWithAction(e,C(e)),this.#z()}willSubmitFormLinkToLocation(e){return this.#J(e)}submittedFormLinkToLocation(e,t,s){const r=this.#d(e);r&&s.setAttribute("data-turbo-frame",r.id)}shouldInterceptLinkClick(e,t,s){return this.#J(e)}linkClickIntercepted(e,t){this.#G(e,t)}willSubmitForm(e,t){return e.closest("turbo-frame")==this.element&&this.#J(e,t)}formSubmitted(e,t){this.formSubmission&&this.formSubmission.stop(),this.formSubmission=new J(this,e,t);const{fetchRequest:s}=this.formSubmission;this.prepareRequest(s),this.formSubmission.start()}prepareRequest(e){e.headers["Turbo-Frame"]=this.id,this.currentNavigationElement?.hasAttribute("data-turbo-stream")&&e.acceptResponseType(K.contentType)}requestStarted(e){R(this.element)}requestPreventedHandlingResponse(e,t){this.#U()}async requestSucceededWithResponse(e,t){await this.loadResponse(t),this.#U()}async requestFailedWithResponse(e,t){await this.loadResponse(t),this.#U()}requestErrored(e,t){console.error(t),this.#U()}requestFinished(e){L(this.element)}formSubmissionStarted({formElement:e}){R(e,this.#d(e))}formSubmissionSucceededWithResponse(e,t){const s=this.#d(e.formElement,e.submitter);s.delegate.proposeVisitIfNavigatedWithAction(s,C(e.submitter,e.formElement,s)),s.delegate.loadResponse(t),e.isSafe||lt.clearCache()}formSubmissionFailedWithResponse(e,t){this.element.delegate.loadResponse(t),lt.clearCache()}formSubmissionErrored(e,t){console.error(t)}formSubmissionFinished({formElement:e}){L(e,this.#d(e))}allowsImmediateRender({element:e},t){const s=f("turbo:before-frame-render",{target:this.element,detail:{newFrame:e,...t},cancelable:!0}),{defaultPrevented:r,detail:{render:i}}=s;return this.view.renderer&&i&&(this.view.renderer.renderElement=i),!r}viewRenderedSnapshot(e,t,s){}preloadOnLoadLinksForView(e){lt.preloadOnLoadLinksForView(e)}viewInvalidated(){}willRenderFrame(e,t){this.previousFrameElement=e.cloneNode(!0)}visitCachedSnapshot=({element:e})=>{const t=e.querySelector("#"+this.element.id);t&&this.previousFrameElement&&t.replaceChildren(...this.previousFrameElement.children),delete this.previousFrameElement};async#Q(e,t){const s=await this.extractForeignFrameElement(t.body);if(s){const t=new G(s),r=new ce(this,this.view.snapshot,t,ce.renderElement,!1,!1);this.view.renderPromise&&await this.view.renderPromise,this.changeHistory(),await this.view.render(r),this.complete=!0,lt.frameRendered(e,this.element),lt.frameLoaded(this.element),await this.fetchResponseLoaded(e)}else this.#Z(e)&&this.#ee(e)}async#K(e){const t=new U(this,x.get,e,new URLSearchParams,this.element);return this.#W?.cancel(),this.#W=t,new Promise((e=>{this.#U=()=>{this.#U=()=>{},this.#W=null,e()},t.perform()}))}#G(e,t,s){const r=this.#d(e,s);r.delegate.proposeVisitIfNavigatedWithAction(r,C(s,e,r)),this.#te(e,(()=>{r.src=t}))}proposeVisitIfNavigatedWithAction(e,t=null){if(this.action=t,this.action){const t=pe.fromElement(e).clone(),{visitCachedSnapshot:s}=e.delegate;e.delegate.fetchResponseLoaded=async r=>{if(e.src){const{statusCode:i,redirected:n}=r,o={response:{statusCode:i,redirected:n,responseHTML:await r.responseHTML},visitCachedSnapshot:s,willRender:!1,updateHistory:!1,restorationIdentifier:this.restorationIdentifier,snapshot:t};this.action&&(o.action=this.action),lt.visit(e.src,o)}}}}changeHistory(){if(this.action){const e=T(this.action);lt.history.update(e,n(this.element.src||""),this.restorationIdentifier)}}async#Y(e){console.warn(`The response (${e.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`),await this.#se(e.response)}#Z(e){this.element.setAttribute("complete","");const t=e.response;return!f("turbo:frame-missing",{target:this.element,detail:{response:t,visit:async(e,t)=>{e instanceof Response?this.#se(e):lt.visit(e,t)}},cancelable:!0}).defaultPrevented}#ee(e){this.view.missing(),this.#re(e)}#re(e){const t=`The response (${e.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;throw new yt(t)}async#se(e){const t=new m(e),s=await t.responseHTML,{location:r,redirected:i,statusCode:n}=t;return lt.visit(r,{response:{redirected:i,statusCode:n,responseHTML:s}})}#d(e,t){return Rt(y("data-turbo-frame",t,e)||this.element.getAttribute("target"))??this.element}async extractForeignFrameElement(e){let t;const s=CSS.escape(this.id);try{if(t=Lt(e.querySelector(`turbo-frame#${s}`),this.sourceURL),t)return t;if(t=Lt(e.querySelector(`turbo-frame[src][recurse~=${s}]`),this.sourceURL),t)return await t.loaded,await this.extractForeignFrameElement(t)}catch(e){return console.error(e),new i}return null}#ie(e,t){return h(n(a(e,t)),this.rootLocation)}#J(e,t){const s=y("data-turbo-frame",t,e)||this.element.getAttribute("target");if(e instanceof HTMLFormElement&&!this.#ie(e,t))return!1;if(!this.enabled||"_top"==s)return!1;if(s){const e=Rt(s);if(e)return!e.disabled}return!!lt.elementIsNavigatable(e)&&!(t&&!lt.elementIsNavigatable(t))}get id(){return this.element.id}get enabled(){return!this.element.disabled}get sourceURL(){if(this.element.src)return this.element.src}set sourceURL(e){this.#ne("src",(()=>{this.element.src=e??null}))}get loadingStyle(){return this.element.loading}get isLoading(){return void 0!==this.formSubmission||void 0!==this.#U()}get complete(){return this.element.hasAttribute("complete")}set complete(e){e?this.element.setAttribute("complete",""):this.element.removeAttribute("complete")}get isActive(){return this.element.isActive&&this.#j}get rootLocation(){const e=this.element.ownerDocument.querySelector('meta[name="turbo-root"]');return n(e?.content??"/")}#X(e){return this.#_.has(e)}#ne(e,t){this.#_.add(e),t(),this.#_.delete(e)}#te(e,t){this.currentNavigationElement=e,t(),delete this.currentNavigationElement}},void 0===customElements.get("turbo-frame")&&customElements.define("turbo-frame",i),void 0===customElements.get("turbo-stream")&&customElements.define("turbo-stream",Tt),void 0===customElements.get("turbo-stream-source")&&customElements.define("turbo-stream-source",Ct),(()=>{let e=document.currentScript;if(e&&!e.hasAttribute("data-turbo-suppress-warning"))for(e=e.parentElement;e;){if(e==document.body)return console.warn(w`
26
+ You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
27
+
28
+ Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
29
+
30
+ For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements
31
+
32
+ ——
33
+ Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
34
+ `,e.outerHTML);e=e.parentElement}})(),window.Turbo={...Et,StreamActions:At},dt(),e.FetchEnctype=W,e.FetchMethod=x,e.FetchRequest=U,e.FetchResponse=m,e.FrameElement=i,e.FrameLoadingStyle=r,e.FrameRenderer=ce,e.PageRenderer=et,e.PageSnapshot=pe,e.StreamActions=At,e.StreamElement=Tt,e.StreamSourceElement=Ct,e.cache=ct,e.clearCache=bt,e.connectStreamSource=pt,e.disconnectStreamSource=ft,e.fetch=N,e.fetchEnctypeFromString=D,e.fetchMethodFromString=V,e.isSafe=j,e.navigator=ht,e.registerAdapter=ut,e.renderStreamMessage=gt,e.session=lt,e.setConfirmMethod=St,e.setFormMode=wt,e.setProgressBarDelay=vt,e.start=dt,e.visit=mt,Object.defineProperty(e,"__esModule",{value:!0})}));
35
+ //# sourceMappingURL=/sm/f01bad4b4c4469311a6519ace92e657f082eaa074615553d44a41b77bc5bef45.map
@@ -1,18 +1,122 @@
1
1
  /* dispatch_policy: minimal flat dashboard */
2
2
 
3
+ :root {
4
+ /* Light theme (default) */
5
+ --dp-bg: #f6f7fa;
6
+ --dp-surface: #fff;
7
+ --dp-surface-alt: #fafbfd;
8
+ --dp-surface-th: #f1f3f7;
9
+ --dp-fg: #1a1a1a;
10
+ --dp-fg-strong: #374151;
11
+ --dp-fg-muted: #6b7280;
12
+ --dp-border: #e3e6ec;
13
+ --dp-border-soft: #eef0f4;
14
+ --dp-border-input: #cfd5df;
15
+ --dp-link: #1f4ed8;
16
+ --dp-code-bg: #eef2f7;
17
+ --dp-warn: #b45309;
18
+ --dp-paused-bg: #fffbe9;
19
+ --dp-flash-ok-bg: #e8f6ee; --dp-flash-ok-fg: #14532d; --dp-flash-ok-bd: #c7e6d3;
20
+ --dp-flash-err-bg: #fbe7e6; --dp-flash-err-fg: #7a1d1d; --dp-flash-err-bd: #f0c2bf;
21
+ --dp-hint-info-bg: #eff4ff; --dp-hint-info-bd: #1f4ed8;
22
+ --dp-hint-warn-bg: #fff7e6; --dp-hint-warn-bd: #b45309;
23
+ --dp-hint-critical-bg: #fbe7e6; --dp-hint-critical-bd: #b91c1c;
24
+ --dp-btn-ok-bg: #f1faf3; --dp-btn-ok-bd: #2f9e58; --dp-btn-ok-hover: #def0e3;
25
+ --dp-btn-warn-bg: #fdf2f1; --dp-btn-warn-bd: #d05858; --dp-btn-warn-hover: #f7dcd9;
26
+ --dp-btn-hover: #eef1f6;
27
+ }
28
+
29
+ @media (prefers-color-scheme: dark) {
30
+ :root {
31
+ --dp-bg: #0f1116;
32
+ --dp-surface: #1a1d24;
33
+ --dp-surface-alt: #222630;
34
+ --dp-surface-th: #222630;
35
+ --dp-fg: #e7e9ee;
36
+ --dp-fg-strong: #cbd0d9;
37
+ --dp-fg-muted: #9aa0aa;
38
+ --dp-border: #2a2e38;
39
+ --dp-border-soft: #2a2e38;
40
+ --dp-border-input: #3a3f4a;
41
+ --dp-link: #7aa2f7;
42
+ --dp-code-bg: #1d2330;
43
+ --dp-warn: #fbbf24;
44
+ --dp-paused-bg: #3a2f0d;
45
+ --dp-flash-ok-bg: #0f3520; --dp-flash-ok-fg: #86efac; --dp-flash-ok-bd: #1f5236;
46
+ --dp-flash-err-bg: #3a1a1a; --dp-flash-err-fg: #fca5a5; --dp-flash-err-bd: #5b2929;
47
+ --dp-hint-info-bg: #1a2434; --dp-hint-info-bd: #7aa2f7;
48
+ --dp-hint-warn-bg: #332618; --dp-hint-warn-bd: #fbbf24;
49
+ --dp-hint-critical-bg: #3a1a1a; --dp-hint-critical-bd: #f87171;
50
+ --dp-btn-ok-bg: #0f3520; --dp-btn-ok-bd: #2f9e58; --dp-btn-ok-hover: #143f29;
51
+ --dp-btn-warn-bg: #3a1a1a; --dp-btn-warn-bd: #d05858; --dp-btn-warn-hover: #4a2222;
52
+ --dp-btn-hover: #222630;
53
+ }
54
+ }
55
+
56
+ /* Explicit theme overrides win over the media query (same vars, higher specificity). */
57
+ :root[data-theme="light"] {
58
+ --dp-bg: #f6f7fa;
59
+ --dp-surface: #fff;
60
+ --dp-surface-alt: #fafbfd;
61
+ --dp-surface-th: #f1f3f7;
62
+ --dp-fg: #1a1a1a;
63
+ --dp-fg-strong: #374151;
64
+ --dp-fg-muted: #6b7280;
65
+ --dp-border: #e3e6ec;
66
+ --dp-border-soft: #eef0f4;
67
+ --dp-border-input: #cfd5df;
68
+ --dp-link: #1f4ed8;
69
+ --dp-code-bg: #eef2f7;
70
+ --dp-warn: #b45309;
71
+ --dp-paused-bg: #fffbe9;
72
+ --dp-flash-ok-bg: #e8f6ee; --dp-flash-ok-fg: #14532d; --dp-flash-ok-bd: #c7e6d3;
73
+ --dp-flash-err-bg: #fbe7e6; --dp-flash-err-fg: #7a1d1d; --dp-flash-err-bd: #f0c2bf;
74
+ --dp-hint-info-bg: #eff4ff; --dp-hint-info-bd: #1f4ed8;
75
+ --dp-hint-warn-bg: #fff7e6; --dp-hint-warn-bd: #b45309;
76
+ --dp-hint-critical-bg: #fbe7e6; --dp-hint-critical-bd: #b91c1c;
77
+ --dp-btn-ok-bg: #f1faf3; --dp-btn-ok-bd: #2f9e58; --dp-btn-ok-hover: #def0e3;
78
+ --dp-btn-warn-bg: #fdf2f1; --dp-btn-warn-bd: #d05858; --dp-btn-warn-hover: #f7dcd9;
79
+ --dp-btn-hover: #eef1f6;
80
+ }
81
+
82
+ :root[data-theme="dark"] {
83
+ --dp-bg: #0f1116;
84
+ --dp-surface: #1a1d24;
85
+ --dp-surface-alt: #222630;
86
+ --dp-surface-th: #222630;
87
+ --dp-fg: #e7e9ee;
88
+ --dp-fg-strong: #cbd0d9;
89
+ --dp-fg-muted: #9aa0aa;
90
+ --dp-border: #2a2e38;
91
+ --dp-border-soft: #2a2e38;
92
+ --dp-border-input: #3a3f4a;
93
+ --dp-link: #7aa2f7;
94
+ --dp-code-bg: #1d2330;
95
+ --dp-warn: #fbbf24;
96
+ --dp-paused-bg: #3a2f0d;
97
+ --dp-flash-ok-bg: #0f3520; --dp-flash-ok-fg: #86efac; --dp-flash-ok-bd: #1f5236;
98
+ --dp-flash-err-bg: #3a1a1a; --dp-flash-err-fg: #fca5a5; --dp-flash-err-bd: #5b2929;
99
+ --dp-hint-info-bg: #1a2434; --dp-hint-info-bd: #7aa2f7;
100
+ --dp-hint-warn-bg: #332618; --dp-hint-warn-bd: #fbbf24;
101
+ --dp-hint-critical-bg: #3a1a1a; --dp-hint-critical-bd: #f87171;
102
+ --dp-btn-ok-bg: #0f3520; --dp-btn-ok-bd: #2f9e58; --dp-btn-ok-hover: #143f29;
103
+ --dp-btn-warn-bg: #3a1a1a; --dp-btn-warn-bd: #d05858; --dp-btn-warn-hover: #4a2222;
104
+ --dp-btn-hover: #222630;
105
+ }
106
+
3
107
  * { box-sizing: border-box; }
4
108
 
5
109
  body {
6
110
  margin: 0;
7
111
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
8
112
  font-size: 14px;
9
- color: #1a1a1a;
10
- background: #f6f7fa;
113
+ color: var(--dp-fg);
114
+ background: var(--dp-bg);
11
115
  }
12
116
 
13
- a, a:visited { color: #1f4ed8; text-decoration: none; }
117
+ a, a:visited { color: var(--dp-link); text-decoration: none; }
14
118
  a:hover { text-decoration: underline; }
15
- code { font-family: "SFMono-Regular", Menlo, monospace; font-size: 13px; background: #eef2f7; padding: 1px 4px; border-radius: 3px; }
119
+ code { font-family: "SFMono-Regular", Menlo, monospace; font-size: 13px; background: var(--dp-code-bg); padding: 1px 4px; border-radius: 3px; }
16
120
 
17
121
  .dp-header {
18
122
  display: flex;
@@ -24,25 +128,57 @@ code { font-family: "SFMono-Regular", Menlo, monospace; font-size: 13px; backgro
24
128
  border-bottom: 1px solid #0e1218;
25
129
  }
26
130
  .dp-header a { color: #f6f7fa; }
27
- .dp-logo { font-weight: 600; font-size: 16px; letter-spacing: 0.4px; }
131
+ .dp-logo {
132
+ display: inline-flex;
133
+ align-items: center;
134
+ gap: 10px;
135
+ text-decoration: none;
136
+ }
137
+ .dp-logo svg { display: block; width: 44px; height: 44px; flex: 0 0 auto; }
138
+ .dp-logo-text {
139
+ font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
140
+ font-weight: 700;
141
+ font-size: 15px;
142
+ letter-spacing: -0.02em;
143
+ }
144
+ .dp-logo-sep { color: #9ca3af; }
28
145
  .dp-nav { flex: 1; }
29
146
  .dp-nav a { margin-left: 22px; opacity: 0.85; }
30
147
  .dp-nav a:hover { opacity: 1; text-decoration: none; }
31
148
 
32
- .dp-refresh {
149
+ /* Header controls: auto-refresh and theme — same visual pattern. */
150
+ .dp-controls {
151
+ display: flex; align-items: center; gap: 16px;
152
+ }
153
+ .dp-control {
33
154
  display: flex; align-items: center; gap: 6px;
34
155
  font-size: 12px; color: rgba(246, 247, 250, 0.7);
35
156
  }
36
- .dp-refresh-label {
157
+ .dp-control-label {
37
158
  margin-right: 4px; text-transform: uppercase; letter-spacing: 0.6px; font-size: 10.5px;
38
159
  }
39
- .dp-refresh-btn {
160
+ .dp-control-btn {
40
161
  background: transparent; color: rgba(246, 247, 250, 0.85);
41
162
  border: 1px solid rgba(246, 247, 250, 0.25);
42
163
  padding: 3px 9px; border-radius: 3px;
43
164
  font-size: 12px; cursor: pointer;
44
165
  font-variant-numeric: tabular-nums;
45
166
  }
167
+ .dp-control-btn:hover { background: rgba(246, 247, 250, 0.08); }
168
+ .dp-control-btn.dp-control-active {
169
+ background: rgba(246, 247, 250, 0.92); color: #1d2330;
170
+ border-color: rgba(246, 247, 250, 0.92); font-weight: 600;
171
+ }
172
+
173
+ /* Backwards-compat aliases for the auto-refresh classes used by JS / templates. */
174
+ .dp-refresh { display: flex; align-items: center; gap: 6px;
175
+ font-size: 12px; color: rgba(246, 247, 250, 0.7); }
176
+ .dp-refresh-label { margin-right: 4px; text-transform: uppercase; letter-spacing: 0.6px; font-size: 10.5px; }
177
+ .dp-refresh-btn { background: transparent; color: rgba(246, 247, 250, 0.85);
178
+ border: 1px solid rgba(246, 247, 250, 0.25);
179
+ padding: 3px 9px; border-radius: 3px;
180
+ font-size: 12px; cursor: pointer;
181
+ font-variant-numeric: tabular-nums; }
46
182
  .dp-refresh-btn:hover { background: rgba(246, 247, 250, 0.08); }
47
183
  .dp-refresh-btn.dp-refresh-active {
48
184
  background: rgba(246, 247, 250, 0.92); color: #1d2330;
@@ -53,56 +189,57 @@ code { font-family: "SFMono-Regular", Menlo, monospace; font-size: 13px; backgro
53
189
  .dp-footer {
54
190
  display: flex; gap: 24px; justify-content: space-between;
55
191
  max-width: 1200px; margin: 0 auto; padding: 12px 28px 24px;
56
- color: #6b7280; font-size: 12px;
192
+ color: var(--dp-fg-muted); font-size: 12px;
57
193
  }
58
194
 
59
195
  h1 { font-size: 22px; margin: 0 0 18px; font-weight: 600; }
60
- h2 { font-size: 15px; margin: 24px 0 10px; font-weight: 600; color: #374151; text-transform: uppercase; letter-spacing: 0.6px; }
196
+ h2 { font-size: 15px; margin: 24px 0 10px; font-weight: 600; color: var(--dp-fg-strong); text-transform: uppercase; letter-spacing: 0.6px; }
61
197
 
62
198
  .dp-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 18px; }
63
199
  .dp-stat {
64
- background: #fff; border: 1px solid #e3e6ec; border-radius: 6px;
200
+ background: var(--dp-surface); border: 1px solid var(--dp-border); border-radius: 6px;
65
201
  padding: 14px 16px; display: flex; flex-direction: column; gap: 6px;
66
202
  }
67
- .dp-stat-label { color: #6b7280; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; }
203
+ .dp-stat-label { color: var(--dp-fg-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; }
68
204
  .dp-stat-value { font-size: 22px; font-weight: 600; }
69
205
 
70
- .dp-section { background: #fff; border: 1px solid #e3e6ec; border-radius: 6px; padding: 16px 20px; margin: 14px 0; }
206
+ .dp-section { background: var(--dp-surface); border: 1px solid var(--dp-border); border-radius: 6px; padding: 16px 20px; margin: 14px 0; }
71
207
 
72
208
  .dp-table { width: 100%; border-collapse: collapse; font-size: 13px; }
73
- .dp-table th, .dp-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #eef0f4; }
74
- .dp-table th { background: #f1f3f7; font-weight: 600; color: #374151; }
75
- .dp-table tr:hover td { background: #fafbfd; }
209
+ .dp-table th, .dp-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--dp-border-soft); }
210
+ .dp-table th { background: var(--dp-surface-th); font-weight: 600; color: var(--dp-fg-strong); }
211
+ .dp-table tr:hover td { background: var(--dp-surface-alt); }
76
212
  .dp-num { text-align: right; font-variant-numeric: tabular-nums; }
77
213
 
78
- .dp-empty { color: #6b7280; font-style: italic; padding: 6px 0; }
214
+ .dp-empty { color: var(--dp-fg-muted); font-style: italic; padding: 6px 0; }
79
215
 
80
216
  .dp-flash { padding: 10px 16px; border-radius: 0; font-size: 13px; }
81
- .dp-flash-ok { background: #e8f6ee; color: #14532d; border-bottom: 1px solid #c7e6d3; }
82
- .dp-flash-err { background: #fbe7e6; color: #7a1d1d; border-bottom: 1px solid #f0c2bf; }
217
+ .dp-flash-ok { background: var(--dp-flash-ok-bg); color: var(--dp-flash-ok-fg); border-bottom: 1px solid var(--dp-flash-ok-bd); }
218
+ .dp-flash-err { background: var(--dp-flash-err-bg); color: var(--dp-flash-err-fg); border-bottom: 1px solid var(--dp-flash-err-bd); }
83
219
 
84
- .dp-warn { color: #b45309; }
85
- .dp-row-paused td { background: #fffbe9; }
220
+ .dp-warn { color: var(--dp-warn); }
221
+ .dp-row-paused td { background: var(--dp-paused-bg); }
86
222
 
87
223
  .dp-list { margin: 0; padding-left: 20px; }
88
224
  .dp-list li { margin: 3px 0; }
89
225
 
90
- .dp-link { color: #1f4ed8; }
226
+ .dp-link { color: var(--dp-link); }
91
227
 
92
228
  .dp-form-inline { display: inline-block; margin-right: 6px; }
93
229
  .dp-btn {
94
- background: #fff; border: 1px solid #cfd5df; padding: 6px 12px;
95
- font-size: 13px; border-radius: 4px; color: #1a1a1a; cursor: pointer;
230
+ background: var(--dp-surface); border: 1px solid var(--dp-border-input); padding: 6px 12px;
231
+ font-size: 13px; border-radius: 4px; color: var(--dp-fg); cursor: pointer;
96
232
  }
97
- .dp-btn:hover { background: #eef1f6; }
98
- .dp-btn-ok { border-color: #2f9e58; color: #14532d; background: #f1faf3; }
99
- .dp-btn-ok:hover { background: #def0e3; }
100
- .dp-btn-warn { border-color: #d05858; color: #7a1d1d; background: #fdf2f1; }
101
- .dp-btn-warn:hover { background: #f7dcd9; }
233
+ .dp-btn:hover { background: var(--dp-btn-hover); }
234
+ .dp-btn-ok { border-color: var(--dp-btn-ok-bd); color: var(--dp-flash-ok-fg); background: var(--dp-btn-ok-bg); }
235
+ .dp-btn-ok:hover { background: var(--dp-btn-ok-hover); }
236
+ .dp-btn-warn { border-color: var(--dp-btn-warn-bd); color: var(--dp-flash-err-fg); background: var(--dp-btn-warn-bg); }
237
+ .dp-btn-warn:hover { background: var(--dp-btn-warn-hover); }
102
238
 
103
239
  .dp-input {
104
- padding: 6px 10px; border: 1px solid #cfd5df; border-radius: 4px;
240
+ padding: 6px 10px; border: 1px solid var(--dp-border-input); border-radius: 4px;
105
241
  font-size: 13px; min-width: 240px;
242
+ background: var(--dp-surface); color: var(--dp-fg);
106
243
  }
107
244
  .dp-search-form { margin-bottom: 14px; }
108
245
 
@@ -113,14 +250,14 @@ h2 { font-size: 15px; margin: 24px 0 10px; font-weight: 600; color: #374151; tex
113
250
  }
114
251
 
115
252
  .dp-hint {
116
- font-size: 12.5px; color: #6b7280; margin-top: 8px;
117
- border-left: 3px solid #cfd5df; padding: 6px 12px; background: #fafbfd;
253
+ font-size: 12.5px; color: var(--dp-fg-muted); margin-top: 8px;
254
+ border-left: 3px solid var(--dp-border-input); padding: 6px 12px; background: var(--dp-surface-alt);
118
255
  border-radius: 0 4px 4px 0;
119
256
  }
120
- .dp-hint code { background: #eef2f7; }
257
+ .dp-hint code { background: var(--dp-code-bg); }
121
258
 
122
259
  .dp-spark {
123
- margin-top: 10px; font-size: 13px; color: #374151;
260
+ margin-top: 10px; font-size: 13px; color: var(--dp-fg-strong);
124
261
  }
125
262
  .dp-spark code {
126
263
  font-family: "SFMono-Regular", Menlo, monospace; font-size: 16px;
@@ -130,28 +267,28 @@ h2 { font-size: 15px; margin: 24px 0 10px; font-weight: 600; color: #374151; tex
130
267
  .dp-hint-list { padding-left: 0; list-style: none; }
131
268
  .dp-hint-list li {
132
269
  margin: 8px 0; padding: 10px 14px;
133
- border-left: 4px solid #cfd5df; background: #fafbfd;
270
+ border-left: 4px solid var(--dp-border-input); background: var(--dp-surface-alt);
134
271
  border-radius: 0 4px 4px 0;
135
272
  font-size: 13px; line-height: 1.5;
136
273
  }
137
- .dp-hint-list li.dp-hint-info { border-left-color: #1f4ed8; background: #eff4ff; }
138
- .dp-hint-list li.dp-hint-warn { border-left-color: #b45309; background: #fff7e6; }
139
- .dp-hint-list li.dp-hint-critical { border-left-color: #b91c1c; background: #fbe7e6; }
274
+ .dp-hint-list li.dp-hint-info { border-left-color: var(--dp-hint-info-bd); background: var(--dp-hint-info-bg); }
275
+ .dp-hint-list li.dp-hint-warn { border-left-color: var(--dp-hint-warn-bd); background: var(--dp-hint-warn-bg); }
276
+ .dp-hint-list li.dp-hint-critical { border-left-color: var(--dp-hint-critical-bd); background: var(--dp-hint-critical-bg); }
140
277
  .dp-hint-badge {
141
278
  display: inline-block; margin-right: 8px; padding: 1px 7px;
142
279
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.5px;
143
- border-radius: 3px; color: #fff; background: #6b7280;
280
+ border-radius: 3px; color: #fff; background: var(--dp-fg-muted);
144
281
  vertical-align: 1px;
145
282
  }
146
- .dp-hint-info .dp-hint-badge { background: #1f4ed8; }
147
- .dp-hint-warn .dp-hint-badge { background: #b45309; }
148
- .dp-hint-critical .dp-hint-badge { background: #b91c1c; }
283
+ .dp-hint-info .dp-hint-badge { background: var(--dp-hint-info-bd); }
284
+ .dp-hint-warn .dp-hint-badge { background: var(--dp-hint-warn-bd); }
285
+ .dp-hint-critical .dp-hint-badge { background: var(--dp-hint-critical-bd); }
149
286
 
150
287
  .dp-pagination {
151
288
  display: flex; justify-content: space-between; align-items: center;
152
289
  padding: 14px 0; gap: 12px; flex-wrap: wrap;
153
290
  }
154
- .dp-pagination-info { color: #6b7280; font-size: 13px; font-variant-numeric: tabular-nums; }
291
+ .dp-pagination-info { color: var(--dp-fg-muted); font-size: 13px; font-variant-numeric: tabular-nums; }
155
292
  .dp-pagination-nav { display: flex; gap: 6px; }
156
293
  .dp-pagination-nav .dp-btn { padding: 4px 10px; font-size: 12.5px; text-decoration: none; }
157
294
  .dp-btn-disabled { opacity: 0.4; cursor: default; pointer-events: none; }
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DispatchPolicy
4
+ # Serves vendored static assets at content-addressed URLs so browsers
5
+ # can cache them forever. The digest is part of the URL, not a query
6
+ # string, so caches keyed on path alone still bust on upgrade.
7
+ class AssetsController < ApplicationController
8
+ skip_forgery_protection
9
+
10
+ def turbo
11
+ serve(Assets::TURBO_BODY, Assets::TURBO_DIGEST, "application/javascript")
12
+ end
13
+
14
+ def logo
15
+ serve(Assets::LOGO_SMALL_BODY, Assets::LOGO_SMALL_DIGEST, "image/svg+xml")
16
+ end
17
+
18
+ private
19
+
20
+ def serve(body, digest, content_type)
21
+ if params[:digest] != digest
22
+ head :not_found
23
+ return
24
+ end
25
+
26
+ response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
27
+ response.headers["ETag"] = %("#{digest}")
28
+ send_data body, type: content_type, disposition: "inline"
29
+ end
30
+ end
31
+ end
@@ -4,14 +4,27 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <title>dispatch_policy</title>
7
+ <link rel="icon" type="image/svg+xml" href="<%= logo_asset_path(digest: DispatchPolicy::Assets::LOGO_SMALL_DIGEST) %>">
7
8
  <%= csrf_meta_tags %>
8
9
  <%# Same-URL Turbo.visit (used by the auto-refresh) is treated as a "page %>
9
10
  <%# refresh"; with these two meta tags Turbo morphs the body in place and %>
10
11
  <%# preserves scroll position. %>
11
12
  <meta name="turbo-refresh-method" content="morph">
12
13
  <meta name="turbo-refresh-scroll" content="preserve">
14
+ <%# Apply the stored theme synchronously before the stylesheet renders, %>
15
+ <%# otherwise users with explicit dark mode would see a light-mode flash. %>
16
+ <script>
17
+ (function () {
18
+ try {
19
+ var t = localStorage.getItem("dispatch_policy:theme");
20
+ if (t === "dark" || t === "light") {
21
+ document.documentElement.setAttribute("data-theme", t);
22
+ }
23
+ } catch (e) { /* localStorage unavailable; fall through to auto */ }
24
+ })();
25
+ </script>
13
26
  <style><%= DispatchPolicy::Engine.root.join("app/assets/stylesheets/dispatch_policy/application.css").read.html_safe %></style>
14
- <script src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.4/dist/turbo.es2017-umd.min.js"></script>
27
+ <script src="<%= turbo_asset_path(digest: DispatchPolicy::Assets::TURBO_DIGEST) %>"></script>
15
28
  <script>
16
29
  (function () {
17
30
  var KEY = "dispatch_policy:refresh-interval";
@@ -90,24 +103,87 @@
90
103
  }
91
104
  });
92
105
  })();
106
+
107
+ // Theme controls (auto / light / dark). Persists to localStorage so
108
+ // it survives across sessions; the early script in <head> applies
109
+ // the stored theme before paint to avoid FOUC.
110
+ (function () {
111
+ var KEY = "dispatch_policy:theme";
112
+
113
+ function getTheme() {
114
+ try { return localStorage.getItem(KEY) || "auto"; }
115
+ catch (e) { return "auto"; }
116
+ }
117
+
118
+ function applyTheme(theme) {
119
+ if (theme === "light" || theme === "dark") {
120
+ document.documentElement.setAttribute("data-theme", theme);
121
+ } else {
122
+ document.documentElement.removeAttribute("data-theme");
123
+ }
124
+ }
125
+
126
+ function setTheme(theme) {
127
+ try { localStorage.setItem(KEY, theme); } catch (e) { /* ignore */ }
128
+ applyTheme(theme);
129
+ syncControls();
130
+ }
131
+
132
+ function syncControls() {
133
+ var current = getTheme();
134
+ document.querySelectorAll("[data-dp-theme]").forEach(function (btn) {
135
+ if (btn.getAttribute("data-dp-theme") === current) {
136
+ btn.classList.add("dp-control-active");
137
+ } else {
138
+ btn.classList.remove("dp-control-active");
139
+ }
140
+ });
141
+ }
142
+
143
+ function bindControls() {
144
+ document.querySelectorAll("[data-dp-theme]").forEach(function (btn) {
145
+ if (btn.dataset.bound) return;
146
+ btn.dataset.bound = "1";
147
+ btn.addEventListener("click", function (e) {
148
+ e.preventDefault();
149
+ setTheme(btn.getAttribute("data-dp-theme"));
150
+ });
151
+ });
152
+ syncControls();
153
+ }
154
+
155
+ document.addEventListener("DOMContentLoaded", bindControls);
156
+ document.addEventListener("turbo:load", bindControls);
157
+ })();
93
158
  </script>
94
159
  </head>
95
160
  <body>
96
161
  <header class="dp-header">
97
162
  <div class="dp-brand">
98
- <%= link_to "dispatch_policy", root_path, class: "dp-logo" %>
163
+ <%= link_to root_path, class: "dp-logo" do %>
164
+ <%= DispatchPolicy::Assets::LOGO_LARGE_BODY.html_safe %>
165
+ <span class="dp-logo-text">dispatch<span class="dp-logo-sep">_</span>policy</span>
166
+ <% end %>
99
167
  </div>
100
168
  <nav class="dp-nav">
101
169
  <%= link_to "Dashboard", root_path %>
102
170
  <%= link_to "Policies", policies_path %>
103
171
  <%= link_to "Partitions", partitions_path %>
104
172
  </nav>
105
- <div class="dp-refresh">
106
- <span class="dp-refresh-label">Auto-refresh</span>
107
- <button type="button" class="dp-refresh-btn" data-dp-refresh="0">off</button>
108
- <button type="button" class="dp-refresh-btn" data-dp-refresh="2">2s</button>
109
- <button type="button" class="dp-refresh-btn" data-dp-refresh="5">5s</button>
110
- <button type="button" class="dp-refresh-btn" data-dp-refresh="10">10s</button>
173
+ <div class="dp-controls">
174
+ <div class="dp-refresh">
175
+ <span class="dp-refresh-label">Auto-refresh</span>
176
+ <button type="button" class="dp-refresh-btn" data-dp-refresh="0">off</button>
177
+ <button type="button" class="dp-refresh-btn" data-dp-refresh="2">2s</button>
178
+ <button type="button" class="dp-refresh-btn" data-dp-refresh="5">5s</button>
179
+ <button type="button" class="dp-refresh-btn" data-dp-refresh="10">10s</button>
180
+ </div>
181
+ <div class="dp-control">
182
+ <span class="dp-control-label">Theme</span>
183
+ <button type="button" class="dp-control-btn" data-dp-theme="auto">auto</button>
184
+ <button type="button" class="dp-control-btn" data-dp-theme="light">light</button>
185
+ <button type="button" class="dp-control-btn" data-dp-theme="dark">dark</button>
186
+ </div>
111
187
  </div>
112
188
  </header>
113
189
  <% if flash[:notice] %><div class="dp-flash dp-flash-ok"><%= flash[:notice] %></div><% end %>
data/config/routes.rb CHANGED
@@ -19,4 +19,7 @@ DispatchPolicy::Engine.routes.draw do
19
19
  end
20
20
 
21
21
  resources :staged_jobs, only: %i[show]
22
+
23
+ get "assets/turbo-:digest.js", to: "assets#turbo", as: :turbo_asset
24
+ get "assets/logo-:digest.svg", to: "assets#logo", as: :logo_asset
22
25
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ require "pathname"
5
+
6
+ module DispatchPolicy
7
+ # Vendored static assets served by AssetsController. Bodies are read
8
+ # once at boot and the digest is embedded in the URL so the response
9
+ # can be marked `Cache-Control: immutable` — bumping the vendored file
10
+ # produces a new digest and the host's browsers refetch automatically.
11
+ #
12
+ # To upgrade Turbo (current: 8.0.4), overwrite the file from the same
13
+ # CDN/version pair the rest of the Hotwire ecosystem uses:
14
+ #
15
+ # curl -fsSL https://cdn.jsdelivr.net/npm/@hotwired/turbo@<VERSION>/dist/turbo.es2017-umd.min.js \
16
+ # -o app/assets/javascripts/dispatch_policy/turbo.es2017-umd.min.js
17
+ #
18
+ # No other code change is required — TURBO_DIGEST is content-addressed.
19
+ module Assets
20
+ JS_ROOT = Pathname.new(File.expand_path("../../app/assets/javascripts/dispatch_policy", __dir__))
21
+ IMAGE_ROOT = Pathname.new(File.expand_path("../../app/assets/images/dispatch_policy", __dir__))
22
+
23
+ TURBO_BODY = JS_ROOT.join("turbo.es2017-umd.min.js").read.freeze
24
+ TURBO_DIGEST = Digest::SHA1.hexdigest(TURBO_BODY)[0, 12].freeze
25
+
26
+ # The "large" mark (≥ 48px) is used in the admin header — three
27
+ # chevrons with the rightmost one carrying state color via
28
+ # `currentColor`. The "small" mark (≤ 32px) is used as the SVG
29
+ # favicon, where the lanes get lost at downsampling. Both are
30
+ # themable: wrapping with `style="color: …"` swaps the state color
31
+ # (ok/info/neutral/warn/error).
32
+ LOGO_LARGE_BODY = IMAGE_ROOT.join("logo-large.svg").read.freeze
33
+ LOGO_LARGE_DIGEST = Digest::SHA1.hexdigest(LOGO_LARGE_BODY)[0, 12].freeze
34
+
35
+ LOGO_SMALL_BODY = IMAGE_ROOT.join("logo-small.svg").read.freeze
36
+ LOGO_SMALL_DIGEST = Digest::SHA1.hexdigest(LOGO_SMALL_BODY)[0, 12].freeze
37
+ end
38
+ end
@@ -5,8 +5,9 @@ require "rails/engine"
5
5
  module DispatchPolicy
6
6
  # Mounted by the host app. Views, controllers, and AR models live under
7
7
  # `app/`; the layout inlines the engine CSS by reading
8
- # `app/assets/stylesheets/dispatch_policy/application.css` at render time,
9
- # so no asset pipeline integration is required.
8
+ # `app/assets/stylesheets/dispatch_policy/application.css` at render
9
+ # time, and serves the vendored Turbo bundle through `AssetsController`
10
+ # at a content-addressed URL — no asset pipeline integration required.
10
11
  class Engine < ::Rails::Engine
11
12
  isolate_namespace DispatchPolicy
12
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DispatchPolicy
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -26,6 +26,7 @@ require_relative "dispatch_policy/tick"
26
26
  require_relative "dispatch_policy/tick_loop"
27
27
  require_relative "dispatch_policy/job_extension"
28
28
  require_relative "dispatch_policy/operator_hints"
29
+ require_relative "dispatch_policy/assets"
29
30
 
30
31
  module DispatchPolicy
31
32
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dispatch_policy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - José Galisteo
@@ -149,6 +149,34 @@ dependencies:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: capybara
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '3.40'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '3.40'
166
+ - !ruby/object:Gem::Dependency
167
+ name: selenium-webdriver
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '4.20'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '4.20'
152
180
  description: Stages perform_later into a dedicated table, runs a tick loop that admits
153
181
  jobs through declared gates (throttle, concurrency), then forwards survivors to
154
182
  the real ActiveJob adapter. Embedded as a periodic job. Compatible with good_job
@@ -159,10 +187,15 @@ executables: []
159
187
  extensions: []
160
188
  extra_rdoc_files: []
161
189
  files:
190
+ - CHANGELOG.md
162
191
  - MIT-LICENSE
163
192
  - README.md
193
+ - app/assets/images/dispatch_policy/logo-large.svg
194
+ - app/assets/images/dispatch_policy/logo-small.svg
195
+ - app/assets/javascripts/dispatch_policy/turbo.es2017-umd.min.js
164
196
  - app/assets/stylesheets/dispatch_policy/application.css
165
197
  - app/controllers/dispatch_policy/application_controller.rb
198
+ - app/controllers/dispatch_policy/assets_controller.rb
166
199
  - app/controllers/dispatch_policy/dashboard_controller.rb
167
200
  - app/controllers/dispatch_policy/partitions_controller.rb
168
201
  - app/controllers/dispatch_policy/policies_controller.rb
@@ -186,6 +219,7 @@ files:
186
219
  - config/routes.rb
187
220
  - db/migrate/20260501000001_create_dispatch_policy_tables.rb
188
221
  - lib/dispatch_policy.rb
222
+ - lib/dispatch_policy/assets.rb
189
223
  - lib/dispatch_policy/bypass.rb
190
224
  - lib/dispatch_policy/config.rb
191
225
  - lib/dispatch_policy/context.rb