codex-lb 0.1.2__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.
Files changed (80) hide show
  1. app/__init__.py +5 -0
  2. app/cli.py +24 -0
  3. app/core/__init__.py +0 -0
  4. app/core/auth/__init__.py +96 -0
  5. app/core/auth/models.py +49 -0
  6. app/core/auth/refresh.py +144 -0
  7. app/core/balancer/__init__.py +19 -0
  8. app/core/balancer/logic.py +140 -0
  9. app/core/balancer/types.py +9 -0
  10. app/core/clients/__init__.py +0 -0
  11. app/core/clients/http.py +39 -0
  12. app/core/clients/oauth.py +340 -0
  13. app/core/clients/proxy.py +265 -0
  14. app/core/clients/usage.py +143 -0
  15. app/core/config/__init__.py +0 -0
  16. app/core/config/settings.py +69 -0
  17. app/core/crypto.py +37 -0
  18. app/core/errors.py +73 -0
  19. app/core/openai/__init__.py +0 -0
  20. app/core/openai/models.py +122 -0
  21. app/core/openai/parsing.py +55 -0
  22. app/core/openai/requests.py +59 -0
  23. app/core/types.py +4 -0
  24. app/core/usage/__init__.py +185 -0
  25. app/core/usage/logs.py +57 -0
  26. app/core/usage/models.py +35 -0
  27. app/core/usage/pricing.py +172 -0
  28. app/core/usage/types.py +95 -0
  29. app/core/utils/__init__.py +0 -0
  30. app/core/utils/request_id.py +30 -0
  31. app/core/utils/retry.py +16 -0
  32. app/core/utils/sse.py +13 -0
  33. app/core/utils/time.py +19 -0
  34. app/db/__init__.py +0 -0
  35. app/db/models.py +82 -0
  36. app/db/session.py +44 -0
  37. app/dependencies.py +123 -0
  38. app/main.py +124 -0
  39. app/modules/__init__.py +0 -0
  40. app/modules/accounts/__init__.py +0 -0
  41. app/modules/accounts/api.py +81 -0
  42. app/modules/accounts/repository.py +80 -0
  43. app/modules/accounts/schemas.py +66 -0
  44. app/modules/accounts/service.py +211 -0
  45. app/modules/health/__init__.py +0 -0
  46. app/modules/health/api.py +10 -0
  47. app/modules/oauth/__init__.py +0 -0
  48. app/modules/oauth/api.py +57 -0
  49. app/modules/oauth/schemas.py +32 -0
  50. app/modules/oauth/service.py +356 -0
  51. app/modules/oauth/templates/oauth_success.html +122 -0
  52. app/modules/proxy/__init__.py +0 -0
  53. app/modules/proxy/api.py +76 -0
  54. app/modules/proxy/auth_manager.py +51 -0
  55. app/modules/proxy/load_balancer.py +208 -0
  56. app/modules/proxy/schemas.py +85 -0
  57. app/modules/proxy/service.py +707 -0
  58. app/modules/proxy/types.py +37 -0
  59. app/modules/proxy/usage_updater.py +147 -0
  60. app/modules/request_logs/__init__.py +0 -0
  61. app/modules/request_logs/api.py +31 -0
  62. app/modules/request_logs/repository.py +86 -0
  63. app/modules/request_logs/schemas.py +25 -0
  64. app/modules/request_logs/service.py +77 -0
  65. app/modules/shared/__init__.py +0 -0
  66. app/modules/shared/schemas.py +8 -0
  67. app/modules/usage/__init__.py +0 -0
  68. app/modules/usage/api.py +31 -0
  69. app/modules/usage/repository.py +113 -0
  70. app/modules/usage/schemas.py +62 -0
  71. app/modules/usage/service.py +246 -0
  72. app/static/7.css +1336 -0
  73. app/static/index.css +543 -0
  74. app/static/index.html +457 -0
  75. app/static/index.js +1898 -0
  76. codex_lb-0.1.2.dist-info/METADATA +108 -0
  77. codex_lb-0.1.2.dist-info/RECORD +80 -0
  78. codex_lb-0.1.2.dist-info/WHEEL +4 -0
  79. codex_lb-0.1.2.dist-info/entry_points.txt +2 -0
  80. codex_lb-0.1.2.dist-info/licenses/LICENSE +21 -0
app/static/index.html ADDED
@@ -0,0 +1,457 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <meta name="color-scheme" content="light">
8
+ <title>Codex LB</title>
9
+ <link rel="icon"
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
+ type="image/svg+xml">
12
+ <link rel="stylesheet" href="/dashboard/7.css">
13
+ <link rel="stylesheet" href="/dashboard/index.css">
14
+ </head>
15
+
16
+ <body class="has-scrollbar" x-data="feApp" x-init="init()" x-cloak>
17
+ <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>
25
+ </div>
26
+ </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>
33
+ </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>
43
+
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>
55
+ </div>
56
+
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>
63
+ </div>
64
+ </template>
65
+ </div>
66
+
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>
77
+ </div>
78
+ <ul class="legend">
79
+ <template x-for="item in donut.items" :key="item.label">
80
+ <li>
81
+ <span>
82
+ <i :style="{ '--legend-color': item.color }"></i>
83
+ <span x-text="item.label"></span>
84
+ </span>
85
+ <span x-text="item.detail"></span>
86
+ </li>
87
+ </template>
88
+ </ul>
89
+ </div>
90
+ </div>
91
+ </template>
92
+ </div>
93
+
94
+ <section style="margin-top: 12px">
95
+ <h3>Account status</h3>
96
+ <div class="account-grid">
97
+ <template x-for="card in dashboard.accountCards" :key="card.email">
98
+ <div class="account-card">
99
+ <header>
100
+ <div>
101
+ <div x-text="`${card.email} (${card.plan})`"></div>
102
+ <div class="account-id" x-text="card.accountId"></div>
103
+ </div>
104
+ <span class="status-pill" :class="card.status.class" x-text="card.status.label"></span>
105
+ </header>
106
+ <div class="progress-row">
107
+ <label
108
+ x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
109
+ <template x-if="card.marquee">
110
+ <div role="progressbar" class="marquee"></div>
111
+ </template>
112
+ <template x-if="!card.marquee">
113
+ <div role="progressbar" :class="card.progressClass" aria-valuemin="0" aria-valuemax="100"
114
+ :aria-valuenow="card.remaining">
115
+ <div :style="{ width: `${card.remaining}%` }"></div>
116
+ </div>
117
+ </template>
118
+ <span class="text-muted" x-text="card.remainingText"></span>
119
+ </div>
120
+ <div class="text-muted" x-text="card.meta"></div>
121
+ <div class="account-actions">
122
+ <template x-for="action in card.actions" :key="action.label">
123
+ <button x-text="action.label" @click="handleAccountAction(action, card)"></button>
124
+ </template>
125
+ </div>
126
+ </div>
127
+ </template>
128
+ </div>
129
+ </section>
130
+
131
+ <div class="panel" style="margin-top: 12px">
132
+ <h3>Recent requests</h3>
133
+ <div class="table-wrap table-wrap--requests table-wrap--column-lines has-scrollbar">
134
+ <table>
135
+ <thead>
136
+ <tr>
137
+ <th class="highlighted indicator">Time</th>
138
+ <th>Account</th>
139
+ <th>Model</th>
140
+ <th>Status</th>
141
+ <th>Error</th>
142
+ <th style="text-align: right">Tokens</th>
143
+ <th style="text-align: right">Cost</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody>
147
+ <template x-for="request in dashboard.requests" :key="request.key">
148
+ <tr>
149
+ <td x-text="request.time"></td>
150
+ <td x-text="request.account"></td>
151
+ <td x-text="request.model"></td>
152
+ <td><span class="status-pill" :class="request.status.class"
153
+ x-text="request.status.label"></span></td>
154
+ <td x-text="request.error" :title="request.errorTitle || ''"></td>
155
+ <td style="text-align: right" x-text="request.tokens"></td>
156
+ <td style="text-align: right" x-text="request.cost"></td>
157
+ </tr>
158
+ </template>
159
+ </tbody>
160
+ </table>
161
+ </div>
162
+ </div>
163
+ </article>
164
+
165
+ <article role="tabpanel" id="tab-accounts" :hidden="view !== 'accounts'">
166
+ <div class="two-column">
167
+ <div class="panel account-list-panel">
168
+ <h3>Account list</h3>
169
+ <div class="list-actions">
170
+ <div class="searchbox">
171
+ <input type="search" placeholder="Search accounts" aria-label="Search accounts"
172
+ x-ref="accountSearch" x-model="accounts.searchQuery" @keydown.escape.prevent="clearAccountSearch">
173
+ <button type="button" aria-label="search" title="Search accounts"
174
+ @click="focusAccountSearch"></button>
175
+ </div>
176
+ <label role="button" :class="{ 'is-disabled': importState.isLoading }"
177
+ @click="logImportClick('label')">
178
+ <input type="file" class="file-input" accept="application/json" @click="logImportClick('input')"
179
+ @change="handleAuthImport($event)" :disabled="importState.isLoading">
180
+ Import auth.json
181
+ </label>
182
+ <button type="button" @click="openAddAccountDialog">Add account</button>
183
+ </div>
184
+ <div class="table-wrap table-wrap--column-lines has-scrollbar">
185
+ <table>
186
+ <thead>
187
+ <tr>
188
+ <th class="highlighted indicator">Account ID</th>
189
+ <th>Email</th>
190
+ <th>Plan</th>
191
+ <th>Status</th>
192
+ <th style="text-align: right"
193
+ x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
194
+ </th>
195
+ <th
196
+ x-text="`Quota reset (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
197
+ </th>
198
+ </tr>
199
+ </thead>
200
+ <tbody>
201
+ <template x-for="account in filteredAccounts" :key="account.id">
202
+ <tr :class="{ highlighted: account.id === accounts.selectedId }"
203
+ @click="selectAccount(account.id)" style="cursor: pointer">
204
+ <td x-text="account.id"></td>
205
+ <td x-text="account.email"></td>
206
+ <td x-text="planLabel(account.plan)"></td>
207
+ <td><span class="status-pill" :class="account.status"
208
+ x-text="statusLabel(account.status)"></span>
209
+ </td>
210
+ <td style="text-align: right"
211
+ x-text="formatPercent(account.usage?.secondaryRemainingPercent)">
212
+ </td>
213
+ <td x-text="formatQuotaResetLabel(account.resetAtSecondary)"></td>
214
+ </tr>
215
+ </template>
216
+ <template x-if="filteredAccounts.length === 0">
217
+ <tr>
218
+ <td colspan="6" class="text-muted" style="text-align: center">
219
+ <span
220
+ x-text="accounts.searchQuery ? 'No accounts match your search.' : 'No accounts available.'"></span>
221
+ </td>
222
+ </tr>
223
+ </template>
224
+ </tbody>
225
+ </table>
226
+ </div>
227
+ </div>
228
+
229
+ <div class="panel">
230
+ <h3>Selected account</h3>
231
+ <div class="summary-list">
232
+ <div><span>Email</span><strong x-text="selectedAccount.email"></strong></div>
233
+ <div><span>Account ID</span><strong x-text="selectedAccount.id"></strong></div>
234
+ <div><span>Plan</span><strong x-text="planLabel(selectedAccount.plan)"></strong></div>
235
+ <div>
236
+ <span>Status</span>
237
+ <span class="status-pill" :class="selectedAccount.status"
238
+ x-text="statusLabel(selectedAccount.status)"></span>
239
+ </div>
240
+ <div><span
241
+ x-text="`Quota reset (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></span><strong
242
+ x-text="formatQuotaResetLabel(selectedAccount.resetAtPrimary)"></strong></div>
243
+ <div><span
244
+ x-text="`Quota reset (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></span><strong
245
+ x-text="formatQuotaResetLabel(selectedAccount.resetAtSecondary)"></strong></div>
246
+ </div>
247
+
248
+ <div class="split-stack" style="margin-top: 12px">
249
+ <div class="progress-row">
250
+ <label
251
+ x-text="`Remaining (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></label>
252
+ <div role="progressbar" aria-valuemin="0" aria-valuemax="100"
253
+ :aria-valuenow="formatPercentValue(selectedAccount.usage?.primaryRemainingPercent)">
254
+ <div :style="{ width: `${formatPercentValue(selectedAccount.usage?.primaryRemainingPercent)}%` }">
255
+ </div>
256
+ </div>
257
+ <span class="text-muted"
258
+ x-text="formatPercent(selectedAccount.usage?.primaryRemainingPercent)"></span>
259
+ </div>
260
+ <div class="progress-row">
261
+ <label
262
+ x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
263
+ <div role="progressbar" aria-valuemin="0" aria-valuemax="100"
264
+ :aria-valuenow="formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)">
265
+ <div
266
+ :style="{ width: `${formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)}%` }">
267
+ </div>
268
+ </div>
269
+ <span class="text-muted"
270
+ x-text="formatPercent(selectedAccount.usage?.secondaryRemainingPercent)"></span>
271
+ </div>
272
+ </div>
273
+
274
+ <fieldset style="margin-top: 12px">
275
+ <legend>Tokens</legend>
276
+ <div class="summary-list">
277
+ <div><span>Access token</span><span x-text="formatAccessTokenLabel(selectedAccount.auth)"></span>
278
+ </div>
279
+ <div><span>Refresh token</span><span x-text="formatRefreshTokenLabel(selectedAccount.auth)"></span>
280
+ </div>
281
+ <div><span>Id token</span><span x-text="formatIdTokenLabel(selectedAccount.auth)"></span></div>
282
+ </div>
283
+ </fieldset>
284
+
285
+ <div class="inline-actions" style="margin-top: 12px">
286
+ <template x-if="selectedAccount.status === 'deactivated'">
287
+ <button @click="startReauthFlow">Re-authenticate</button>
288
+ </template>
289
+ <template x-if="selectedAccount.status === 'paused'">
290
+ <button @click="resumeSelectedAccount">Resume</button>
291
+ </template>
292
+ <template x-if="selectedAccount.status !== 'deactivated' && selectedAccount.status !== 'paused'">
293
+ <button @click="pauseSelectedAccount">Pause</button>
294
+ </template>
295
+ <button @click="deleteSelectedAccount">Delete</button>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </article>
300
+ </section>
301
+ </div>
302
+ <div class="status-bar">
303
+ <template x-for="item in statusItems" :key="item">
304
+ <p class="status-bar-field" x-text="item"></p>
305
+ </template>
306
+ </div>
307
+ <div class="dialog-backdrop auth-dialog-backdrop" :class="{ 'is-open': authDialog.open }"
308
+ :aria-hidden="!authDialog.open"></div>
309
+ <div class="window active is-bright auth-dialog" role="dialog" aria-modal="true"
310
+ aria-labelledby="add-account-title" :aria-hidden="!authDialog.open" :class="{ 'is-open': authDialog.open }">
311
+ <div class="title-bar">
312
+ <div class="title-bar-text" id="add-account-title">Add account</div>
313
+ <div class="title-bar-controls">
314
+ <button aria-label="Close" @click="closeAddAccountDialog"></button>
315
+ </div>
316
+ </div>
317
+ <div class="window-body has-space auth-dialog__body">
318
+ <template x-if="authDialog.stage === 'intro'">
319
+ <div class="auth-dialog__intro">
320
+ <p class="auth-dialog__lead">Sign in with ChatGPT OAuth. API keys are not supported.</p>
321
+ <fieldset class="auth-dialog__methods">
322
+ <legend>Sign-in method</legend>
323
+ <div class="auth-dialog__option">
324
+ <input id="auth-method-browser" type="radio" name="auth-method" value="browser"
325
+ x-model="authDialog.selectedMethod">
326
+ <label for="auth-method-browser">Browser login (PKCE)</label>
327
+ <div class="auth-dialog__option-meta text-muted">Requires local callback on port 1455.</div>
328
+ </div>
329
+ <div class="auth-dialog__option">
330
+ <input id="auth-method-device" type="radio" name="auth-method" value="device"
331
+ x-model="authDialog.selectedMethod">
332
+ <label for="auth-method-device">Device code</label>
333
+ <div class="auth-dialog__option-meta text-muted">Use when running in Docker or remote environments.
334
+ </div>
335
+ </div>
336
+ </fieldset>
337
+ </div>
338
+ </template>
339
+ <template x-if="authDialog.stage === 'browser'">
340
+ <div class="auth-dialog__panel">
341
+ <p class="auth-dialog__lead">Open the authorization page to complete sign-in.</p>
342
+ <div class="auth-dialog__detail">
343
+ <div class="auth-dialog__label">Authorization URL</div>
344
+ <div class="auth-dialog__value auth-dialog__link" x-text="authDialog.authorizationUrl"></div>
345
+ </div>
346
+ <div class="auth-dialog__status">
347
+ <span class="spinner animate" aria-hidden="true" x-show="authDialog.status === 'pending'"></span>
348
+ <span x-text="authDialog.statusLabel"></span>
349
+ </div>
350
+ </div>
351
+ </template>
352
+ <template x-if="authDialog.stage === 'device'">
353
+ <div class="auth-dialog__panel">
354
+ <p class="auth-dialog__lead">Use the device code to sign in. Enter the code at the verification URL.</p>
355
+ <ol class="auth-dialog__steps">
356
+ <li>Open the verification URL.</li>
357
+ <li>Enter the user code shown below.</li>
358
+ <li>Return here to finish the sign-in.</li>
359
+ </ol>
360
+ <div class="auth-dialog__device-grid">
361
+ <div>
362
+ <div class="auth-dialog__label">User code</div>
363
+ <div class="auth-dialog__code-value" x-text="authDialog.userCode"></div>
364
+ </div>
365
+ <div>
366
+ <div class="auth-dialog__label">Verification URL</div>
367
+ <div class="auth-dialog__value auth-dialog__link" x-text="authDialog.verificationUrl"></div>
368
+ </div>
369
+ </div>
370
+ <div class="auth-dialog__device-meta text-muted">
371
+ Expires in <span x-text="formatCountdown(authDialog.remainingSeconds)"></span>
372
+ </div>
373
+ <div class="auth-dialog__status">
374
+ <span class="spinner animate" aria-hidden="true" x-show="authDialog.status === 'pending'"></span>
375
+ <span x-text="authDialog.statusLabel"></span>
376
+ </div>
377
+ </div>
378
+ </template>
379
+ <template x-if="authDialog.stage === 'success'">
380
+ <div class="auth-dialog__panel">
381
+ <p class="auth-dialog__lead">Account linked successfully.</p>
382
+ <p class="text-muted">Return to the dashboard and select the new account.</p>
383
+ </div>
384
+ </template>
385
+ <template x-if="authDialog.stage === 'error'">
386
+ <div class="auth-dialog__panel auth-dialog__error">
387
+ <p class="auth-dialog__lead">Authorization failed.</p>
388
+ <p class="text-muted" x-text="authDialog.errorMessage"></p>
389
+ </div>
390
+ </template>
391
+ </div>
392
+ <footer class="auth-dialog__actions">
393
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'intro'">
394
+ <button type="button" @click="closeAddAccountDialog">Cancel</button>
395
+ <button type="button" class="default" @click="startOAuth" :disabled="authDialog.isLoading">Start sign-in
396
+ </button>
397
+ </div>
398
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'browser'">
399
+ <button type="button" @click="resetAuthDialogState">Change method</button>
400
+ <button type="button" class="default" @click="openAuthorizationUrl"
401
+ :disabled="!authDialog.authorizationUrl">Open sign-in page</button>
402
+ </div>
403
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'device'">
404
+ <button type="button" @click="resetAuthDialogState">Change method</button>
405
+ <button type="button" @click="copyToClipboard(authDialog.userCode, 'User code')"
406
+ :disabled="!authDialog.userCode">Copy code</button>
407
+ <button type="button" @click="openVerificationUrl" :disabled="!authDialog.verificationUrl">Open link
408
+ </button>
409
+ </div>
410
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'success'">
411
+ <button type="button" class="default" @click="closeAddAccountDialog">Done</button>
412
+ </div>
413
+ <div class="auth-dialog__actions-row" x-show="authDialog.stage === 'error'">
414
+ <button type="button" @click="resetAuthDialogState">Try again</button>
415
+ <button type="button" class="default" @click="closeAddAccountDialog">Close</button>
416
+ </div>
417
+ </footer>
418
+ </div>
419
+ <div class="dialog-backdrop" :class="{ 'is-open': messageBox.open }" :aria-hidden="!messageBox.open"
420
+ @click="closeMessageBox"></div>
421
+ <div class="window active is-bright message-dialog" role="dialog" aria-modal="true"
422
+ aria-labelledby="message-box-title" aria-describedby="message-box-body" :aria-hidden="!messageBox.open"
423
+ :class="{ 'is-open': messageBox.open }">
424
+ <div class="title-bar">
425
+ <div class="title-bar-text" id="message-box-title" x-text="messageBox.title"></div>
426
+ <div class="title-bar-controls">
427
+ <button aria-label="Close" @click="closeMessageBox"></button>
428
+ </div>
429
+ </div>
430
+ <div class="window-body has-space message-dialog__body">
431
+ <div class="message-dialog__content">
432
+ <div class="message-dialog__icon" :class="messageBox.iconTone ? `is-${messageBox.iconTone}` : ''"
433
+ aria-hidden="true" x-show="messageBox.iconTone"></div>
434
+ <div class="message-dialog__copy">
435
+ <p id="message-box-body" x-text="messageBox.message"></p>
436
+ <template x-if="messageBox.details">
437
+ <pre class="code-box message-dialog__details" x-text="messageBox.details"></pre>
438
+ </template>
439
+ </div>
440
+ </div>
441
+ </div>
442
+ <footer class="message-dialog__actions">
443
+ <button x-show="messageBox.mode === 'confirm'" type="button" @click="cancelMessageBox"
444
+ x-text="messageBox.cancelLabel"></button>
445
+ <button x-show="messageBox.mode === 'confirm'" type="button" class="default" @click="confirmMessageBox"
446
+ x-text="messageBox.confirmLabel"></button>
447
+ <button x-show="messageBox.mode !== 'confirm'" type="button" class="default"
448
+ @click="closeMessageBox">OK</button>
449
+ </footer>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ <script defer src="/dashboard/index.js"></script>
454
+ <script defer src="https://unpkg.com/alpinejs@3.13.2/dist/cdn.min.js"></script>
455
+ </body>
456
+
457
+ </html>