codex-lb 0.3.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

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.
app/static/index.html CHANGED
@@ -9,496 +9,533 @@
9
9
  <link rel="icon"
10
10
  href="data:image/svg+xml,%3Csvg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; fill=&quot;none&quot; viewBox=&quot;0 0 32 32&quot;%3E%3Cpath stroke=&quot;%23000&quot; stroke-linecap=&quot;round&quot; stroke-width=&quot;2.484&quot; d=&quot;M22.356 19.797H17.17M9.662 12.29l1.979 3.576a.511.511 0 0 1-.005.504l-1.974 3.409M30.758 16c0 8.15-6.607 14.758-14.758 14.758-8.15 0-14.758-6.607-14.758-14.758C1.242 7.85 7.85 1.242 16 1.242c8.15 0 14.758 6.608 14.758 14.758Z&quot;/%3E%3C/svg%3E"
11
11
  type="image/svg+xml">
12
- <link rel="stylesheet" href="/dashboard/7.css">
13
12
  <link rel="stylesheet" href="/dashboard/index.css">
14
13
  </head>
15
14
 
16
- <body class="has-scrollbar" x-data="feApp" x-init="init()" x-cloak>
15
+ <body class="has-scrollbar" x-data="feApp" :data-theme="theme" x-init="init()" x-cloak>
17
16
  <div class="app-shell">
18
- <div class="window glass active">
19
- <div class="title-bar">
20
- <div class="title-bar-text" x-text="currentPage.title"></div>
21
- <div class="title-bar-controls">
22
- <button aria-label="Minimize"></button>
23
- <button aria-label="Maximize"></button>
24
- <button aria-label="Close"></button>
17
+ <header class="main-header">
18
+ <div class="header-content">
19
+ <h1 class="app-logo">Codex Load Balancer</h1>
20
+ <div class="header-actions">
21
+ <button class="theme-toggle" @click="toggleTheme" aria-label="Toggle theme">
22
+ <svg x-show="theme === 'dark'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
23
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
24
+ <circle cx="12" cy="12" r="4" />
25
+ <path d="M12 2v2" />
26
+ <path d="M12 20v2" />
27
+ <path d="m4.93 4.93 1.41 1.41" />
28
+ <path d="m17.66 17.66 1.41 1.41" />
29
+ <path d="M2 12h2" />
30
+ <path d="M20 12h2" />
31
+ <path d="m6.34 17.66-1.41 1.41" />
32
+ <path d="m19.07 4.93-1.41 1.41" />
33
+ </svg>
34
+ <svg x-show="theme === 'light'" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
35
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
36
+ stroke-linejoin="round">
37
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
38
+ </svg>
39
+ </button>
25
40
  </div>
26
41
  </div>
27
- <div class="window-body has-space">
28
- <div class="loading-overlay" x-show="isLoading" x-transition.opacity>
29
- <div class="loading-panel" role="status" aria-live="polite">
30
- <span class="spinner animate" aria-hidden="true"></span>
31
- <div>Loading dashboard...</div>
32
- </div>
42
+ </header>
43
+ <main class="main-content">
44
+ <div class="loading-overlay" x-show="isLoading" x-transition.opacity>
45
+ <div class="loading-panel" role="status" aria-live="polite">
46
+ <span class="spinner animate" aria-hidden="true"></span>
47
+ <div>Loading dashboard...</div>
33
48
  </div>
34
- <section class="tabs" aria-label="Codex FE tabs">
35
- <menu role="tablist" aria-label="Codex FE tabs">
36
- <template x-for="page in pages" :key="page.id">
37
- <button role="tab" type="button" :aria-controls="page.tabId" :aria-selected="view === page.id"
38
- :tabindex="view === page.id ? 0 : -1" @click="setView(page.id)" @focus="setView(page.id)">
39
- <span x-text="page.label"></span>
40
- </button>
41
- </template>
42
- </menu>
49
+ </div>
50
+ <section class="tabs" aria-label="Codex FE tabs">
51
+ <menu role="tablist" aria-label="Codex FE tabs">
52
+ <template x-for="page in pages" :key="page.id">
53
+ <button role="tab" type="button" :aria-controls="page.tabId" :aria-selected="view === page.id"
54
+ :tabindex="view === page.id ? 0 : -1" @click="setView(page.id)" @focus="setView(page.id)">
55
+ <span x-text="page.label"></span>
56
+ </button>
57
+ </template>
58
+ </menu>
43
59
 
44
- <article role="tabpanel" id="tab-dashboard" :hidden="view !== 'dashboard'">
45
- <div class="hero-banner">
46
- <h2>Quota remaining and routing health</h2>
47
- <p>Live summary of remaining quota for Codex traffic routed through the ChatGPT-style backend. Account
48
- status and load
49
- balancing signals are surfaced here.</p>
50
- <div class="badge-row">
51
- <template x-for="badge in dashboard.badges" :key="badge.label">
52
- <span class="status-pill" :class="badge.status" x-text="badge.label"></span>
53
- </template>
54
- </div>
60
+ <article role="tabpanel" id="tab-dashboard" :hidden="view !== 'dashboard'">
61
+ <div class="hero-banner">
62
+ <h2>Quota remaining and routing health</h2>
63
+ <p>Live summary of remaining quota for Codex traffic routed through the ChatGPT-style backend. Account
64
+ status and load
65
+ balancing signals are surfaced here.</p>
66
+ <div class="badge-row">
67
+ <template x-for="badge in dashboard.badges" :key="badge.label">
68
+ <span class="status-pill" :class="badge.status" x-text="badge.label"></span>
69
+ </template>
55
70
  </div>
71
+ </div>
56
72
 
57
- <div class="section-grid" style="margin-top: 12px">
58
- <template x-for="stat in dashboard.stats" :key="stat.title">
59
- <div class="panel">
60
- <h3 x-text="stat.title"></h3>
61
- <div class="stat-value" x-text="stat.value"></div>
62
- <div class="stat-meta" x-text="stat.meta"></div>
73
+ <div class="section-grid" style="margin-top: 12px">
74
+ <template x-for="stat in dashboard.stats" :key="stat.title">
75
+ <div class="panel">
76
+ <h3 x-text="stat.title"></h3>
77
+ <div class="stat-value" x-text="stat.value"></div>
78
+ <div class="stat-meta" x-text="stat.meta"></div>
79
+ </div>
80
+ </template>
81
+ </div>
82
+
83
+ <div class="two-column two-column--equal" style="margin-top: 12px">
84
+ <template x-for="donut in dashboard.donuts" :key="donut.title">
85
+ <div class="panel">
86
+ <h3 x-text="donut.title"></h3>
87
+ <div class="donut-wrap">
88
+ <div class="donut" :style="{ '--donut-data': donut.gradient }">
89
+ <div class="donut-center">
90
+ <strong x-text="donut.range"></strong>
91
+ <div x-text="donut.total"></div>
92
+ </div>
93
+ </div>
94
+ <ul class="legend">
95
+ <template x-for="item in donut.items" :key="item.label">
96
+ <li>
97
+ <span class="legend-label">
98
+ <i :style="{ '--legend-color': item.color }"></i>
99
+ <span class="legend-label-text" x-text="item.label" :title="item.fullLabel"></span>
100
+ </span>
101
+ <span class="legend-detail">
102
+ <span class="legend-detail-label" x-text="item.detailLabel"></span>
103
+ <span class="legend-detail-value" :class="item.valueClass" x-text="item.detailValue"></span>
104
+ </span>
105
+ </li>
106
+ </template>
107
+ </ul>
63
108
  </div>
64
- </template>
65
- </div>
109
+ </div>
110
+ </template>
111
+ </div>
66
112
 
67
- <div class="two-column two-column--equal" style="margin-top: 12px">
68
- <template x-for="donut in dashboard.donuts" :key="donut.title">
69
- <div class="panel">
70
- <h3 x-text="donut.title"></h3>
71
- <div class="donut-wrap">
72
- <div class="donut" :style="{ '--donut-data': donut.gradient }">
73
- <div class="donut-center">
74
- <strong x-text="donut.range"></strong>
75
- <div x-text="donut.total"></div>
76
- </div>
113
+ <section style="margin-top: 12px">
114
+ <h3>Account status</h3>
115
+ <div class="account-grid">
116
+ <template x-for="card in dashboard.accountCards" :key="card.email">
117
+ <div class="account-card">
118
+ <header>
119
+ <div>
120
+ <div x-text="`${card.email} (${card.plan})`" :title="`${card.email} (${card.plan})`"></div>
121
+ <div class="account-id" x-text="card.accountId"></div>
77
122
  </div>
78
- <ul class="legend">
79
- <template x-for="item in donut.items" :key="item.label">
80
- <li>
81
- <span class="legend-label">
82
- <i :style="{ '--legend-color': item.color }"></i>
83
- <span class="legend-label-text" x-text="item.label" :title="item.label"></span>
84
- </span>
85
- <span class="legend-detail">
86
- <span class="legend-detail-label" x-text="item.detailLabel"></span>
87
- <span class="legend-detail-value" x-text="item.detailValue"></span>
88
- </span>
89
- </li>
90
- </template>
91
- </ul>
123
+ <span class="status-pill" :class="card.status.class" x-text="card.status.label"></span>
124
+ </header>
125
+ <div class="progress-row">
126
+ <label
127
+ x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
128
+ <template x-if="card.marquee">
129
+ <div role="progressbar" class="marquee"></div>
130
+ </template>
131
+ <template x-if="!card.marquee">
132
+ <div role="progressbar" :class="card.progressClass" aria-valuemin="0" aria-valuemax="100"
133
+ :aria-valuenow="card.remaining">
134
+ <div :style="{ width: `${card.remaining}%` }"></div>
135
+ </div>
136
+ </template>
137
+ <span class="text-muted" x-text="card.remainingText"></span>
138
+ </div>
139
+ <div class="text-muted" x-text="card.meta"></div>
140
+ <div class="account-actions">
141
+ <template x-for="action in card.actions" :key="action.label">
142
+ <button x-text="action.label" @click="handleAccountAction(action, card)"></button>
143
+ </template>
92
144
  </div>
93
145
  </div>
94
146
  </template>
95
147
  </div>
148
+ </section>
96
149
 
97
- <section style="margin-top: 12px">
98
- <h3>Account status</h3>
99
- <div class="account-grid">
100
- <template x-for="card in dashboard.accountCards" :key="card.email">
101
- <div class="account-card">
102
- <header>
103
- <div>
104
- <div x-text="`${card.email} (${card.plan})`"></div>
105
- <div class="account-id" x-text="card.accountId"></div>
106
- </div>
107
- <span class="status-pill" :class="card.status.class" x-text="card.status.label"></span>
108
- </header>
109
- <div class="progress-row">
110
- <label
111
- x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
112
- <template x-if="card.marquee">
113
- <div role="progressbar" class="marquee"></div>
114
- </template>
115
- <template x-if="!card.marquee">
116
- <div role="progressbar" :class="card.progressClass" aria-valuemin="0" aria-valuemax="100"
117
- :aria-valuenow="card.remaining">
118
- <div :style="{ width: `${card.remaining}%` }"></div>
119
- </div>
120
- </template>
121
- <span class="text-muted" x-text="card.remainingText"></span>
122
- </div>
123
- <div class="text-muted" x-text="card.meta"></div>
124
- <div class="account-actions">
125
- <template x-for="action in card.actions" :key="action.label">
126
- <button x-text="action.label" @click="handleAccountAction(action, card)"></button>
127
- </template>
128
- </div>
129
- </div>
130
- </template>
131
- </div>
132
- </section>
150
+ <div class="panel" style="margin-top: 12px">
151
+ <h3>Recent requests</h3>
152
+ <div class="table-wrap table-wrap--requests table-wrap--column-lines has-scrollbar">
153
+ <table>
154
+ <thead>
155
+ <tr>
156
+ <th class="highlighted indicator" style="width: 15%">Time</th>
157
+ <th style="width: 25%">Account</th>
158
+ <th style="width: 15%">Model</th>
159
+ <th style="width: 10%">Status</th>
160
+ <th style="width: 15%">Error</th>
161
+ <th style="text-align: right; width: 10%">Tokens</th>
162
+ <th style="text-align: right; width: 10%">Cost</th>
163
+ </tr>
164
+ </thead>
165
+ <tbody>
166
+ <template x-for="request in dashboard.requests" :key="request.key">
167
+ <tr>
168
+ <td x-text="request.time"></td>
169
+ <td class="cell-truncate" x-text="request.account" :title="request.account"></td>
170
+ <td class="cell-truncate" x-text="request.model" :title="request.model"></td>
171
+ <td><span class="status-pill" :class="request.status.class" x-text="request.status.label"></span>
172
+ </td>
173
+ <td class="cell-truncate" x-text="request.error" :title="request.errorTitle || ''"></td>
174
+ <td style="text-align: right" x-text="request.tokens"></td>
175
+ <td style="text-align: right" x-text="request.cost"></td>
176
+ </tr>
177
+ </template>
178
+ </tbody>
179
+ </table>
180
+ </div>
181
+ </div>
182
+ </article>
133
183
 
134
- <div class="panel" style="margin-top: 12px">
135
- <h3>Recent requests</h3>
136
- <div class="table-wrap table-wrap--requests table-wrap--column-lines has-scrollbar">
184
+ <article role="tabpanel" id="tab-accounts" :hidden="view !== 'accounts'">
185
+ <div class="two-column">
186
+ <div class="panel account-list-panel">
187
+ <h3>Account list</h3>
188
+ <div class="list-actions">
189
+ <div class="searchbox">
190
+ <input type="search" placeholder="Search accounts" aria-label="Search accounts" x-ref="accountSearch"
191
+ x-model="accounts.searchQuery" @keydown.escape.prevent="clearAccountSearch">
192
+ <button type="button" aria-label="search" title="Search accounts"
193
+ @click="focusAccountSearch"></button>
194
+ </div>
195
+ <label role="button" :class="{ 'is-disabled': importState.isLoading }" @click="logImportClick('label')">
196
+ <input type="file" class="file-input" accept="application/json" @click="logImportClick('input')"
197
+ @change="handleAuthImport($event)" :disabled="importState.isLoading">
198
+ Import auth.json
199
+ </label>
200
+ <button type="button" @click="openAddAccountDialog">Add account</button>
201
+ </div>
202
+ <div class="table-wrap table-wrap--column-lines has-scrollbar">
137
203
  <table>
138
204
  <thead>
139
205
  <tr>
140
- <th class="highlighted indicator">Time</th>
141
- <th>Account</th>
142
- <th>Model</th>
143
- <th>Status</th>
144
- <th>Error</th>
145
- <th style="text-align: right">Tokens</th>
146
- <th style="text-align: right">Cost</th>
206
+ <th class="highlighted indicator" style="width: 28%">Account ID</th>
207
+ <th style="width: 25%">Email</th>
208
+ <th style="width: 10%">Plan</th>
209
+ <th style="width: 12%">Status</th>
210
+ <th style="text-align: center; width: 12%">
211
+ <span style="display: inline-flex; flex-direction: column; align-items: center">
212
+ <span>Remaining</span>
213
+ <span style="font-weight: normal; font-size: 0.9em; color: var(--text-muted)"
214
+ x-text="`(${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
215
+ </span>
216
+ </span>
217
+ </th>
218
+ <th style="text-align: center; width: 13%">
219
+ <span style="display: inline-flex; flex-direction: column; align-items: center">
220
+ <span style="white-space: nowrap">Quota reset</span>
221
+ <span style="font-weight: normal; font-size: 0.9em; color: var(--text-muted)"
222
+ x-text="`(${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
223
+ </span>
224
+ </span>
225
+ </th>
147
226
  </tr>
148
227
  </thead>
149
228
  <tbody>
150
- <template x-for="request in dashboard.requests" :key="request.key">
229
+ <template x-for="account in filteredAccounts" :key="account.id">
230
+ <tr :class="{ highlighted: account.id === accounts.selectedId }"
231
+ @click="selectAccount(account.id)" style="cursor: pointer">
232
+ <td class="cell-truncate" x-text="account.id" :title="account.id"></td>
233
+ <td class="cell-truncate" x-text="account.email" :title="account.email"></td>
234
+ <td x-text="planLabel(account.plan)"></td>
235
+ <td><span class="status-pill" :class="account.status"
236
+ x-text="statusLabel(account.status)"></span>
237
+ </td>
238
+ <td style="text-align: center"
239
+ :class="calculateTextUsageTextClass(account.status, account.usage?.secondaryRemainingPercent)"
240
+ x-text="formatPercent(account.usage?.secondaryRemainingPercent)">
241
+ </td>
242
+ <td style="text-align: center" x-text="formatQuotaResetLabel(account.resetAtSecondary)"></td>
243
+ </tr>
244
+ </template>
245
+ <template x-if="filteredAccounts.length === 0">
151
246
  <tr>
152
- <td x-text="request.time"></td>
153
- <td x-text="request.account"></td>
154
- <td x-text="request.model"></td>
155
- <td><span class="status-pill" :class="request.status.class"
156
- x-text="request.status.label"></span></td>
157
- <td x-text="request.error" :title="request.errorTitle || ''"></td>
158
- <td style="text-align: right" x-text="request.tokens"></td>
159
- <td style="text-align: right" x-text="request.cost"></td>
247
+ <td colspan="6" class="text-muted" style="text-align: center">
248
+ <span
249
+ x-text="accounts.searchQuery ? 'No accounts match your search.' : 'No accounts available.'"></span>
250
+ </td>
160
251
  </tr>
161
252
  </template>
162
253
  </tbody>
163
254
  </table>
164
255
  </div>
165
256
  </div>
166
- </article>
167
257
 
168
- <article role="tabpanel" id="tab-accounts" :hidden="view !== 'accounts'">
169
- <div class="two-column">
170
- <div class="panel account-list-panel">
171
- <h3>Account list</h3>
172
- <div class="list-actions">
173
- <div class="searchbox">
174
- <input type="search" placeholder="Search accounts" aria-label="Search accounts"
175
- x-ref="accountSearch" x-model="accounts.searchQuery" @keydown.escape.prevent="clearAccountSearch">
176
- <button type="button" aria-label="search" title="Search accounts"
177
- @click="focusAccountSearch"></button>
178
- </div>
179
- <label role="button" :class="{ 'is-disabled': importState.isLoading }"
180
- @click="logImportClick('label')">
181
- <input type="file" class="file-input" accept="application/json" @click="logImportClick('input')"
182
- @change="handleAuthImport($event)" :disabled="importState.isLoading">
183
- Import auth.json
184
- </label>
185
- <button type="button" @click="openAddAccountDialog">Add account</button>
258
+ <div class="panel">
259
+ <h3>Selected account</h3>
260
+ <div class="summary-list">
261
+ <div><span>Email</span><strong x-text="selectedAccount.email" :title="selectedAccount.email"></strong>
186
262
  </div>
187
- <div class="table-wrap table-wrap--column-lines has-scrollbar">
188
- <table>
189
- <thead>
190
- <tr>
191
- <th class="highlighted indicator">Account ID</th>
192
- <th>Email</th>
193
- <th>Plan</th>
194
- <th>Status</th>
195
- <th style="text-align: right"
196
- x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
197
- </th>
198
- <th
199
- x-text="`Quota reset (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
200
- </th>
201
- </tr>
202
- </thead>
203
- <tbody>
204
- <template x-for="account in filteredAccounts" :key="account.id">
205
- <tr :class="{ highlighted: account.id === accounts.selectedId }"
206
- @click="selectAccount(account.id)" style="cursor: pointer">
207
- <td x-text="account.id"></td>
208
- <td x-text="account.email"></td>
209
- <td x-text="planLabel(account.plan)"></td>
210
- <td><span class="status-pill" :class="account.status"
211
- x-text="statusLabel(account.status)"></span>
212
- </td>
213
- <td style="text-align: right"
214
- x-text="formatPercent(account.usage?.secondaryRemainingPercent)">
215
- </td>
216
- <td x-text="formatQuotaResetLabel(account.resetAtSecondary)"></td>
217
- </tr>
218
- </template>
219
- <template x-if="filteredAccounts.length === 0">
220
- <tr>
221
- <td colspan="6" class="text-muted" style="text-align: center">
222
- <span
223
- x-text="accounts.searchQuery ? 'No accounts match your search.' : 'No accounts available.'"></span>
224
- </td>
225
- </tr>
226
- </template>
227
- </tbody>
228
- </table>
263
+ <div><span>Account ID</span><strong x-text="selectedAccount.id" :title="selectedAccount.id"></strong>
229
264
  </div>
230
- </div>
231
-
232
- <div class="panel">
233
- <h3>Selected account</h3>
234
- <div class="summary-list">
235
- <div><span>Email</span><strong x-text="selectedAccount.email"></strong></div>
236
- <div><span>Account ID</span><strong x-text="selectedAccount.id"></strong></div>
237
- <div><span>Plan</span><strong x-text="planLabel(selectedAccount.plan)"></strong></div>
238
- <div>
239
- <span>Status</span>
240
- <span class="status-pill" :class="selectedAccount.status"
241
- x-text="statusLabel(selectedAccount.status)"></span>
242
- </div>
243
- <div><span
244
- x-text="`Quota reset (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></span><strong
245
- x-text="formatQuotaResetLabel(selectedAccount.resetAtPrimary)"></strong></div>
246
- <div><span
247
- x-text="`Quota reset (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></span><strong
248
- x-text="formatQuotaResetLabel(selectedAccount.resetAtSecondary)"></strong></div>
265
+ <div><span>Plan</span><strong x-text="planLabel(selectedAccount.plan)"></strong></div>
266
+ <div>
267
+ <span>Status</span>
268
+ <span class="status-pill" :class="selectedAccount.status"
269
+ x-text="statusLabel(selectedAccount.status)"></span>
249
270
  </div>
271
+ <div><span
272
+ x-text="`Quota reset (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></span><strong
273
+ x-text="formatQuotaResetLabel(selectedAccount.resetAtPrimary)"></strong></div>
274
+ <div><span
275
+ x-text="`Quota reset (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></span><strong
276
+ x-text="formatQuotaResetLabel(selectedAccount.resetAtSecondary)"></strong></div>
277
+ </div>
250
278
 
251
- <div class="split-stack" style="margin-top: 12px">
252
- <div class="progress-row">
253
- <label
254
- x-text="`Remaining (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></label>
255
- <div role="progressbar" aria-valuemin="0" aria-valuemax="100"
256
- :aria-valuenow="formatPercentValue(selectedAccount.usage?.primaryRemainingPercent)">
257
- <div :style="{ width: `${formatPercentValue(selectedAccount.usage?.primaryRemainingPercent)}%` }">
258
- </div>
279
+ <div class="split-stack" style="margin-top: 12px">
280
+ <div class="progress-row">
281
+ <label
282
+ x-text="`Remaining (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></label>
283
+ <div role="progressbar" aria-valuemin="0" aria-valuemax="100"
284
+ :class="calculateProgressClass(selectedAccount.status, selectedAccount.usage?.primaryRemainingPercent)"
285
+ :aria-valuenow="formatPercentValue(selectedAccount.usage?.primaryRemainingPercent)">
286
+ <div :style="{ width: `${formatPercentValue(selectedAccount.usage?.primaryRemainingPercent)}%` }">
259
287
  </div>
260
- <span class="text-muted"
261
- x-text="formatPercent(selectedAccount.usage?.primaryRemainingPercent)"></span>
262
- </div>
263
- <div class="progress-row">
264
- <label
265
- x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
266
- <div role="progressbar" aria-valuemin="0" aria-valuemax="100"
267
- :aria-valuenow="formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)">
268
- <div
269
- :style="{ width: `${formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)}%` }">
270
- </div>
271
- </div>
272
- <span class="text-muted"
273
- x-text="formatPercent(selectedAccount.usage?.secondaryRemainingPercent)"></span>
274
288
  </div>
289
+ <span
290
+ :class="calculateTextUsageTextClass(selectedAccount.status, selectedAccount.usage?.primaryRemainingPercent)"
291
+ x-text="formatPercent(selectedAccount.usage?.primaryRemainingPercent)"></span>
275
292
  </div>
276
-
277
- <fieldset style="margin-top: 12px">
278
- <legend>Tokens</legend>
279
- <div class="summary-list">
280
- <div><span>Access token</span><span x-text="formatAccessTokenLabel(selectedAccount.auth)"></span>
281
- </div>
282
- <div><span>Refresh token</span><span x-text="formatRefreshTokenLabel(selectedAccount.auth)"></span>
293
+ <div class="progress-row">
294
+ <label
295
+ x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
296
+ <div role="progressbar" aria-valuemin="0" aria-valuemax="100"
297
+ :class="calculateProgressClass(selectedAccount.status, selectedAccount.usage?.secondaryRemainingPercent)"
298
+ :aria-valuenow="formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)">
299
+ <div :style="{ width: `${formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)}%` }">
283
300
  </div>
284
- <div><span>Id token</span><span x-text="formatIdTokenLabel(selectedAccount.auth)"></span></div>
285
301
  </div>
286
- </fieldset>
287
-
288
- <div class="inline-actions" style="margin-top: 12px">
289
- <template x-if="selectedAccount.status === 'deactivated'">
290
- <button @click="startReauthFlow">Re-authenticate</button>
291
- </template>
292
- <template x-if="selectedAccount.status === 'paused'">
293
- <button @click="resumeSelectedAccount">Resume</button>
294
- </template>
295
- <template x-if="selectedAccount.status !== 'deactivated' && selectedAccount.status !== 'paused'">
296
- <button @click="pauseSelectedAccount">Pause</button>
297
- </template>
298
- <button @click="deleteSelectedAccount">Delete</button>
302
+ <span
303
+ :class="calculateTextUsageTextClass(selectedAccount.status, selectedAccount.usage?.secondaryRemainingPercent)"
304
+ x-text="formatPercent(selectedAccount.usage?.secondaryRemainingPercent)"></span>
299
305
  </div>
300
306
  </div>
301
- </div>
302
- </article>
303
-
304
- <article role="tabpanel" id="tab-settings" :hidden="view !== 'settings'">
305
- <div class="panel">
306
- <h3>Routing settings</h3>
307
- <p class="text-muted">Toggle routing features. When both options are off, accounts are selected by
308
- balancing usage evenly.</p>
309
307
 
310
- <fieldset style="margin-top: 12px">
311
- <legend>Sticky threads</legend>
312
- <input
313
- id="sticky-threads-toggle"
314
- type="checkbox"
315
- x-model="settings.stickyThreadsEnabled"
316
- aria-describedby="sticky-threads-help"
317
- >
318
- <label for="sticky-threads-toggle">Enable sticky threads (reuse the same upstream account per conversation)</label>
319
- <div id="sticky-threads-help" class="text-muted" style="margin-top: 6px">
320
- When enabled, requests with a prompt cache key stay pinned to the same upstream account unless the
321
- pinned account becomes unavailable.
322
- </div>
323
- </fieldset>
324
-
325
- <fieldset style="margin-top: 12px">
326
- <legend>Reset priority</legend>
327
- <input
328
- id="reset-priority-toggle"
329
- type="checkbox"
330
- x-model="settings.preferEarlierResetAccounts"
331
- aria-describedby="reset-priority-help"
332
- >
333
- <label for="reset-priority-toggle">Prefer accounts that reset earlier first</label>
334
- <div id="reset-priority-help" class="text-muted" style="margin-top: 6px">
335
- When enabled, the load balancer prefers accounts whose secondary quota resets sooner, then balances
336
- usage.
308
+ <fieldset class="token-fieldset">
309
+ <legend>Tokens</legend>
310
+ <div class="summary-list">
311
+ <div><span>Access token</span><span x-text="formatAccessTokenLabel(selectedAccount.auth)"></span>
312
+ </div>
313
+ <div><span>Refresh token</span><span x-text="formatRefreshTokenLabel(selectedAccount.auth)"></span>
314
+ </div>
315
+ <div><span>Id token</span><span x-text="formatIdTokenLabel(selectedAccount.auth)"></span></div>
337
316
  </div>
338
317
  </fieldset>
339
318
 
340
319
  <div class="inline-actions" style="margin-top: 12px">
341
- <button type="button" @click="saveSettings" :disabled="settings.isSaving">
342
- <span>Save</span>
343
- </button>
320
+ <template x-if="selectedAccount.status === 'deactivated'">
321
+ <button @click="startReauthFlow">Re-authenticate</button>
322
+ </template>
323
+ <template x-if="selectedAccount.status === 'paused'">
324
+ <button @click="resumeSelectedAccount">Resume</button>
325
+ </template>
326
+ <template x-if="selectedAccount.status !== 'deactivated' && selectedAccount.status !== 'paused'">
327
+ <button @click="pauseSelectedAccount">Pause</button>
328
+ </template>
329
+ <button @click="deleteSelectedAccount">Delete</button>
344
330
  </div>
345
331
  </div>
346
- </article>
347
- </section>
348
- </div>
349
- <div class="status-bar">
350
- <template x-for="item in statusItems" :key="item">
351
- <p class="status-bar-field" x-text="item"></p>
352
- </template>
353
- </div>
354
- <div class="dialog-backdrop auth-dialog-backdrop" :class="{ 'is-open': authDialog.open }"
355
- :aria-hidden="!authDialog.open"></div>
356
- <div class="window active is-bright auth-dialog" role="dialog" aria-modal="true"
357
- aria-labelledby="add-account-title" :aria-hidden="!authDialog.open" :class="{ 'is-open': authDialog.open }">
358
- <div class="title-bar">
359
- <div class="title-bar-text" id="add-account-title">Add account</div>
360
- <div class="title-bar-controls">
361
- <button aria-label="Close" @click="closeAddAccountDialog"></button>
362
332
  </div>
363
- </div>
364
- <div class="window-body has-space auth-dialog__body">
365
- <template x-if="authDialog.stage === 'intro'">
366
- <div class="auth-dialog__intro">
367
- <p class="auth-dialog__lead">Sign in with ChatGPT OAuth. API keys are not supported.</p>
368
- <fieldset class="auth-dialog__methods">
369
- <legend>Sign-in method</legend>
370
- <div class="auth-dialog__option">
371
- <input id="auth-method-browser" type="radio" name="auth-method" value="browser"
372
- x-model="authDialog.selectedMethod">
373
- <label for="auth-method-browser">Browser login (PKCE)</label>
374
- <div class="auth-dialog__option-meta text-muted">Requires local callback on port 1455.</div>
375
- </div>
376
- <div class="auth-dialog__option">
377
- <input id="auth-method-device" type="radio" name="auth-method" value="device"
378
- x-model="authDialog.selectedMethod">
379
- <label for="auth-method-device">Device code</label>
380
- <div class="auth-dialog__option-meta text-muted">Use when running in Docker or remote environments.
381
- </div>
382
- </div>
383
- </fieldset>
384
- </div>
385
- </template>
386
- <template x-if="authDialog.stage === 'browser'">
387
- <div class="auth-dialog__panel">
388
- <p class="auth-dialog__lead">Open the authorization page to complete sign-in.</p>
389
- <div class="auth-dialog__detail">
390
- <div class="auth-dialog__label">Authorization URL</div>
391
- <div class="auth-dialog__value auth-dialog__link" x-text="authDialog.authorizationUrl"></div>
392
- </div>
393
- <div class="auth-dialog__status">
394
- <span class="spinner animate" aria-hidden="true" x-show="authDialog.status === 'pending'"></span>
395
- <span x-text="authDialog.statusLabel"></span>
396
- </div>
397
- </div>
398
- </template>
399
- <template x-if="authDialog.stage === 'device'">
400
- <div class="auth-dialog__panel">
401
- <p class="auth-dialog__lead">Use the device code to sign in. Enter the code at the verification URL.</p>
402
- <ol class="auth-dialog__steps">
403
- <li>Open the verification URL.</li>
404
- <li>Enter the user code shown below.</li>
405
- <li>Return here to finish the sign-in.</li>
406
- </ol>
407
- <div class="auth-dialog__device-grid">
408
- <div>
409
- <div class="auth-dialog__label">User code</div>
410
- <div class="auth-dialog__code-value" x-text="authDialog.userCode"></div>
411
- </div>
412
- <div>
413
- <div class="auth-dialog__label">Verification URL</div>
414
- <div class="auth-dialog__value auth-dialog__link" x-text="authDialog.verificationUrl"></div>
415
- </div>
416
- </div>
417
- <div class="auth-dialog__device-meta text-muted">
418
- Expires in <span x-text="formatCountdown(authDialog.remainingSeconds)"></span>
333
+ </article>
334
+
335
+ <article role="tabpanel" id="tab-settings" :hidden="view !== 'settings'">
336
+ <div class="panel">
337
+ <h3>Routing settings</h3>
338
+ <p class="text-muted">Toggle routing features. When both options are off, accounts are selected by
339
+ balancing usage evenly.</p>
340
+
341
+ <fieldset class="settings-fieldset">
342
+ <legend>Sticky threads</legend>
343
+ <input id="sticky-threads-toggle" type="checkbox" x-model="settings.stickyThreadsEnabled"
344
+ aria-describedby="sticky-threads-help">
345
+ <label for="sticky-threads-toggle">Enable sticky threads (reuse the same upstream account per
346
+ conversation)</label>
347
+ <div id="sticky-threads-help" class="text-muted" style="margin-top: 6px">
348
+ When enabled, requests with a prompt cache key stay pinned to the same upstream account unless the
349
+ pinned account becomes unavailable.
419
350
  </div>
420
- <div class="auth-dialog__status">
421
- <span class="spinner animate" aria-hidden="true" x-show="authDialog.status === 'pending'"></span>
422
- <span x-text="authDialog.statusLabel"></span>
351
+ </fieldset>
352
+
353
+ <fieldset class="settings-fieldset">
354
+ <legend>Reset priority</legend>
355
+ <input id="reset-priority-toggle" type="checkbox" x-model="settings.preferEarlierResetAccounts"
356
+ aria-describedby="reset-priority-help">
357
+ <label for="reset-priority-toggle">Prefer accounts that reset earlier first</label>
358
+ <div id="reset-priority-help" class="text-muted" style="margin-top: 6px">
359
+ When enabled, the load balancer prefers accounts whose secondary quota resets sooner, then balances
360
+ usage.
423
361
  </div>
362
+ </fieldset>
363
+
364
+ <div class="inline-actions" style="margin-top: 12px">
365
+ <button type="button" @click="saveSettings" :disabled="settings.isSaving">
366
+ <span>Save</span>
367
+ </button>
424
368
  </div>
425
- </template>
426
- <template x-if="authDialog.stage === 'success'">
427
- <div class="auth-dialog__panel">
428
- <p class="auth-dialog__lead">Account linked successfully.</p>
429
- <p class="text-muted">Return to the dashboard and select the new account.</p>
369
+ </div>
370
+ </article>
371
+ </section>
372
+ </div>
373
+ <div class="status-bar">
374
+ <template x-for="item in statusItems" :key="item">
375
+ <p class="status-bar-field" x-text="item"></p>
376
+ </template>
377
+ </div>
378
+ <div class="dialog-backdrop auth-dialog-backdrop" :class="{ 'is-open': authDialog.open }"
379
+ :aria-hidden="!authDialog.open"></div>
380
+ <div class="window active is-bright auth-dialog" role="dialog" aria-modal="true" aria-labelledby="add-account-title"
381
+ :aria-hidden="!authDialog.open" :class="{ 'is-open': authDialog.open }">
382
+ <div class="title-bar">
383
+ <div class="title-bar-text" id="add-account-title">Add account</div>
384
+ <div class="title-bar-controls">
385
+ <button aria-label="Close" @click="closeAddAccountDialog"></button>
386
+ </div>
387
+ </div>
388
+ <div class="window-body has-space auth-dialog__body">
389
+ <template x-if="authDialog.stage === 'intro'">
390
+ <div class="auth-dialog__intro">
391
+ <p class="auth-dialog__lead">Sign in with ChatGPT OAuth. API keys are not supported.</p>
392
+ <fieldset class="auth-dialog__methods">
393
+ <legend>Sign-in method</legend>
394
+ <div class="auth-dialog__option" @click="authDialog.selectedMethod = 'browser'">
395
+ <input id="auth-method-browser" type="radio" name="auth-method" value="browser"
396
+ x-model="authDialog.selectedMethod">
397
+ <label for="auth-method-browser">Browser login (PKCE)</label>
398
+ <div class="auth-dialog__option-meta text-muted">Requires local callback on port 1455.</div>
430
399
  </div>
431
- </template>
432
- <template x-if="authDialog.stage === 'error'">
433
- <div class="auth-dialog__panel auth-dialog__error">
434
- <p class="auth-dialog__lead">Authorization failed.</p>
435
- <p class="text-muted" x-text="authDialog.errorMessage"></p>
400
+ <div class="auth-dialog__option" @click="authDialog.selectedMethod = 'device'">
401
+ <input id="auth-method-device" type="radio" name="auth-method" value="device"
402
+ x-model="authDialog.selectedMethod">
403
+ <label for="auth-method-device">Device code</label>
404
+ <div class="auth-dialog__option-meta text-muted">Use when running in Docker or remote environments.
405
+ </div>
436
406
  </div>
437
- </template>
407
+ </fieldset>
438
408
  </div>
439
- <footer class="auth-dialog__actions">
440
- <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'intro'">
441
- <button type="button" @click="closeAddAccountDialog">Cancel</button>
442
- <button type="button" class="default" @click="startOAuth" :disabled="authDialog.isLoading">Start sign-in
443
- </button>
444
- </div>
445
- <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'browser'">
446
- <button type="button" @click="resetAuthDialogState">Change method</button>
447
- <button type="button" class="default" @click="openAuthorizationUrl"
448
- :disabled="!authDialog.authorizationUrl">Open sign-in page</button>
449
- </div>
450
- <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'device'">
451
- <button type="button" @click="resetAuthDialogState">Change method</button>
452
- <button type="button" @click="copyToClipboard(authDialog.userCode, 'User code')"
453
- :disabled="!authDialog.userCode">Copy code</button>
454
- <button type="button" @click="openVerificationUrl" :disabled="!authDialog.verificationUrl">Open link
455
- </button>
456
- </div>
457
- <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'success'">
458
- <button type="button" class="default" @click="closeAddAccountDialog">Done</button>
409
+ </template>
410
+ <template x-if="authDialog.stage === 'browser'">
411
+ <div class="auth-dialog__panel">
412
+ <p class="auth-dialog__lead">Open the authorization page to complete sign-in.</p>
413
+ <div class="auth-dialog__detail">
414
+ <div class="auth-dialog__label">Authorization URL</div>
415
+ <div class="url-copy-row">
416
+ <a class="auth-dialog__value auth-dialog__link" :href="authDialog.authorizationUrl" target="_blank"
417
+ x-text="authDialog.authorizationUrl"></a>
418
+ <button type="button" class="copy-btn"
419
+ @click="copyToClipboard(authDialog.authorizationUrl, 'Authorization URL', $event)">Copy</button>
420
+ </div>
459
421
  </div>
460
- <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'error'">
461
- <button type="button" @click="resetAuthDialogState">Try again</button>
462
- <button type="button" class="default" @click="closeAddAccountDialog">Close</button>
463
- </div>
464
- </footer>
465
- </div>
466
- <div class="dialog-backdrop" :class="{ 'is-open': messageBox.open }" :aria-hidden="!messageBox.open"
467
- @click="closeMessageBox"></div>
468
- <div class="window active is-bright message-dialog" role="dialog" aria-modal="true"
469
- aria-labelledby="message-box-title" aria-describedby="message-box-body" :aria-hidden="!messageBox.open"
470
- :class="{ 'is-open': messageBox.open }">
471
- <div class="title-bar">
472
- <div class="title-bar-text" id="message-box-title" x-text="messageBox.title"></div>
473
- <div class="title-bar-controls">
474
- <button aria-label="Close" @click="closeMessageBox"></button>
422
+ <div class="auth-dialog__status">
423
+ <span class="auth-dialog__status-text" x-text="authDialog.statusLabel"></span>
424
+ <span class="spinner animate" aria-hidden="true" x-show="authDialog.status === 'pending'"></span>
475
425
  </div>
476
426
  </div>
477
- <div class="window-body has-space message-dialog__body">
478
- <div class="message-dialog__content">
479
- <div class="message-dialog__icon" :class="messageBox.iconTone ? `is-${messageBox.iconTone}` : ''"
480
- aria-hidden="true" x-show="messageBox.iconTone"></div>
481
- <div class="message-dialog__copy">
482
- <p id="message-box-body" x-text="messageBox.message"></p>
483
- <template x-if="messageBox.details">
484
- <pre class="code-box message-dialog__details" x-text="messageBox.details"></pre>
485
- </template>
427
+ </template>
428
+ <template x-if="authDialog.stage === 'device'">
429
+ <div class="auth-dialog__panel">
430
+ <p class="auth-dialog__lead">Use the device code to sign in. Enter the code at the verification URL.</p>
431
+ <ol class="auth-dialog__steps">
432
+ <li>Open the verification URL.</li>
433
+ <li>Enter the user code shown below.</li>
434
+ <li>Return here to finish the sign-in.</li>
435
+ </ol>
436
+ <div class="auth-dialog__device-grid">
437
+ <div>
438
+ <div class="auth-dialog__label">User code</div>
439
+ <div class="auth-dialog__code-value">
440
+ <span x-text="authDialog.userCode"></span>
441
+ <button type="button" class="copy-btn"
442
+ @click="copyToClipboard(authDialog.userCode, 'User code', $event)">Copy</button>
443
+ </div>
444
+ </div>
445
+ <div>
446
+ <div class="auth-dialog__label">Verification URL</div>
447
+ <div class="url-copy-row">
448
+ <a class="auth-dialog__value auth-dialog__link" :href="authDialog.verificationUrl" target="_blank"
449
+ x-text="authDialog.verificationUrl"></a>
450
+ <button type="button" class="copy-btn"
451
+ @click="copyToClipboard(authDialog.verificationUrl, 'Verification URL', $event)">Copy</button>
452
+ </div>
453
+ </div>
454
+ </div>
455
+ <div class="auth-dialog__status">
456
+ <span class="auth-dialog__status-text" x-text="authDialog.statusLabel"></span>
457
+ <div class="auth-dialog__status-meta" x-show="authDialog.status === 'pending'">
458
+ <span class="spinner animate" aria-hidden="true"></span>
459
+ <span class="text-muted">Expires in <span
460
+ x-text="formatCountdown(authDialog.remainingSeconds)"></span></span>
486
461
  </div>
487
462
  </div>
488
463
  </div>
489
- <footer class="message-dialog__actions">
490
- <button x-show="messageBox.mode === 'confirm'" type="button" @click="cancelMessageBox"
491
- x-text="messageBox.cancelLabel"></button>
492
- <button x-show="messageBox.mode === 'confirm'" type="button" class="default" @click="confirmMessageBox"
493
- x-text="messageBox.confirmLabel"></button>
494
- <button x-show="messageBox.mode !== 'confirm'" type="button" class="default"
495
- @click="closeMessageBox">OK</button>
496
- </footer>
464
+ </template>
465
+ <template x-if="authDialog.stage === 'success'">
466
+ <div class="auth-dialog__panel">
467
+ <p class="auth-dialog__lead">Account linked successfully.</p>
468
+ <p class="text-muted">Return to the dashboard and select the new account.</p>
469
+ </div>
470
+ </template>
471
+ <template x-if="authDialog.stage === 'error'">
472
+ <div class="auth-dialog__panel auth-dialog__error">
473
+ <p class="auth-dialog__lead">Authorization failed.</p>
474
+ <p class="text-muted" x-text="authDialog.errorMessage"></p>
475
+ </div>
476
+ </template>
477
+ </div>
478
+ <footer class="auth-dialog__actions">
479
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'intro'">
480
+ <button type="button" @click="closeAddAccountDialog">Cancel</button>
481
+ <button type="button" class="default" @click="startOAuth" :disabled="authDialog.isLoading">Start sign-in
482
+ </button>
483
+ </div>
484
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'browser'">
485
+ <button type="button" @click="resetAuthDialogState">Change method</button>
486
+ <button type="button" class="default" @click="openAuthorizationUrl"
487
+ :disabled="!authDialog.authorizationUrl">Open sign-in page</button>
488
+ </div>
489
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'device'">
490
+ <button type="button" @click="resetAuthDialogState">Change method</button>
491
+ <button type="button" class="default" @click="openVerificationUrl" :disabled="!authDialog.verificationUrl">Open
492
+ link</button>
493
+ </div>
494
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'success'">
495
+ <button type="button" class="default" @click="closeAddAccountDialog">Done</button>
496
+ </div>
497
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'error'">
498
+ <button type="button" @click="resetAuthDialogState">Try again</button>
499
+ <button type="button" class="default" @click="closeAddAccountDialog">Close</button>
500
+ </div>
501
+ </footer>
502
+ </div>
503
+ <div class="dialog-backdrop" :class="{ 'is-open': messageBox.open }" :aria-hidden="!messageBox.open"
504
+ @click="closeMessageBox"></div>
505
+ <div class="window active is-bright message-dialog" role="dialog" aria-modal="true"
506
+ aria-labelledby="message-box-title" aria-describedby="message-box-body" :aria-hidden="!messageBox.open"
507
+ :class="{ 'is-open': messageBox.open }">
508
+ <div class="title-bar">
509
+ <div class="title-bar-text" id="message-box-title" x-text="messageBox.title"></div>
510
+ <div class="title-bar-controls">
511
+ <button aria-label="Close" @click="closeMessageBox"></button>
512
+ </div>
513
+ </div>
514
+ <div class="window-body has-space message-dialog__body">
515
+ <div class="message-dialog__content">
516
+ <div class="message-dialog__icon" :class="messageBox.iconTone ? `is-${messageBox.iconTone}` : ''"
517
+ aria-hidden="true" x-show="messageBox.iconTone"></div>
518
+ <div class="message-dialog__copy">
519
+ <p id="message-box-body" x-text="messageBox.message"></p>
520
+ <template x-if="messageBox.details">
521
+ <pre class="code-box message-dialog__details" x-text="messageBox.details"></pre>
522
+ </template>
523
+ </div>
497
524
  </div>
498
525
  </div>
526
+ <footer class="message-dialog__actions">
527
+ <button x-show="messageBox.mode === 'confirm'" type="button" @click="cancelMessageBox"
528
+ x-text="messageBox.cancelLabel"></button>
529
+ <button x-show="messageBox.mode === 'confirm'" type="button" class="default" @click="confirmMessageBox"
530
+ x-text="messageBox.confirmLabel"></button>
531
+ <button x-show="messageBox.mode !== 'confirm'" type="button" class="default" @click="closeMessageBox">OK</button>
532
+ </footer>
533
+ </div>
534
+ </div>
535
+ </main>
499
536
  </div>
500
537
  <script defer src="/dashboard/index.js"></script>
501
538
  <script defer src="https://unpkg.com/alpinejs@3.13.2/dist/cdn.min.js"></script>
502
539
  </body>
503
540
 
504
- </html>
541
+ </html>