bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
use super::features::browser_ui_feature_definitions;
use super::*;
use bucketwarden_browser_ui_e2e::browser_ui_e2e_authenticated_workflows;
use bucketwarden_browser_ui_shell::{
    browser_ui_accessibility_checks, browser_ui_api_endpoints, browser_ui_responsive_breakpoints,
    browser_ui_routes, browser_ui_security_policy,
};

pub fn browser_ui_manifest_json() -> String {
    let mut manifest_bytes = 0;
    let mut json = String::new();
    for _ in 0..4 {
        json = serde_json::to_string(&browser_ui_manifest_report(manifest_bytes))
            .expect("browser UI manifest is serializable");
        if json.len() == manifest_bytes {
            break;
        }
        manifest_bytes = json.len();
    }
    json
}

fn browser_ui_manifest_report(manifest_bytes: usize) -> BrowserUiProductReport {
    BrowserUiProductReport {
        boundary_id: BROWSER_UI_BOUNDARY_ID.to_string(),
        claim_tier: BROWSER_UI_CLAIM_TIER.to_string(),
        implementation_status: "implemented".to_string(),
        generated_at_epoch_seconds: 0,
        features: browser_ui_feature_statuses(),
        routes: browser_ui_routes(),
        api_endpoints: browser_ui_api_endpoints(),
        assets: browser_ui_assets_with_manifest_bytes(manifest_bytes),
        security: browser_ui_security_policy(),
        accessibility_checks: browser_ui_accessibility_checks(),
        responsive_breakpoints: browser_ui_responsive_breakpoints(),
        preference_keys: vec![
            "bucketwarden.ui.density".to_string(),
            "bucketwarden.ui.visibleColumns".to_string(),
            "bucketwarden.ui.reportFilters".to_string(),
            "bucketwarden.ui.refreshSeconds".to_string(),
        ],
        export_actions: vec![
            "download health report JSON".to_string(),
            "download config report JSON".to_string(),
            "download evidence export JSON".to_string(),
            "download audit log JSONL".to_string(),
        ],
        e2e_workflows: browser_ui_product_e2e_workflows(),
    }
}

pub(super) fn browser_ui_product_e2e_workflows() -> Vec<String> {
    let mut workflows = vec!["login -> overview -> health report -> logout".to_string()];
    workflows.extend(browser_ui_e2e_authenticated_workflows());
    workflows.extend([
        "login failure -> auth error state -> retry".to_string(),
        "responsive navigation and keyboard traversal".to_string(),
        "malformed response -> visible error state".to_string(),
    ]);
    workflows
}

pub fn browser_ui_html() -> String {
    browser_ui_html_for_base("/ui")
}

pub fn browser_ui_v1_html() -> String {
    browser_ui_html_for_base("/ui/v1")
}

fn browser_ui_html_for_base(ui_base: &str) -> String {
    format!(
        r#"<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'self'">
  <title>BucketWarden Console</title>
  <link rel="icon" type="image/png" href="{ui_base}/assets/favicon.png">
  <link rel="shortcut icon" type="image/png" href="{ui_base}/assets/favicon.png">
  <link rel="apple-touch-icon" href="{ui_base}/assets/bucketwarden-monogram.png">
  <link rel="stylesheet" href="{ui_base}/assets/app.css">
</head>
<body>
  <div id="app" data-boundary="{boundary}" data-claim-tier="{tier}" data-ui-feature="feat:bucketwarden.ui.browser.format.bytes-humanized feat:bucketwarden.ui.browser.format.numbers-commas">
    <aside class="shell-nav" aria-label="Primary">
      <a class="brand" href="{ui_base}" aria-label="BucketWarden overview">
        <img src="{ui_base}/assets/bucketwarden-monogram.png" alt="" width="42" height="42">
        <span>BucketWarden</span>
      </a>
      <p class="principal" data-current-principal data-ui-feature="feat:bucketwarden.ui.browser.auth.current-user">Signed out</p>
      <nav data-ui-feature="feat:bucketwarden.ui.browser.sidebar-order">{nav}</nav>
      <button type="button" data-action="logout" data-ui-feature="feat:bucketwarden.ui.browser.auth.logout">Sign out</button>
    </aside>
    <main>
      <header class="topbar" data-ui-feature="feat:bucketwarden.ui.browser.topbar-context-order">
        <div>
          <p class="route-kicker">BucketWarden Console</p>
          <h1 data-route-title>Overview</h1>
          <p class="route-summary" data-route-summary>Runtime status, reports, buckets, governance, audit, evidence, and settings.</p>
          <div class="context-strip" aria-label="Current context" data-ui-feature="feat:bucketwarden.ui.browser.context-chips.ordered-conditional">
            <span class="context-chip" data-context-session data-ui-feature="feat:bucketwarden.ui.browser.auth.current-user">Session: signed out</span>
            <span class="context-chip" data-context-tenant>Tenant: default</span>
            <span class="context-chip" data-context-bucket hidden>Bucket: none</span>
            <span class="context-chip" data-context-object hidden>Object: none</span>
          </div>
        </div>
        <div class="refresh-controls" data-ui-feature="feat:bucketwarden.ui.browser.refresh-realtime">
          <button type="button" data-action="refresh">Refresh</button>
          <label for="refresh-interval">Auto refresh</label>
          <select id="refresh-interval" data-refresh-interval aria-label="Auto refresh interval">
            <option value="0">Off</option>
            <option value="15">15s</option>
            <option value="30" selected>30s</option>
            <option value="60">60s</option>
          </select>
        </div>
      </header>
      <form class="action-filter-bar" data-form="global-filter" data-ui-feature="feat:bucketwarden.ui.browser.action-filter-bar feat:bucketwarden.ui.browser.action-filter-bar.enhanced feat:bucketwarden.ui.browser.action-filter-bar.scoped-state feat:bucketwarden.ui.browser.action-filter-bar.clear-reset feat:bucketwarden.ui.browser.search.route-scoped-visibility" aria-label="Search, filter, and sort current view">
        <label class="field-control" for="global-q"><span>Search</span><input id="global-q" name="q" placeholder="Search current view"></label>
        <label class="field-control" for="global-prefix"><span>Prefix</span><input id="global-prefix" name="prefix" placeholder="Folder prefix"></label>
        <label class="field-control" for="global-status"><span>Status</span><select id="global-status" name="status">
          <option value="all">All</option>
          <option value="allowed">Allowed</option>
          <option value="denied">Denied</option>
          <option value="failed">Failed</option>
        </select></label>
        <label class="field-control" for="global-sort"><span>Sort</span><select id="global-sort" name="sort">
          <option value="-sequence">Newest first</option>
          <option value="sequence">Oldest first</option>
          <option value="name">Name</option>
          <option value="key">Object key</option>
        </select></label>
        <span class="filter-scope" data-filter-scope>Scope: current view</span>
        <button type="submit">Apply</button>
        <button type="button" class="secondary-action" data-action="clear-filters">Clear</button>
      </form>
      <div class="content-inspector" data-ui-feature="feat:bucketwarden.ui.browser.visual-layout-ia feat:bucketwarden.ui.browser.shell.route-chrome-rules feat:bucketwarden.ui.browser.route-active-content-isolation">
        <div>
      <section id="login" class="panel" data-route-panel="login" data-ui-feature="feat:bucketwarden.ui.browser.auth.login" aria-labelledby="login-title">
        <img class="login-mark" src="{ui_base}/assets/bucketwarden-monogram.png" alt="" width="88" height="88">
        <h2 id="login-title">Operator Login</h2>
        <div class="session-card" data-view="login-authenticated" data-ui-feature="feat:bucketwarden.ui.browser.login.authenticated-session-state" hidden>
          <p data-view="login-session-summary">Signed in.</p>
          <button type="button" data-action="login-continue">Continue to overview</button>
          <button type="button" class="secondary-action" data-action="login-signout">Sign out</button>
        </div>
        <form data-form="login" data-ui-feature="feat:bucketwarden.ui.browser.login-submit feat:bucketwarden.ui.browser.login.error-state-polish">
          <label for="principal">Principal</label>
          <input id="principal" name="principal" autocomplete="username" required>
          <label for="identity-provider" data-ui-feature="feat:bucketwarden.ui.browser.identity-provider-selection">Identity Provider</label>
          <select id="identity-provider" name="identity_provider" data-identity-provider>
            <option value="custom-shared-secret">Shared secret</option>
          </select>
          <label for="secret">Shared Secret</label>
          <input id="secret" name="secret" type="password" autocomplete="current-password" required>
          <button type="submit">Sign in</button>
          <p class="login-status" role="status" aria-live="polite" data-auth-state>Unauthenticated</p>
        </form>
      </section>
      <section id="overview" class="dashboard overview-kpi-dashboard" data-route-panel="overview" aria-label="Operational overview">
        <article class="panel overview-summary" data-ui-feature="feat:bucketwarden.ui.browser.overview-dashboard feat:bucketwarden.ui.browser.overview.kpi-summary-only"><h2>Runtime KPIs</h2><dl class="kpi-grid" data-view="overview"></dl></article>
      </section>
      <section class="panel reports-workspace" data-route-panel="reports" data-ui-feature="feat:bucketwarden.ui.browser.reports-dashboard feat:bucketwarden.ui.browser.reports-diagnostics.boundary feat:bucketwarden.ui.browser.reports.category-workspace feat:bucketwarden.ui.browser.reports.freshness-actions feat:bucketwarden.ui.browser.reports.actionable-index feat:bucketwarden.ui.browser.reports.index-detail-separation"><h2>Reports</h2><div class="report-index-region" data-view="report-index"></div><div class="report-detail-grid" data-view="report-detail-region"><section class="report-detail-panel"><h3>Health</h3><dl data-view="route-report" data-ui-feature="feat:bucketwarden.ui.browser.health-report-view"></dl></section><section class="report-detail-panel"><h3>Configuration</h3><dl data-view="route-config" data-ui-feature="feat:bucketwarden.ui.browser.configuration-report-view feat:bucketwarden.ui.browser.reports.config"></dl></section><section class="report-detail-panel"><h3>Storage</h3><dl data-view="route-storage" data-ui-feature="feat:bucketwarden.ui.browser.reports.storage"></dl></section><section class="report-detail-panel"><h3>Governance</h3><dl data-view="route-report-governance" data-ui-feature="feat:bucketwarden.ui.browser.reports.governance"></dl></section><section class="report-detail-panel"><h3>Incident</h3><dl data-view="route-incident" data-ui-feature="feat:bucketwarden.ui.browser.incident-report-view feat:bucketwarden.ui.browser.reports.incident feat:bucketwarden.ui.browser.reports.detail-region feat:bucketwarden.ui.browser.reports.staleness-indicator"></dl></section></div></section>
      <section class="panel bucket-workspace" data-route-panel="buckets" data-has-selection="false" data-ui-feature="feat:bucketwarden.ui.browser.bucket-object-explorer feat:bucketwarden.ui.browser.bucket-list-view feat:bucketwarden.ui.browser.buckets.tabs feat:bucketwarden.ui.browser.bucket-list.full-page feat:bucketwarden.ui.browser.bucket-detail.full-page feat:bucketwarden.ui.browser.bucket-detail.route-state-separation feat:bucketwarden.ui.browser.object-detail.route-state-separation feat:bucketwarden.ui.browser.bucket-explorer.stable-file-browser-layout feat:bucketwarden.ui.browser.file-explorer.professional-affordances feat:bucketwarden.ui.browser.selection.route-reset feat:bucketwarden.ui.browser.empty-state.resource-panels feat:bucketwarden.ui.browser.empty-state.bucket-object-version"><h2>Buckets</h2><div class="explorer-shell"><section class="explorer-region bucket-collection-region" data-view="bucket-collection" aria-label="Bucket list"><h3>Bucket list</h3><div data-view="route-buckets"></div></section><section class="explorer-region bucket-detail-workspace" data-view="bucket-detail-workspace" aria-label="Bucket workspace"><div class="bucket-tabs" role="tablist" aria-label="Bucket detail tabs" data-ui-feature="feat:bucketwarden.ui.browser.bucket-tabs.semantic-tablist"><button id="bucket-tab-files" type="button" role="tab" data-bucket-tab="files" aria-controls="bucket-panel-files" data-ui-feature="feat:bucketwarden.ui.browser.bucket-detail.file-explorer-tab">File explorer</button><button id="bucket-tab-governance" type="button" role="tab" data-bucket-tab="governance" aria-controls="bucket-panel-governance" data-ui-feature="feat:bucketwarden.ui.browser.bucket-detail.governance-policy-tab">Governance and policy</button></div><section id="bucket-panel-files" data-bucket-panel="files" role="tabpanel" aria-labelledby="bucket-tab-files"><h3>Bucket detail</h3><dl data-view="bucket-detail" data-ui-feature="feat:bucketwarden.ui.browser.bucket-detail-view feat:bucketwarden.ui.browser.buckets.detail feat:bucketwarden.ui.browser.bucket-detail.route-state-separation"></dl><h3>File explorer</h3><nav class="breadcrumbs" data-view="file-breadcrumbs" data-ui-feature="feat:bucketwarden.ui.browser.file-explorer.breadcrumbs feat:bucketwarden.ui.browser.file-explorer.path-breadcrumb feat:bucketwarden.ui.browser.file-explorer.professional-affordances" aria-label="Object prefix breadcrumbs"></nav><div class="file-explorer-grid"><div data-view="file-tree" data-ui-feature="feat:bucketwarden.ui.browser.file-explorer.tree feat:bucketwarden.ui.browser.file-explorer.collapsible-tree feat:bucketwarden.ui.browser.file-explorer.icons feat:bucketwarden.ui.browser.file-explorer.professional-affordances"></div><div data-view="object-list" data-ui-feature="feat:bucketwarden.ui.browser.object-list-view feat:bucketwarden.ui.browser.objects.table feat:bucketwarden.ui.browser.objects.prefix-tree feat:bucketwarden.ui.browser.objects.breadcrumbs feat:bucketwarden.ui.browser.objects.detail-drawer feat:bucketwarden.ui.browser.objects.metadata feat:bucketwarden.ui.browser.file-explorer.object-table feat:bucketwarden.ui.browser.file-explorer.icons feat:bucketwarden.ui.browser.file-explorer.professional-affordances"></div></div><h3>Versions</h3><div data-view="version-history" data-ui-feature="feat:bucketwarden.ui.browser.object-version-history-view feat:bucketwarden.ui.browser.versions.table feat:bucketwarden.ui.browser.versions.latest-marker feat:bucketwarden.ui.browser.versions.delete-markers feat:bucketwarden.ui.browser.object-detail.route-state-separation"></div><div data-view="version-actions" data-ui-feature="feat:bucketwarden.ui.browser.versions.download feat:bucketwarden.ui.browser.versions.restore-copy feat:bucketwarden.ui.browser.object-detail.download feat:bucketwarden.ui.browser.object-detail.crud-actions feat:bucketwarden.ui.browser.governance.object-crud-gates"></div><h3>Object detail</h3><dl data-view="object-detail" data-ui-feature="feat:bucketwarden.ui.browser.object-detail.full-page feat:bucketwarden.ui.browser.object-detail.metadata feat:bucketwarden.ui.browser.object-detail.governance feat:bucketwarden.ui.browser.object-detail.route-state-separation feat:bucketwarden.ui.browser.governance.object-crud-gates"></dl><h3>Preview</h3><div data-view="object-preview" data-ui-feature="feat:bucketwarden.ui.browser.object-detail.preview"></div></section><section id="bucket-panel-governance" data-bucket-panel="governance" role="tabpanel" aria-labelledby="bucket-tab-governance"><h3>Bucket governance and policy</h3><dl data-view="bucket-governance" data-ui-feature="feat:bucketwarden.ui.browser.governance.bucket-versioning feat:bucketwarden.ui.browser.governance.bucket-object-lock feat:bucketwarden.ui.browser.governance.bucket-retention-defaults feat:bucketwarden.ui.browser.governance.lifecycle-rules feat:bucketwarden.ui.browser.governance.policy-acl-summary feat:bucketwarden.ui.browser.bucket-detail.governance-policy-tab feat:bucketwarden.ui.browser.governance.bucket-crud-gates"></dl><div class="governance-gates" data-view="bucket-governance-gates" data-ui-feature="feat:bucketwarden.ui.browser.governance.bucket-crud-gates feat:bucketwarden.ui.browser.governance.gated-action-visual-state"></div></section></section></div></section>
      <section class="panel governance-workspace" data-route-panel="governance" data-ui-feature="feat:bucketwarden.ui.browser.governance.scope-separated-route feat:bucketwarden.ui.browser.global-governance.dashboard feat:bucketwarden.ui.browser.global-governance.findings feat:bucketwarden.ui.browser.governance.structured-findings-gates feat:bucketwarden.ui.browser.governance.global-crud-gates feat:bucketwarden.ui.browser.governance.global-policy-crud-gates feat:bucketwarden.ui.browser.governance.global-retention-posture feat:bucketwarden.ui.browser.governance.global-legal-hold-posture feat:bucketwarden.ui.browser.governance.bucket-scope-exclusion feat:bucketwarden.ui.browser.governance.object-scope-exclusion feat:bucketwarden.ui.browser.governance.gated-action-visual-state feat:bucketwarden.ui.browser.tenant-governance.views feat:bucketwarden.ui.browser.empty-state.resource-panels feat:bucketwarden.ui.browser.empty-state.governance-records"><h2>Global Governance</h2><p class="route-note">Global and tenant-level governance only. Bucket and object governance remain on their detail pages.</p><p class="route-note" data-view="bucket-governance-remediation" data-ui-feature="feat:bucketwarden.ui.browser.governance.bucket-scope-exclusion">Bucket governance is bucket-scoped. Open a bucket detail governance and policy tab for bucket policy, lifecycle, ACL, object-lock, and retention-default actions.</p><p class="route-note" data-view="object-governance-remediation" data-ui-feature="feat:bucketwarden.ui.browser.governance.object-scope-exclusion">Object governance is object-scoped. Open an object detail workflow for version retention, legal hold, delete-marker, and object policy actions.</p><dl data-view="global-governance-dashboard" data-ui-feature="feat:bucketwarden.ui.browser.global-governance.dashboard"></dl><h3>Tenant policy controls</h3><p class="route-note" data-view="global-policy-mode" data-ui-feature="feat:bucketwarden.ui.browser.governance.global-policy-crud-gates">Read-only report mode: global policy mutation requires a runtime tenant governance API.</p><div class="governance-gates" data-view="global-governance-gates" data-ui-feature="feat:bucketwarden.ui.browser.governance.global-crud-gates feat:bucketwarden.ui.browser.governance.global-policy-crud-gates feat:bucketwarden.ui.browser.governance.gated-action-visual-state"></div><p role="status" data-view="global-governance-action"></p><h3>Tenant retention posture</h3><p class="route-note" data-view="global-retention-remediation" data-ui-feature="feat:bucketwarden.ui.browser.governance.global-retention-posture">Retention remediation is bucket-scoped. Open a bucket detail governance tab to change defaults where supported.</p><dl data-view="global-retention-summary" data-ui-feature="feat:bucketwarden.ui.browser.tenant-governance.views feat:bucketwarden.ui.browser.governance.global-retention-posture"></dl><h3>Tenant legal hold posture</h3><p class="route-note" data-view="global-legal-hold-remediation" data-ui-feature="feat:bucketwarden.ui.browser.governance.global-legal-hold-posture">Legal-hold remediation is object-scoped. Open an object detail governance workflow to apply or lift holds.</p><dl data-view="global-legal-hold-summary" data-ui-feature="feat:bucketwarden.ui.browser.tenant-governance.views feat:bucketwarden.ui.browser.governance.global-legal-hold-posture"></dl><h3>Findings</h3><div data-view="global-governance-findings" data-ui-feature="feat:bucketwarden.ui.browser.global-governance.findings"></div></section>
      <section class="panel" data-route-panel="audit" data-ui-feature="feat:bucketwarden.ui.browser.audit-log-view feat:bucketwarden.ui.browser.audit-search-filter-sort feat:bucketwarden.ui.browser.audit.single-filter-source"><h2>Audit</h2><p class="route-note">Use the route filter bar above to search, filter by outcome, and sort audit events.</p><div data-view="route-audit" data-ui-feature="feat:bucketwarden.ui.browser.audit.table-layout"></div></section>
      <section class="panel evidence-workspace" data-route-panel="evidence" data-ui-feature="feat:bucketwarden.ui.browser.evidence-view feat:bucketwarden.ui.browser.evidence.list feat:bucketwarden.ui.browser.evidence.detail-preview feat:bucketwarden.ui.browser.evidence.selection-detail-readability"><h2>Evidence</h2><div class="page-actions"><button type="button" data-action="download-evidence" data-ui-feature="feat:bucketwarden.ui.browser.evidence-export-download feat:bucketwarden.ui.browser.evidence.export">Download export</button><p role="status" data-view="evidence-download"></p></div><div class="detail-grid evidence-detail-grid"><div data-view="route-evidence" data-ui-feature="feat:bucketwarden.ui.browser.evidence.table-layout feat:bucketwarden.ui.browser.evidence.selection-detail-readability"></div><aside class="detail-panel" data-view="evidence-detail" aria-label="Evidence detail" data-ui-feature="feat:bucketwarden.ui.browser.evidence.selection-detail-readability"></aside></div></section>
      <section class="panel settings-workspace" data-route-panel="settings" data-ui-feature="feat:bucketwarden.ui.browser.preferences-view feat:bucketwarden.ui.browser.preferences.user-settings feat:bucketwarden.ui.browser.settings.admin-settings feat:bucketwarden.ui.browser.settings.sectioned-management feat:bucketwarden.ui.browser.settings.ia-section-separation feat:bucketwarden.ui.browser.empty-state.resource-panels feat:bucketwarden.ui.browser.empty-state.admin-tenants-users-roles"><h2>Settings</h2><div class="settings-grid"><section class="management-section preferences-section" data-settings-section="preferences"><h3>Preferences</h3><form class="toolbar form-grid" data-form="preferences" data-ui-feature="feat:bucketwarden.ui.browser.preferences-persistence feat:bucketwarden.ui.browser.preferences.user-settings"><label for="pref-density">Density</label><input id="pref-density" name="bucketwarden.ui.density"><label for="pref-refresh">Refresh seconds</label><input id="pref-refresh" name="bucketwarden.ui.refreshSeconds" inputmode="numeric"><label for="pref-columns">Visible columns</label><input id="pref-columns" name="bucketwarden.ui.visibleColumns"><label for="pref-filters">Report filters</label><input id="pref-filters" name="bucketwarden.ui.reportFilters"><button type="submit">Save</button></form><p role="status" data-view="preferences-save"></p><dl class="preference-summary" data-view="route-preferences"></dl></section><section class="management-section tenant-section" data-settings-section="tenant"><h3 data-ui-feature="feat:bucketwarden.ui.browser.tenants.selector feat:bucketwarden.ui.browser.tenant-management.views">Tenant scope</h3><dl data-view="tenant-scope" data-ui-feature="feat:bucketwarden.ui.browser.tenants.scoped-requests feat:bucketwarden.ui.browser.tenant-governance.views"></dl></section><section class="management-section identity-section" data-settings-section="identity"><h3>Users</h3><div data-view="admin-users" data-ui-feature="feat:bucketwarden.ui.browser.admin.users-list feat:bucketwarden.ui.browser.user-management.humans"></div><h3>User detail</h3><dl data-view="admin-user-detail" data-ui-feature="feat:bucketwarden.ui.browser.admin.user-detail feat:bucketwarden.ui.browser.user-management.humans"></dl></section><section class="management-section access-section" data-settings-section="access"><h3>Roles and permissions</h3><div data-view="admin-roles" data-ui-feature="feat:bucketwarden.ui.browser.admin.roles-list feat:bucketwarden.ui.browser.role-management.views"></div><h3>Role assignments</h3><div data-view="admin-role-assignments" data-ui-feature="feat:bucketwarden.ui.browser.admin.role-assignments feat:bucketwarden.ui.browser.role-management.views"></div><h3>Effective permissions</h3><div data-view="admin-effective-permissions" data-ui-feature="feat:bucketwarden.ui.browser.admin.effective-permissions feat:bucketwarden.ui.browser.permission-management.views"></div><h3>Groups</h3><div data-view="admin-groups" data-ui-feature="feat:bucketwarden.ui.browser.group-management.views"></div></section></div></section>
      <section class="state-panel" aria-live="polite" role="status" data-ui-feature="feat:bucketwarden.ui.browser.auth.route-guards feat:bucketwarden.ui.browser.status.blank-strip-suppression">
        <span data-loading hidden>Loading</span>
        <span data-empty hidden>No records</span>
        <span data-denied data-ui-feature="feat:bucketwarden.ui.browser.auth.permission-denied" hidden>Permission denied</span>
        <span data-error data-ui-feature="feat:bucketwarden.ui.browser.auth.session-expiry" hidden></span>
        <span data-disabled-action-reason data-ui-feature="feat:bucketwarden.ui.browser.auth.disabled-action-reasons" hidden></span>
      </section>
        </div>
        <aside class="secondary-inspector" aria-label="Selection inspector" data-inspector-state="open" data-ui-feature="feat:bucketwarden.ui.browser.secondary-inspector-drawer feat:bucketwarden.ui.browser.secondary-inspector.visibility-rules feat:bucketwarden.ui.browser.secondary-inspector.collapse-expand feat:bucketwarden.ui.browser.secondary-inspector.row-toggle feat:bucketwarden.ui.browser.secondary-inspector.readable-layout">
          <div class="inspector-head"><h2>Inspector</h2><button type="button" class="secondary-action" data-action="toggle-inspector" aria-expanded="true">Collapse</button></div>
          <dl class="inspector-list" data-view="selection-inspector"></dl>
        </aside>
      </div>
    </main>
  </div>
  <script src="{ui_base}/assets/app.js"></script>
</body>
</html>"#,
        boundary = BROWSER_UI_BOUNDARY_ID,
        tier = BROWSER_UI_CLAIM_TIER,
        ui_base = ui_base,
        nav = browser_ui_routes()
            .into_iter()
            .filter(|route| !route.route.starts_with("/ui/v1"))
            .map(|route| format!(
                r#"<a href="{href}" data-ui-feature="{feature}">{label}</a>"#,
                href = escape_html(&route.route.replacen("/ui", ui_base, 1)),
                feature = escape_html(&route.feature_id),
                label = escape_html(&route.label)
            ))
            .collect::<Vec<_>>()
            .join("")
    )
}

pub fn browser_ui_css() -> &'static str {
    r#":root{color-scheme:light;--color-bg-app:#f4f7f9;--color-bg-surface:#fff;--color-bg-sidebar:#13232d;--color-text-primary:#17212b;--color-text-muted:#5c6670;--color-border-subtle:#d8dde3;--color-action-primary:#176b87;--color-status-success:#247a4b;--color-status-danger:#a53333;--space-2:8px;--space-3:12px;--space-4:16px;--radius-1:4px;--radius-2:8px;--font-size-sm:14px;--table-row-height:40px;--focus-ring:3px solid #88c8dc;font-family:Inter,Segoe UI,Arial,sans-serif}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;color:var(--color-text-primary);background:var(--color-bg-app)}#app{min-height:100vh;display:grid;grid-template-columns:256px 1fr}.shell-nav{background:var(--color-bg-sidebar);color:#fff;padding:20px}.brand{display:flex;align-items:center;gap:10px;color:#fff;text-decoration:none;font-size:20px;font-weight:800;line-height:1;margin:0 0 10px}.brand img{width:42px;height:42px;object-fit:contain}.login-mark{width:74px;height:74px;object-fit:contain;display:block;margin:0 0 14px}.principal{color:#c3dbe4;margin:0 0 18px}.shell-nav nav{display:grid;gap:6px}.shell-nav a{color:#dcecf2;text-decoration:none;padding:9px 10px;border-radius:var(--radius-2)}.shell-nav a.is-active,.shell-nav a:focus,.shell-nav a:hover{outline:2px solid #9bd6e8;background:#203744}main{padding:24px;display:grid;gap:18px;align-content:start}.topbar{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:14px;align-items:start}.route-kicker{margin:0;color:var(--color-text-muted);font-size:var(--font-size-sm);font-weight:700}.topbar h1{font-size:28px;line-height:1.15;margin:2px 0 4px}.route-summary,.muted,.route-note{margin:0;color:var(--color-text-muted)}.context-strip{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.context-chip{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fff;padding:6px 8px;font-size:var(--font-size-sm);font-weight:700}.refresh-controls{display:grid;grid-template-columns:auto minmax(96px,130px);gap:8px;align-items:end}.action-filter-bar{display:grid;grid-template-columns:minmax(180px,1.3fr) minmax(140px,1fr) minmax(120px,.8fr) minmax(140px,.9fr) minmax(120px,.7fr) auto auto;gap:10px;align-items:end;background:#fff;border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);padding:12px}.field-control{display:grid;gap:5px;margin:0}.field-control span{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--color-text-muted)}.dashboard{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.panel,.state-panel{background:var(--color-bg-surface);border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);padding:var(--space-4)}.panel[data-route-panel=login]{max-width:560px;width:100%;margin:28px auto 0;padding:28px}.panel h2{font-size:18px;margin:0 0 10px}.toolbar{display:grid;grid-template-columns:repeat(3,minmax(140px,1fr)) auto;gap:10px;align-items:end;margin-bottom:12px}.form-grid{grid-template-columns:repeat(2,minmax(180px,1fr)) auto}.explorer-shell{display:grid;grid-template-columns:minmax(260px,320px) minmax(520px,1fr);gap:16px;align-items:start;overflow-x:auto}.file-explorer-grid{display:grid;grid-template-columns:minmax(240px,300px) minmax(440px,1fr);gap:12px;overflow-x:auto}.bucket-tabs{display:flex;gap:0;margin:8px 0 12px;border-bottom:1px solid var(--color-border-subtle)}.bucket-tabs [role=tab]{appearance:none;min-height:40px;margin:0 0 -1px;background:#f8fafb;color:var(--color-text-muted);border:1px solid transparent;border-bottom:0;border-radius:6px 6px 0 0;padding:9px 14px}.bucket-tabs [role=tab]:hover{background:#eef4f7}.bucket-tabs [aria-selected=true]{background:#fff;border-color:var(--color-border-subtle);box-shadow:inset 0 3px 0 var(--color-action-primary);color:var(--color-text-primary);font-weight:800}.breadcrumbs{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin:8px 0 12px;border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fbfcfd;padding:7px 10px}.breadcrumb-item{margin:0;border:0;background:transparent;color:var(--color-action-primary);padding:4px 2px;font-weight:700}.breadcrumb-item[aria-current=page]{color:var(--color-text-primary);cursor:default}.breadcrumb-separator{color:var(--color-text-muted)}.content-inspector{display:grid;grid-template-columns:minmax(0,1fr) minmax(320px,380px);gap:18px}body[data-inspector-visible=false] .content-inspector{grid-template-columns:1fr}body[data-inspector-visible=false] .secondary-inspector{display:none!important}.secondary-inspector{position:sticky;top:18px;align-self:start;background:#fbfcfd;border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);padding:var(--space-4);min-width:0}.secondary-inspector[hidden]{display:none!important}.secondary-inspector.is-collapsed .inspector-list{display:none}.secondary-inspector.is-collapsed{max-height:72px;overflow:hidden}.inspector-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.inspector-list{grid-template-columns:minmax(92px,120px) minmax(0,1fr);align-items:start}.inspector-list dd{font-weight:700;line-height:1.35}.explorer{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fbfcfd;min-height:220px;padding:8px;overflow:auto}.explorer ul{list-style:none;margin:0;padding:0}.explorer li{margin:2px 0}.explorer button{width:100%;margin:0;background:transparent;color:var(--color-text-primary);text-align:left;border:1px solid transparent;border-radius:var(--radius-1);font-weight:600;cursor:pointer}.explorer button:hover,.explorer button:focus{background:#eaf3f6;border-color:#9bd6e8}.explorer button.is-selected,.explorer button[aria-pressed=true]{background:#dcecf2;border-color:var(--color-action-primary);box-shadow:inset 3px 0 0 var(--color-action-primary)}.entity-button{display:grid;grid-template-columns:22px minmax(0,1fr) auto;gap:6px;align-items:center}.entity-icon{font-size:16px;text-align:center}.entity-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.entity-meta{grid-column:3;color:var(--color-text-muted);font-size:var(--font-size-sm);font-weight:600;white-space:nowrap}.bucket-list .entity-icon{color:#176b87}.file-tree .entity-icon{color:#a35a00}.object-table .entity-icon{color:#247a4b}.governance-gates{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0 14px}.gate-control{display:grid;gap:4px}.gate-note{font-size:12px;color:var(--color-text-muted)}.secondary-action.is-gated{background:#f3f5f6;color:var(--color-text-muted);border-style:dashed}.gate-control[data-gate-state=read-only]{opacity:.95}.bucket-workspace[data-has-selection=false] .bucket-detail-workspace{display:none}.bucket-workspace[data-has-selection=true] .bucket-detail-workspace{display:block}.report-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(260px,1fr));gap:12px}.report-detail-panel{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fbfcfd;padding:12px}.login-status{border-left:3px solid var(--color-border-subtle);padding-left:8px}.evidence-detail-grid table tr{cursor:pointer}.evidence-detail-grid table tr[aria-selected=true]{background:#eaf3f6}.empty-state{border:1px dashed var(--color-border-subtle);border-radius:var(--radius-2);color:var(--color-text-muted);padding:14px;background:#fbfcfd}.session-card,.detail-panel,.management-section,.report-card{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fbfcfd;padding:12px}.report-index-grid{display:grid;grid-template-columns:repeat(2,minmax(260px,1fr));gap:12px;margin-bottom:14px}.report-card h3{margin:0 0 4px}.report-card code{display:block;margin:8px 0;color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis}.page-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:12px}.detail-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(260px,360px);gap:14px;align-items:start}.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(280px,1fr));gap:14px;align-items:start}.management-section h3{margin-top:0}.preference-summary{grid-template-columns:minmax(260px,1fr) minmax(120px,220px)}.preview-panel{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fbfcfd;padding:12px;color:var(--color-text-muted);white-space:pre-wrap}.state-panel{display:none}.state-panel:has([data-loading]:not([hidden])),.state-panel:has([data-empty]:not([hidden])),.state-panel:has([data-denied]:not([hidden])),.state-panel:has([data-error]:not(:empty)),.state-panel:has([data-disabled-action-reason]:not([hidden])){display:block}dl{display:grid;grid-template-columns:minmax(120px,180px) 1fr;gap:6px 12px}dt{color:var(--color-text-muted)}dd{margin:0;font-weight:700;overflow-wrap:anywhere}table{width:100%;border-collapse:collapse;table-layout:fixed}th,td{border-bottom:1px solid var(--color-border-subtle);min-height:var(--table-row-height);padding:8px;text-align:left;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}label{display:block;font-weight:600;margin-top:10px}input,select{width:100%;min-height:38px;border:1px solid var(--color-border-subtle);border-radius:6px;padding:8px;background:#fff}button{margin-top:12px;min-height:38px;border:0;border-radius:6px;background:var(--color-action-primary);color:#fff;padding:8px 14px;font-weight:700}.secondary-action{border:1px solid var(--color-border-subtle);background:#fff;color:var(--color-text-primary)}button:disabled{opacity:.64;cursor:not-allowed}button:focus,input:focus,select:focus,.brand:focus{outline:var(--focus-ring);outline-offset:2px}[role=status]{color:var(--color-text-muted)}[role=status]:empty{display:none}[data-denied]{color:var(--color-status-danger);font-weight:700}.is-error{color:var(--color-status-danger)}.is-ok{color:var(--color-status-success)}body[data-route-chrome=login] #app{grid-template-columns:1fr}body[data-route-chrome=login] .shell-nav{display:none}body[data-route-chrome=login] .topbar{display:none}body[data-route-chrome=login] main{min-height:100vh;place-content:center;background:linear-gradient(180deg,#eef4f7 0,#f8fafb 100%)}.overview-kpi-dashboard{grid-template-columns:1fr}.overview-summary{max-width:980px}.kpi-grid{grid-template-columns:repeat(4,minmax(130px,1fr));gap:10px}.kpi-grid dt{font-size:12px;text-transform:uppercase;letter-spacing:.04em}.kpi-grid dd{font-size:24px;line-height:1.1}.explorer-region{min-width:0}.explorer-region>h3{margin:0 0 8px}@media(max-width:1180px){.content-inspector,.explorer-shell,.file-explorer-grid,.detail-grid,.settings-grid{grid-template-columns:1fr}.secondary-inspector{position:relative;top:auto}.action-filter-bar{grid-template-columns:repeat(2,minmax(0,1fr))}.filter-scope{grid-column:1/-1}}@media(max-width:760px){#app{grid-template-columns:1fr}.shell-nav{position:relative}.dashboard,.toolbar,.explorer-shell,.topbar,.action-filter-bar,.content-inspector,.refresh-controls,.file-explorer-grid,.report-index-grid{grid-template-columns:1fr}main{padding:14px}dl{grid-template-columns:1fr}.shell-nav nav{grid-template-columns:repeat(2,minmax(0,1fr))}.secondary-inspector{position:relative;top:auto;order:2}.topbar h1{font-size:24px}.panel[data-route-panel=login]{margin-top:10px;padding:18px}}"#
}

pub fn browser_ui_js() -> &'static str {
    r#"'use strict';
const uiBase=location.pathname==='/ui/v1'||location.pathname.startsWith('/ui/v1/')?'/ui/v1':'/ui';
const apiBase=uiBase==='/ui/v1'?'/api/v1':'/ui/api';
function routePath(suffix=''){return `${uiBase}${suffix}`}
function apiPath(suffix=''){return `${apiBase}${suffix}`}
const routeMap=new Map([[routePath(),'overview'],[routePath('/login'),'login'],[routePath('/logout'),'logout'],[routePath('/reports'),'reports'],[routePath('/buckets'),'buckets'],[routePath('/governance'),'governance'],[routePath('/audit'),'audit'],[routePath('/evidence'),'evidence'],[routePath('/settings'),'settings']]);
function bucketDetailPath(bucket){return `${routePath('/buckets')}/${enc(bucket)}`}
function objectDetailPath(bucket,key){return `${bucketDetailPath(bucket)}/objects/${enc(key)}`}
function routeForPath(path){if(routeMap.has(path))return routeMap.get(path);if(path.startsWith(`${routePath('/buckets')}/`))return 'buckets';return 'overview'}
const state={principal:null,session:null,route:routeForPath(location.pathname),returnTo:null,selectedBucket:null,selectedObject:null,activeBucketTab:'files',inspectorCollapsed:false,collapsedPrefixes:new Set(),filters:{query:'',prefix:'',status:'all',sort:'-sequence'},preferences:{density:'comfortable',refreshSeconds:30}};
const routeMeta={login:['Operator Login','Authenticate with a configured principal and shared secret.'],logout:['Sign Out','Clear the browser session and return to login.'],overview:['Overview','Runtime status, reports, buckets, governance, audit, evidence, and settings.'],reports:['Reports','Health, configuration, storage, incident, and governance report summaries.'],buckets:['Buckets','Browse buckets, folders, objects, metadata, and object-specific versions.'],governance:['Governance','Global and tenant-level policy, retention, legal-hold posture, and governance findings.'],audit:['Audit','Search, filter, sort, and inspect operator and runtime audit events.'],evidence:['Evidence','Review evidence bundles and export runtime proof records.'],settings:['Settings','Manage operator display, refresh, columns, and report preferences.']};
const routeChromeRules={login:{filters:false,refresh:false,inspector:false,selection:false},logout:{filters:false,refresh:false,inspector:false,selection:false},overview:{filters:false,refresh:true,inspector:false,selection:false},reports:{filters:false,refresh:true,inspector:false,selection:false},buckets:{filters:true,refresh:true,inspector:true,selection:true},governance:{filters:false,refresh:true,inspector:false,selection:false},audit:{filters:true,refresh:true,inspector:false,selection:false},evidence:{filters:false,refresh:true,inspector:false,selection:false},settings:{filters:false,refresh:true,inspector:false,selection:false}};
const governanceGateCatalog={
  global:[{action:'create-global-policy',label:'Create global policy',requires:'tenant governance API',confirmation:'not runtime-enabled',available:false,unavailable:'Global policy CRUD is tracked but this runtime exposes read-only tenant governance reports.'},{action:'update-global-policy',label:'Update global policy',requires:'tenant governance API',confirmation:'not runtime-enabled',available:false,unavailable:'Global policy updates are tracked but not runtime-enabled in this browser UI.'},{action:'retire-global-policy',label:'Retire global policy',requires:'tenant governance API',confirmation:'not runtime-enabled',available:false,unavailable:'Global policy retirement is tracked but not runtime-enabled in this browser UI.'}],
  bucket:[{action:'update-bucket-policy',label:'Update bucket policy',requires:'bucket governance permission',confirmation:'required'},{action:'update-lifecycle-rule',label:'Update lifecycle rule',requires:'bucket lifecycle permission',confirmation:'required'},{action:'update-retention-defaults',label:'Update retention defaults',requires:'bucket object lock permission',confirmation:'required'}],
  object:[{action:'apply-legal-hold',label:'Apply legal hold',requires:'object governance permission',confirmation:'reason required'},{action:'lift-legal-hold',label:'Lift legal hold',requires:'object governance permission',confirmation:'reason required'},{action:'update-retention',label:'Update retention',requires:'object retention permission',confirmation:'required'}]
};
let refreshTimer=null;
const statusNode=document.querySelector('[data-auth-state]');
const principalNode=document.querySelector('[data-current-principal]');
const providerSelect=document.querySelector('[data-identity-provider]');
const routeTitleNode=document.querySelector('[data-route-title]');
const routeSummaryNode=document.querySelector('[data-route-summary]');
const tenantContextNode=document.querySelector('[data-context-tenant]');
const bucketContextNode=document.querySelector('[data-context-bucket]');
const objectContextNode=document.querySelector('[data-context-object]');
const sessionContextNode=document.querySelector('[data-context-session]');
const refreshIntervalNode=document.querySelector('[data-refresh-interval]');
const loadingNode=document.querySelector('[data-loading]');
const emptyNode=document.querySelector('[data-empty]');
const deniedNode=document.querySelector('[data-denied]');
const errorNode=document.querySelector('[data-error]');
const disabledActionReasonNode=document.querySelector('[data-disabled-action-reason]');
const loginForm=document.querySelector('[data-form=login]');
const loginButton=loginForm?.querySelector('button[type=submit]');
const loginAuthenticatedPanel=document.querySelector('[data-view=login-authenticated]');
function token(){return state.session?.access_key_id||sessionStorage.getItem('bwui_access_key')||''}
function setDisabledActionReason(message=''){if(!disabledActionReasonNode)return;disabledActionReasonNode.hidden=!message;text(disabledActionReasonNode,message)}
function clearSession(message='Unauthenticated'){state.session=null;state.principal=null;sessionStorage.removeItem('bwui_access_key');principalNode.textContent='Signed out';statusNode.textContent=message;statusNode.className='';setDisabledActionReason('missing authenticated session')}
function setStatus(kind,message){loadingNode.hidden=kind!=='loading';emptyNode.hidden=kind!=='empty';deniedNode.hidden=kind!=='denied';errorNode.hidden=kind!=='error';deniedNode.textContent=kind==='denied'?(message||'Permission denied'):'';errorNode.textContent=kind==='error'?message:'';if(kind!=='denied'&&token())setDisabledActionReason('')}
function isAuthExpired(error){return error.status===401||error.code==='SessionExpired'||error.code==='SessionInvalid'||error.code==='AuthenticationFailed'}
function handleApiFailure(error){if(error.status===403||error.code==='PermissionDenied'){setDisabledActionReason(error.message||'missing operator permission');setStatus('denied',error.message||'Permission denied for this action');renderInspector('permission denied');return}if(isAuthExpired(error)){const expired=error.code==='SessionExpired';clearSession(expired?'Session expired. Sign in again.':'Unauthenticated');state.returnTo=location.pathname===routePath('/login')?state.returnTo:location.pathname;navigate(routePath('/login'),false);return}setStatus('error',error.message||error.code||'Request failed')}
async function api(path,options={}){setStatus('loading','');const headers=Object.assign({'accept':'application/json'},options.headers||{});if(token())headers['x-bucketwarden-ui-access-key']=token();if(options.body&&!headers['content-type'])headers['content-type']='application/json';const response=await fetch(path,Object.assign({},options,{headers}));const payload=await response.json().catch(()=>({code:'InvalidJson',message:'Response was not JSON'}));if(!response.ok){const error=Object.assign({status:response.status},payload);handleApiFailure(error);throw error}setStatus('', '');return payload}
function text(node,value){node.textContent=value==null?'':String(value)}
function enc(value){return encodeURIComponent(String(value||''))}
function renderEmpty(selector,message,resource='record'){const host=document.querySelector(selector);host.replaceChildren();const node=document.createElement('div');node.className='empty-state';node.dataset.emptyResource=resource;node.setAttribute('role','status');node.setAttribute('aria-label',`${resource} empty state`);text(node,message);host.append(node);setStatus('empty','')}
function renderExplorerEmpty(selector,message,resource='record'){const host=document.querySelector(selector);host.replaceChildren();const explorer=document.createElement('div');explorer.className='explorer';const node=document.createElement('div');node.className='empty-state';node.dataset.emptyResource=resource;node.setAttribute('role','status');node.setAttribute('aria-label',`${resource} empty state`);text(node,message);explorer.append(node);host.append(explorer);setStatus('empty','')}
function formatNumber(value){const numeric=Number(value);return Number.isFinite(numeric)?new Intl.NumberFormat('en-US').format(numeric):String(value??'')}
function formatBytes(value){const bytes=Number(value);if(!Number.isFinite(bytes))return String(value??'');const units=['B','KB','MB','GB','TB'];let size=bytes;let unit=0;while(size>=1024&&unit<units.length-1){size=size/1024;unit+=1}return `${unit===0?formatNumber(size):size.toFixed(size>=10?1:2)} ${units[unit]}`}
function formatValue(label,value){if(value===null||value===undefined||value==='')return 'none';const name=String(label||'').toLowerCase();if(name.includes('bytes')||name.includes('size')||name.includes('content_length'))return formatBytes(value);if(typeof value==='number')return formatNumber(value);return String(value)}
function renderPairs(selector,pairs,emptyMessage='No records found',resource='record'){const node=document.querySelector(selector);node.replaceChildren();if(!pairs.length){renderEmpty(selector,emptyMessage,resource);return}for(const [key,value]of pairs){const dt=document.createElement('dt');const dd=document.createElement('dd');text(dt,key);text(dd,formatValue(key,value));node.append(dt,dd)}}
function routeChrome(route,key){return Boolean(routeChromeRules[route]?.[key])}
function inspectorAllowed(){return routeChrome(state.route,'inspector')&&(state.route==='buckets'?Boolean(state.selectedBucket):state.route==='governance'?Boolean(state.selectedObject):false)}
function setInspectorCollapsed(collapsed){state.inspectorCollapsed=collapsed;const inspector=document.querySelector('.secondary-inspector');const button=document.querySelector('[data-action=toggle-inspector]');if(inspector){inspector.hidden=!inspectorAllowed();inspector.dataset.inspectorState=collapsed?'collapsed':'open';inspector.classList.toggle('is-collapsed',collapsed)}if(button){button.setAttribute('aria-expanded',collapsed?'false':'true');text(button,collapsed?'Expand':'Collapse')}}
function renderInspector(reason='Ready'){setInspectorCollapsed(state.inspectorCollapsed);if(!inspectorAllowed())return;renderPairs('[data-view=selection-inspector]',[
  ['Route',routeMeta[state.route]?.[0]||state.route],
  ['Tenant','default'],
  ['Principal',state.session?.principal_id||'signed out'],
  ['Bucket',state.selectedBucket||'none'],
  ['Object',state.selectedObject||'none'],
  ['Filter',state.filters.query||state.filters.prefix||state.filters.status!=='all'?`${state.filters.query||'*'} / ${state.filters.prefix||'*'} / ${state.filters.status}`:'none'],
  ['State',reason]
])}
function renderTable(selector,rows,columns,emptyMessage='No records found',resource='record'){const host=document.querySelector(selector);host.replaceChildren();if(!rows.length){renderEmpty(selector,emptyMessage,resource);return}const table=document.createElement('table');const thead=document.createElement('thead');const headRow=document.createElement('tr');for(const col of columns){const th=document.createElement('th');text(th,col.label);headRow.append(th)}thead.append(headRow);table.append(thead);const body=document.createElement('tbody');for(const row of rows){const tr=document.createElement('tr');tr.tabIndex=0;for(const col of columns){const td=document.createElement('td');text(td,formatValue(col.key,row[col.key]));tr.append(td)}body.append(tr)}table.append(body);host.append(table)}
function renderReportIndex(rows){const host=document.querySelector('[data-view=report-index]');host.replaceChildren();if(!rows.length){renderEmpty('[data-view=report-index]','No reports available','report');return}const grid=document.createElement('div');grid.className='report-index-grid';for(const row of rows){const card=document.createElement('article');card.className='report-card';const title=document.createElement('h3');text(title,row.title||row.id);const meta=document.createElement('p');meta.className='muted';text(meta,`${row.category||'runtime'} / ${row.status||'unknown'} / ${row.freshness||'runtime'}`);const endpoint=document.createElement('code');text(endpoint,row.endpoint||'none');const action=document.createElement('button');action.type='button';action.className='secondary-action';action.dataset.reportAction=row.id||row.title||'report';text(action,`Open ${row.title||row.id}`);action.addEventListener('click',()=>{const target=document.querySelector('[data-view=route-report]');target?.scrollIntoView({block:'start',behavior:'smooth'});});card.append(title,meta,endpoint,action);grid.append(card)}host.append(grid)}
function renderActionTable(selector,rows,columns,actionLabel,onAction,emptyMessage='No records found',resource='record'){const host=document.querySelector(selector);host.replaceChildren();if(!rows.length){renderEmpty(selector,emptyMessage,resource);return}const table=document.createElement('table');const thead=document.createElement('thead');const headRow=document.createElement('tr');for(const col of columns){const th=document.createElement('th');text(th,col.label);headRow.append(th)}const actionHead=document.createElement('th');text(actionHead,'Action');headRow.append(actionHead);thead.append(headRow);table.append(thead);const body=document.createElement('tbody');for(const row of rows){const tr=document.createElement('tr');for(const col of columns){const td=document.createElement('td');text(td,row[col.key]);tr.append(td)}const action=document.createElement('td');const button=document.createElement('button');button.type='button';text(button,actionLabel);button.addEventListener('click',()=>onAction(row));action.append(button);tr.append(action);body.append(tr)}table.append(body);host.append(table)}
function appendEntityLabel(button,icon,label,meta){button.classList.add('entity-button');const mark=document.createElement('span');mark.className='entity-icon';mark.setAttribute('aria-hidden','true');text(mark,icon);const name=document.createElement('span');name.className='entity-name';text(name,label);button.append(mark,name);if(meta){const detail=document.createElement('span');detail.className='entity-meta';text(detail,meta);button.append(detail)}}
function renderBucketExplorer(rows){const host=document.querySelector('[data-view=route-buckets]');host.replaceChildren();if(!rows.length){renderExplorerEmpty('[data-view=route-buckets]','No buckets exist in this runtime','bucket');return}const explorer=document.createElement('div');explorer.className='explorer bucket-list';const list=document.createElement('ul');for(const row of rows){const selected=row.name===state.selectedBucket;const item=document.createElement('li');const button=document.createElement('button');button.type='button';button.className=selected?'is-selected':'';button.dataset.bucket=row.name;button.dataset.entityRow='bucket';button.setAttribute('aria-pressed',selected?'true':'false');button.setAttribute('aria-label',`Open bucket ${row.name}`);button.setAttribute('title',`Open bucket ${row.name}`);appendEntityLabel(button,'▣',row.name,`${formatNumber(row.object_count)} objects`);button.addEventListener('click',()=>{if(row.name===state.selectedBucket){setInspectorCollapsed(!state.inspectorCollapsed)}else{setInspectorCollapsed(false)}selectBucket(row.name)});item.append(button);list.append(item)}explorer.append(list);host.append(explorer)}
function prefixVisible(prefix){for(const collapsed of state.collapsedPrefixes){if(prefix!==collapsed&&prefix.startsWith(collapsed))return false}return true}
function renderFileTree(rows,bucket){const host=document.querySelector('[data-view=file-tree]');if(!host)return;host.replaceChildren();const explorer=document.createElement('div');explorer.className='explorer file-tree';explorer.setAttribute('role','tree');const list=document.createElement('ul');const prefixes=new Set();for(const row of rows){const parts=String(row.key).split('/');for(let i=1;i<parts.length;i++)prefixes.add(parts.slice(0,i).join('/')+'/')}if(!prefixes.size){const item=document.createElement('li');item.setAttribute('role','treeitem');item.setAttribute('aria-level','1');text(item,'/');list.append(item)}for(const prefix of [...prefixes].sort().filter(prefixVisible)){const item=document.createElement('li');const depth=String(prefix).split('/').filter(Boolean).length;const collapsed=state.collapsedPrefixes.has(prefix);item.setAttribute('role','treeitem');item.setAttribute('aria-level',String(depth));item.setAttribute('aria-expanded',collapsed?'false':'true');const button=document.createElement('button');button.type='button';button.dataset.prefix=prefix;button.dataset.treePath=prefix;button.setAttribute('aria-expanded',collapsed?'false':'true');button.setAttribute('aria-label',`${collapsed?'Expand':'Collapse'} prefix ${prefix}`);appendEntityLabel(button,collapsed?'▸':'▾',prefix,'prefix');button.addEventListener('click',()=>{if(state.collapsedPrefixes.has(prefix)){state.collapsedPrefixes.delete(prefix)}else{state.collapsedPrefixes.add(prefix)}state.filters.prefix=prefix;document.querySelector('[name=prefix]').value=prefix;selectBucket(bucket)});item.append(button);list.append(item)}explorer.append(list);host.append(explorer)}
function renderBreadcrumbs(bucket){const host=document.querySelector('[data-view=file-breadcrumbs]');if(!host)return;host.replaceChildren();const parts=String(state.filters.prefix||'').split('/').filter(Boolean);const root=document.createElement('button');root.type='button';root.className='breadcrumb-item';root.setAttribute('aria-label',`Open bucket root ${bucket||''}`.trim());if(!parts.length)root.setAttribute('aria-current','page');text(root,bucket||'bucket');root.addEventListener('click',()=>{state.filters.prefix='';document.querySelector('[name=prefix]').value='';if(bucket)selectBucket(bucket)});host.append(root);let path='';parts.forEach((part,index)=>{path+=`${part}/`;const separator=document.createElement('span');separator.className='breadcrumb-separator';separator.setAttribute('aria-hidden','true');text(separator,'/');const crumb=document.createElement('button');crumb.type='button';crumb.className='breadcrumb-item';crumb.dataset.prefix=path;if(index===parts.length-1)crumb.setAttribute('aria-current','page');text(crumb,part);crumb.addEventListener('click',()=>{state.filters.prefix=path;document.querySelector('[name=prefix]').value=path;if(bucket)selectBucket(bucket)});host.append(separator,crumb)})}
function renderObjectExplorer(rows,bucket){renderBreadcrumbs(bucket);renderFileTree(rows,bucket);const query=String(state.filters.query||'').toLowerCase();const filtered=query?rows.filter((row)=>String(row.key).toLowerCase().includes(query)):rows;if(!filtered.length){renderExplorerEmpty('[data-view=object-list]',query?`No objects matching ${state.filters.query}`:`No objects in ${bucket}`,'object');return}const host=document.querySelector('[data-view=object-list]');host.replaceChildren();const explorer=document.createElement('div');explorer.className='explorer object-table';const list=document.createElement('ul');for(const row of filtered){const selected=row.key===state.selectedObject;const item=document.createElement('li');item.className='object-node';const button=document.createElement('button');button.type='button';button.className=selected?'is-selected':'';button.dataset.bucket=bucket;button.dataset.objectKey=row.key;button.dataset.entityRow='object';button.setAttribute('aria-pressed',selected?'true':'false');button.setAttribute('aria-label',`Open object ${row.key}`);button.setAttribute('title',`Open object ${row.key}`);appendEntityLabel(button,'□',row.key,`${formatNumber(row.version_count)} versions, ${formatBytes(row.total_bytes||0)}`);button.addEventListener('click',()=>{if(row.key===state.selectedObject){setInspectorCollapsed(!state.inspectorCollapsed)}else{setInspectorCollapsed(false)}selectObject(bucket,row.key)});item.append(button);list.append(item)}explorer.append(list);host.append(explorer)}
function renderVersionActions(rows){const host=document.querySelector('[data-view=version-actions]');if(!host)return;host.replaceChildren();if(!rows.length)return;const latest=rows.find((row)=>row.is_latest)||rows[0];for(const label of ['Preview latest','Download latest','Restore as copy','Delete object']){const button=document.createElement('button');button.type='button';button.dataset.action=`object-${label.toLowerCase().split(' ')[0]}`;button.disabled=label!=='Preview latest';button.title=`${label} uses selected version ${latest.version_id}`;text(button,label);host.append(button)}}
function renderObjectDetail(bucket,key,versions){const latest=versions.find((row)=>row.is_latest)||versions[0];if(!latest){renderEmpty('[data-view=object-detail]','No selected object');renderEmpty('[data-view=object-preview]','No preview available');return}renderPairs('[data-view=object-detail]',[['Bucket',bucket],['Object key',key],['Latest version',latest.version_id],['Version label',latest.version_label],['Bytes',latest.content_length],['ETag',latest.etag],['Owner',latest.owner],['Legal hold',latest.legal_hold],['Retention',latest.retention_mode||'none'],['Replication',latest.replication_status||'none']]);const preview=document.querySelector('[data-view=object-preview]');preview.replaceChildren();const card=document.createElement('div');card.className='preview-panel';text(card,`Preview: ${key} (${formatBytes(latest.content_length)}) is available through the authenticated S3 GetObject/download surface. Binary-safe inline previews stay metadata-only until a content preview API is enabled.`);preview.append(card)}
function renderBucketSelectionEmpty(){renderEmpty('[data-view=bucket-detail]','Select a bucket to open its full detail page','bucket');renderExplorerEmpty('[data-view=file-tree]','Select a bucket to show its folder tree','bucket');renderEmpty('[data-view=file-breadcrumbs]','No bucket path selected','bucket');renderExplorerEmpty('[data-view=object-list]','Select a bucket to browse objects','object');renderEmpty('[data-view=version-history]','No object selected','version');renderEmpty('[data-view=version-actions]','No object actions available','object-action');renderEmpty('[data-view=object-detail]','No object selected','object');renderEmpty('[data-view=object-preview]','No object preview available','object');renderEmpty('[data-view=bucket-governance]','Select a bucket to show governance and policy records','bucket-governance');renderEmpty('[data-view=bucket-governance-gates]','No bucket actions available','bucket-action')}
async function loadBucketExplorer(){const buckets=await api(`${apiPath('/buckets')}?sort=name`);renderBucketExplorer(buckets.buckets);if(!buckets.buckets.length){state.selectedBucket=null;state.selectedObject=null;renderBucketSelectionEmpty();syncShell();return}if(!state.selectedBucket){state.selectedObject=null;renderBucketSelectionEmpty();syncShell();return}await selectBucket(state.selectedBucket,false)}
function renderGovernanceGates(selector,scope,allowed){const host=document.querySelector(selector);if(!host)return;host.replaceChildren();for(const gate of governanceGateCatalog[scope]||[]){const runtimeSupported=gate.available!==false;const enabled=allowed&&runtimeSupported;const reason=enabled?`Requires ${gate.requires}; ${gate.confirmation}`:gate.unavailable||`Blocked: requires ${gate.requires}; ${gate.confirmation}`;const button=document.createElement('button');button.type='button';button.className=enabled?'secondary-action':'secondary-action is-gated';button.disabled=!enabled;button.setAttribute('aria-disabled',enabled?'false':'true');button.dataset.governanceGate=scope;button.dataset.governanceAction=gate.action;button.dataset.requires=gate.requires;button.dataset.confirmation=gate.confirmation;button.dataset.runtimeSupport=runtimeSupported?'supported':'unsupported';button.dataset.policyScope=scope==='global'?'tenant-global':'scoped-resource';button.title=enabled?`${gate.label}; requires ${gate.requires}; ${gate.confirmation}`:reason;text(button,gate.label);const note=document.createElement('span');note.className='gate-note';note.dataset.disabledReason=enabled?'none':reason;text(note,reason);const wrap=document.createElement('span');wrap.className='gate-control';wrap.dataset.gateState=enabled?'enabled':'read-only';wrap.append(button,note);host.append(wrap)}}
function governanceFindingRecord(finding){const raw=String(finding||'');const parts=raw.split(':').map((part)=>part.trim()).filter(Boolean);return {scope:parts[0]||'global',check:parts[1]||'governance',status:raw.toLowerCase().includes('missing')?'Action needed':'Review',message:raw}}
function renderEvidenceDetail(item){const host=document.querySelector('[data-view=evidence-detail]');if(!host)return;host.replaceChildren();if(!item){renderEmpty('[data-view=evidence-detail]','No evidence selected','evidence');return}const title=document.createElement('h3');text(title,item.name);const dl=document.createElement('dl');for(const pair of [['Type',item.content_type],['Records',item.record_count],['Bytes',item.bytes]]){const dt=document.createElement('dt');const dd=document.createElement('dd');text(dt,pair[0]);text(dd,formatValue(pair[0],pair[1]));dl.append(dt,dd)}const preview=document.createElement('pre');preview.className='preview-panel';text(preview,`Evidence ${item.name} contains ${formatNumber(item.record_count)} records and ${formatBytes(item.bytes)}.`);host.append(title,dl,preview)}
function selectEvidenceRow(row,tr){for(const current of document.querySelectorAll('[data-evidence-row]'))current.setAttribute('aria-selected','false');if(tr)tr.setAttribute('aria-selected','true');renderEvidenceDetail(row)}
function renderEvidenceList(rows){const host=document.querySelector('[data-view=route-evidence]');host.replaceChildren();if(!rows.length){renderEmpty('[data-view=route-evidence]','No evidence records available','evidence');renderEvidenceDetail(null);return}const table=document.createElement('table');const thead=document.createElement('thead');const head=document.createElement('tr');for(const label of ['Evidence','Type','Records','Bytes']){const th=document.createElement('th');text(th,label);head.append(th)}thead.append(head);table.append(thead);const body=document.createElement('tbody');rows.forEach((row,index)=>{const tr=document.createElement('tr');tr.tabIndex=0;tr.dataset.evidenceRow=row.name;tr.setAttribute('aria-selected','false');tr.addEventListener('click',()=>selectEvidenceRow(row,tr));tr.addEventListener('keydown',(event)=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();selectEvidenceRow(row,tr)}});for(const [key,label]of [['name','Evidence'],['content_type','Type'],['record_count','Records'],['bytes','Bytes']]){const td=document.createElement('td');text(td,formatValue(label,row[key]));tr.append(td)}body.append(tr);if(index===0)queueMicrotask(()=>selectEvidenceRow(row,tr))});table.append(body);host.append(table)}
async function selectBucket(bucket,push=true){state.selectedBucket=bucket;state.selectedObject=null;const detail=await api(`${apiPath('/buckets')}/${enc(bucket)}`);renderPairs('[data-view=bucket-detail]',[['Name',detail.bucket.name],['Owner',detail.bucket.owner],['Region',detail.bucket.region],['Versioning',detail.bucket.versioning_status],['Object lock',detail.bucket.has_object_lock],['Policy statements',detail.policies.length],['Findings',detail.findings.length],['Total bytes',detail.bucket.total_bytes]]);renderPairs('[data-view=bucket-governance]',[['Versioning',detail.bucket.versioning_status],['Object lock',detail.bucket.has_object_lock],['Lifecycle',detail.bucket.has_lifecycle],['Policy statements',detail.policies.length],['Findings',detail.findings.length],['Encrypted',detail.bucket.has_encryption],['Replication',detail.bucket.has_replication]]);renderGovernanceGates('[data-view=bucket-governance-gates]','bucket',Boolean(state.session));const objects=await api(`${apiPath('/buckets')}/${enc(bucket)}/objects?sort=key&prefix=${enc(state.filters.prefix)}`);renderObjectExplorer(objects.objects,bucket);const query=String(state.filters.query||'').toLowerCase();const selectable=query?objects.objects.filter((row)=>String(row.key).toLowerCase().includes(query)):objects.objects;if(selectable.length){const params=new URLSearchParams(location.search);await selectObject(bucket,params.get('key')||state.selectedObject||selectable[0].key,false)}else{state.selectedObject=null;renderEmpty('[data-view=version-history]',`No versions in ${bucket}`);renderEmpty('[data-view=object-detail]','No object selected');renderEmpty('[data-view=object-preview]','No preview available');renderVersionActions([])}renderInspector('bucket selected');if(push)history.pushState(null,'',bucketDetailPath(bucket));syncShell()}
async function selectObject(bucket,key,push=true){state.selectedBucket=bucket;state.selectedObject=key;const versions=await api(`${apiPath('/object-versions')}?bucket=${enc(bucket)}&key=${enc(key)}`);renderTable('[data-view=version-history]',versions.versions,[{key:'version_label',label:'Version'},{key:'version_id',label:'Version ID'},{key:'is_latest',label:'Latest'},{key:'delete_marker',label:'Delete marker'},{key:'last_modified_epoch_seconds',label:'Modified'},{key:'content_length',label:'Bytes'},{key:'retention_mode',label:'Retention'},{key:'legal_hold',label:'Legal hold'}]);renderVersionActions(versions.versions);renderObjectDetail(bucket,key,versions.versions);renderInspector('object selected');if(push)history.pushState(null,'',objectDetailPath(bucket,key));syncShell()}
async function ensureSelectedObject(){if(state.selectedBucket&&state.selectedObject)return;const buckets=await api(`${apiPath('/buckets')}?sort=name&limit=1`);if(!buckets.buckets.length)return;state.selectedBucket=buckets.buckets[0].name;const objects=await api(`${apiPath('/buckets')}/${enc(state.selectedBucket)}/objects?sort=key&limit=1`);if(objects.objects.length)state.selectedObject=objects.objects[0].key}
async function loadGovernanceView(){resetSelectionsForRoute('governance');const global=await api(apiPath('/reports/governance'));renderPairs('[data-view=global-governance-dashboard]',[['Scope','Global tenant governance'],['Retention buckets',global.retention_bucket_count],['Retained versions',global.retained_version_count],['Lifecycle buckets',global.lifecycle_bucket_count],['Lifecycle rules',global.lifecycle_rule_count],['Active legal holds',global.legal_hold_object_count||0],['Findings',global.security_governance_findings.length]]);renderGovernanceGates('[data-view=global-governance-gates]','global',Boolean(state.session));renderPairs('[data-view=global-retention-summary]',[['Scope',global.retention_scope||'tenant'],['Total buckets',global.total_bucket_count||0],['Retention buckets',global.retention_bucket_count],['Retention coverage',`${global.retention_coverage_percent||0}%`],['Retention exceptions',global.retention_exception_bucket_count||0],['Retained versions',global.retained_version_count],['Lifecycle buckets',global.lifecycle_bucket_count],['Lifecycle rules',global.lifecycle_rule_count],['Remediation surface',global.retention_remediation_surface||routePath('/buckets')]]);renderPairs('[data-view=global-legal-hold-summary]',[['Scope',global.legal_hold_scope||'tenant'],['Total objects',global.total_object_count||0],['Active legal holds',global.legal_hold_object_count||0],['Legal-hold coverage',`${global.legal_hold_coverage_percent||0}%`],['Legal-hold exceptions',global.legal_hold_exception_object_count||0],['Mutation surface',global.legal_hold_mutation_surface||'object-detail'],['Remediation surface',global.legal_hold_remediation_surface||routePath('/buckets')]]);renderTable('[data-view=global-governance-findings]',(global.security_governance_findings||[]).map(governanceFindingRecord),[{key:'scope',label:'Scope'},{key:'check',label:'Check'},{key:'status',label:'Status'},{key:'message',label:'Message'}],'No governance findings','finding');syncShell()}
async function submitLegalHold(event){event.preventDefault();if(!state.selectedBucket||!state.selectedObject){setStatus('empty','No selected object');return}const submitter=event.submitter;const data=new FormData(event.currentTarget);const enabled=String(submitter?.value||'true')==='true';const reason=String(data.get('reason')||'').trim();const status=document.querySelector('[data-view=legal-hold-action]');if(!reason){text(status,'Reason is required');return}const qs=`bucket=${enc(state.selectedBucket)}&key=${enc(state.selectedObject)}`;const updated=await api(`${apiPath('/legal-hold')}?${qs}`,{method:'POST',body:JSON.stringify({enabled,reason})});text(status,`${enabled?'Applied':'Lifted'} legal hold for ${updated.version_id}`);await loadGovernanceView()}
async function loadAuditView(){const params=new URLSearchParams();params.set('limit','25');if(state.filters.query)params.set('q',state.filters.query);if(state.filters.status&&state.filters.status!=='all')params.set('outcome',state.filters.status);params.set('sort',state.filters.sort||'-sequence');const audit=await api(`${apiPath('/audit')}?${params.toString()}`);renderTable('[data-view=route-audit]',audit.events,[{key:'sequence',label:'Seq'},{key:'subject',label:'Subject'},{key:'action',label:'Action'},{key:'resource',label:'Resource'},{key:'outcome',label:'Outcome'},{key:'detail',label:'Detail'}])}
async function loadEvidenceView(){const evidence=await api(`${apiPath('/evidence')}?sort=name`);renderEvidenceList(evidence.evidence)}
async function downloadEvidenceExport(){const button=document.querySelector('[data-action=download-evidence]');const status=document.querySelector('[data-view=evidence-download]');if(button)button.disabled=true;try{const exportPayload=await api(apiPath('/evidence-export'));const blob=new Blob([JSON.stringify(exportPayload.report,null,2)],{type:exportPayload.content_type});const link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=exportPayload.filename;link.click();URL.revokeObjectURL(link.href);text(status,`Prepared ${exportPayload.filename}`)}finally{if(button)button.disabled=false}}
const preferenceKeys=['bucketwarden.ui.density','bucketwarden.ui.refreshSeconds','bucketwarden.ui.visibleColumns','bucketwarden.ui.reportFilters'];
function applyPreferenceControls(values){for(const key of preferenceKeys){const input=document.querySelector(`[name="${key}"]`);if(input)input.value=values[key]||''}state.preferences=Object.assign({},state.preferences,values)}
function preferenceFormValues(){const values={};for(const key of preferenceKeys){const input=document.querySelector(`[name="${key}"]`);const value=String(input?.value||'').trim();if(value)values[key]=value}return values}
async function loadPreferencesView(){const prefs=await api(apiPath('/preferences'));applyPreferenceControls(prefs.values||{});renderPairs('[data-view=route-preferences]',preferenceKeys.map((key)=>[key,(prefs.values||{})[key]||'default']));await loadAdminView()}
async function loadAdminView(){const admin=await api(apiPath('/admin'));if(admin.tenant_scope.tenant_ids.length){renderPairs('[data-view=tenant-scope]',[['Selected tenant',admin.tenant_scope.selected_tenant_id],['Tenant count',admin.tenant_scope.tenant_ids.length],['Scoped request header',admin.tenant_scope.scoped_request_header]])}else{renderEmpty('[data-view=tenant-scope]','No tenants configured','tenant')}renderTable('[data-view=admin-users]',admin.users,[{key:'principal_id',label:'Principal'},{key:'tenant_id',label:'Tenant'},{key:'kind',label:'Kind'},{key:'enabled',label:'Enabled'},{key:'operator_role_count',label:'Roles'}],'No users configured','user');renderTable('[data-view=admin-roles]',admin.roles,[{key:'role',label:'Role'},{key:'assignment_count',label:'Assignments'}],'No roles configured','role');renderTable('[data-view=admin-role-assignments]',admin.assignments,[{key:'principal_id',label:'Principal'},{key:'role',label:'Role'},{key:'scope',label:'Scope'}],'No role assignments configured','role-assignment');renderTable('[data-view=admin-effective-permissions]',admin.effective_permissions.map((permission)=>({permission})),[{key:'permission',label:'Permission'}],'No permissions configured','permission');renderTable('[data-view=admin-groups]',(admin.groups||[]).map((group)=>({group})),[{key:'group',label:'Group'}],'No groups configured','group');if(admin.users.length){const detail=await api(`${apiPath('/admin/users')}/${enc(admin.users[0].principal_id)}`);renderPairs('[data-view=admin-user-detail]',[['Principal',detail.principal_id],['Tenant',detail.tenant_id],['Kind',detail.kind],['Enabled',detail.enabled],['Assignments',detail.assignments.length],['Effective permissions',detail.effective_permissions.length]])}else{renderEmpty('[data-view=admin-user-detail]','No user selected','user')}}
async function savePreferencesView(){const values=preferenceFormValues();const saved=await api(apiPath('/preferences'),{method:'PUT',body:JSON.stringify({values})});applyPreferenceControls(saved.values||{});renderPairs('[data-view=route-preferences]',preferenceKeys.map((key)=>[key,(saved.values||{})[key]||'default']));text(document.querySelector('[data-view=preferences-save]'),'Preferences saved')}
async function loadIdentityProviders(){if(!providerSelect)return;const response=await api(apiPath('/identity-providers'));providerSelect.replaceChildren();for(const provider of response.providers||[]){const option=document.createElement('option');option.value=provider.provider_id;option.disabled=!provider.enabled;text(option,provider.enabled?provider.label:`${provider.label} (disabled)`);providerSelect.append(option)}providerSelect.value=response.default_provider_id||'custom-shared-secret'}
function routeUsesGlobalFilter(route){return routeChrome(route,'filters')}
function syncShell(){document.body.dataset.routeChrome=state.route;document.body.dataset.inspectorVisible=inspectorAllowed()?'true':'false';const bucketPanel=document.querySelector('[data-route-panel=buckets]');if(bucketPanel)bucketPanel.dataset.hasSelection=state.selectedBucket?'true':'false';principalNode.textContent=state.session?`Signed in as ${state.session.principal_id}`:'Signed out';const meta=routeMeta[state.route]||['Overview','Runtime status'];text(routeTitleNode,meta[0]);text(routeSummaryNode,meta[1]);text(sessionContextNode,state.session?`Session: ${state.session.principal_id}`:'Session: signed out');text(tenantContextNode,'Tenant: default');text(bucketContextNode,`Bucket: ${state.selectedBucket||'none'}`);text(objectContextNode,`Object: ${state.selectedObject||'none'}`);bucketContextNode.hidden=!state.selectedBucket;objectContextNode.hidden=!state.selectedObject;for(const link of document.querySelectorAll('.shell-nav a')){const linkRoute=routeForPath(new URL(link.href).pathname);link.classList.toggle('is-active',linkRoute===state.route);link.hidden=linkRoute==='login'&&Boolean(state.session)}for(const panel of document.querySelectorAll('[data-route-panel]'))panel.hidden=panel.getAttribute('data-route-panel')!==state.route;const filter=document.querySelector('[data-form=global-filter]');if(filter){filter.hidden=!routeUsesGlobalFilter(state.route);filter.dataset.searchScope=state.route}if(loginForm&&loginAuthenticatedPanel){const signedIn=Boolean(state.session);loginForm.hidden=signedIn;loginAuthenticatedPanel.hidden=!signedIn;text(document.querySelector('[data-view=login-session-summary]'),signedIn?`Signed in as ${state.session.principal_id}.`:'Signed out.')}document.querySelector('.refresh-controls').hidden=!routeChrome(state.route,'refresh');syncFilterControls();renderInspector()}
async function logoutCurrent(push=true){const hadToken=Boolean(token());if(hadToken)await api(apiPath('/logout'),{method:'POST'}).catch(()=>{});clearSession('Unauthenticated');state.route='login';state.returnTo=null;if(push)history.pushState(null,'',routePath('/login'));syncShell()}
function resetSelectionsForRoute(route){if(!routeChrome(route,'selection')){state.selectedBucket=null;state.selectedObject=null;state.filters.prefix=''}}
async function navigate(path,push=true){const next=routeForPath(path);if(next==='logout'){state.route='logout';if(push)history.pushState(null,'',path);syncShell();return logoutCurrent(true)}if(next!=='login'&&!token()){state.route='login';state.returnTo=path;syncShell();return}state.route=next;hydrateRouteState(path);if(push)history.pushState(null,'',path);syncShell();await refresh()}
async function refresh(){if(!token()&&state.route!=='login')return navigate(routePath('/login'),false);if(state.route==='login')return;try{if(state.route==='overview'){const overview=await api(apiPath('/overview'));state.session=overview.session;state.principal=overview.session.principal_id;syncShell();renderPairs('[data-view=overview]',[['Buckets',overview.metrics.bucket_count],['Objects',overview.metrics.object_count],['Versions',overview.metrics.version_count],['Total bytes',overview.metrics.total_bytes],['Retention buckets',overview.retention_bucket_count],['Retained versions',overview.retained_version_count],['Lifecycle buckets',overview.lifecycle_bucket_count],['Status',overview.health.status]])}else if(state.route==='reports'){const reports=await api(apiPath('/reports'));const reportRows=(reports.reports||[]).map((row)=>Object.assign({freshness:'runtime'},row));renderReportIndex(reportRows);const health=await api(apiPath('/reports/health'));renderPairs('[data-view=route-report]',[['Status',health.status],['Ready',health.ready],['Generated',health.generated_at_epoch_seconds],['Issues',health.issues.length],['Active storage',health.active_storage_backend]]);const config=await api(apiPath('/reports/config'));renderPairs('[data-view=route-config]',[['Default region',config.default_bucket_region],['KMS key',config.key_id],['Storage backend',config.active_storage_backend],['Replication strategy',config.active_replication_strategy],['Metadata persistence',config.metadata_persistence_model],['Buckets with object lock',config.buckets_with_object_lock.length]]);const storage=await api(apiPath('/reports/storage'));renderPairs('[data-view=route-storage]',[['Active backend',storage.active_backend],['Supported',storage.supported_backends.length],['Unsupported',storage.unsupported_backends.length],['Caveats',storage.caveats.length]]);const governance=await api(apiPath('/reports/governance'));renderPairs('[data-view=route-report-governance]',[['Retention buckets',governance.retention_bucket_count],['Retained versions',governance.retained_version_count],['Lifecycle buckets',governance.lifecycle_bucket_count],['Findings',governance.security_governance_findings.length]]);const incident=await api(apiPath('/reports/incident'));renderPairs('[data-view=route-incident]',[['Type',incident.incident_type],['Status',incident.status],['Summary',incident.summary],['Evidence',incident.evidence.length]])}else if(state.route==='buckets'){await loadBucketExplorer()}else if(state.route==='governance'){await loadGovernanceView()}else if(state.route==='audit'){await loadAuditView()}else if(state.route==='evidence'){await loadEvidenceView()}else if(state.route==='settings'){await loadPreferencesView()}}catch(error){handleApiFailure(error)}}
async function bootstrapSession(){if(state.route==='logout')return logoutCurrent(true);if(!token()){if(state.route!=='login')state.returnTo=location.pathname;return navigate(routePath('/login'),false)}try{const session=await api(apiPath('/current-user'));state.session=session;state.principal=session.principal_id;statusNode.textContent=`Authenticated as ${state.principal}`;statusNode.className='is-ok';syncShell();await refresh()}catch(error){handleApiFailure(error)}}
function syncFilterControls(){const form=document.querySelector('[data-form=global-filter]');if(!form)return;form.elements.q.value=state.filters.query||'';form.elements.prefix.value=state.filters.prefix||'';form.elements.status.value=state.filters.status||'all';form.elements.sort.value=state.filters.sort||'-sequence';const scope=document.querySelector('[data-filter-scope]');if(scope)text(scope,`Scope: ${routeMeta[state.route]?.[0]||state.route}`)}
function setBucketTab(tab){state.activeBucketTab=tab;for(const panel of document.querySelectorAll('[data-bucket-panel]'))panel.hidden=panel.getAttribute('data-bucket-panel')!==tab;for(const button of document.querySelectorAll('[data-bucket-tab]')){const selected=button.dataset.bucketTab===tab;button.setAttribute('aria-selected',selected?'true':'false');button.tabIndex=selected?0:-1}}
function hydrateRouteState(path=location.pathname){const params=new URLSearchParams(location.search);const bucketPrefix=`${routePath('/buckets')}/`;if(path.startsWith(bucketPrefix)){const parts=path.slice(bucketPrefix.length).split('/');state.selectedBucket=decodeURIComponent(parts[0]||'')||null;state.selectedObject=parts[1]==='objects'?decodeURIComponent(parts.slice(2).join('/')||''):null}else{if(params.get('bucket'))state.selectedBucket=params.get('bucket');if(params.get('key'))state.selectedObject=params.get('key')}if(params.get('prefix'))state.filters.prefix=params.get('prefix');if(params.get('q'))state.filters.query=params.get('q');resetSelectionsForRoute(state.route);syncFilterControls();setBucketTab(state.activeBucketTab)}
function scheduleRefresh(){if(refreshTimer)clearInterval(refreshTimer);const seconds=Number(refreshIntervalNode?.value||0);if(seconds>0)refreshTimer=setInterval(()=>refresh().catch(handleApiFailure),seconds*1000)}
hydrateRouteState();
loadIdentityProviders().catch(handleApiFailure);
loginForm?.addEventListener('submit',(event)=>{
  event.preventDefault();
  const form=event.currentTarget;
  const data=new FormData(event.currentTarget);
  if(loginButton)loginButton.disabled=true;
  api(apiPath('/login'),{method:'POST',body:JSON.stringify({principal_id:String(data.get('principal')||''),shared_secret:String(data.get('secret')||''),identity_provider:String(data.get('identity_provider')||'custom-shared-secret')})}).then((session)=>{state.session=session;state.principal=session.principal_id;sessionStorage.setItem('bwui_access_key',session.access_key_id);statusNode.textContent=`Authenticated as ${state.principal}`;statusNode.className='is-ok';const secret=form.querySelector('[name=secret]');if(secret)secret.value='';navigate(state.returnTo&&state.returnTo!==routePath('/login')?state.returnTo:routePath(),true)}).catch((error)=>{clearSession('Unauthenticated');statusNode.textContent=error.message||'Authentication failed';statusNode.className='is-error'}).finally(()=>{if(loginButton)loginButton.disabled=false});
});
document.querySelector('[data-action=logout]')?.addEventListener('click',()=>logoutCurrent(true));
document.querySelector('[data-action=login-continue]')?.addEventListener('click',()=>navigate(routePath(),true));
document.querySelector('[data-action=login-signout]')?.addEventListener('click',()=>logoutCurrent(true));
document.querySelector('[data-action=toggle-inspector]')?.addEventListener('click',()=>setInspectorCollapsed(!state.inspectorCollapsed));
document.querySelector('[data-action=clear-filters]')?.addEventListener('click',()=>{state.filters={query:'',prefix:'',status:'all',sort:'-sequence'};syncFilterControls();refresh().catch(handleApiFailure)});
document.querySelectorAll('[data-bucket-tab]').forEach((button)=>button.addEventListener('click',()=>setBucketTab(button.dataset.bucketTab)));
document.querySelector('[data-form=global-filter]')?.addEventListener('submit',(event)=>{event.preventDefault();const data=new FormData(event.currentTarget);state.filters.query=String(data.get('q')||'');state.filters.prefix=String(data.get('prefix')||'');state.filters.status=String(data.get('status')||'all')||'all';state.filters.sort=String(data.get('sort')||'-sequence');syncFilterControls();refresh().catch(handleApiFailure)});
document.querySelector('[data-form=legal-hold]')?.addEventListener('submit',(event)=>submitLegalHold(event).catch(handleApiFailure));
document.querySelector('[data-action=refresh]')?.addEventListener('click',()=>refresh().catch(handleApiFailure));
refreshIntervalNode?.addEventListener('change',scheduleRefresh);
document.querySelector('[data-action=download-evidence]')?.addEventListener('click',()=>downloadEvidenceExport().catch(handleApiFailure));
document.querySelector('[data-form=preferences]')?.addEventListener('submit',(event)=>{event.preventDefault();savePreferencesView().catch(handleApiFailure)});
document.querySelectorAll('.shell-nav a').forEach((link)=>link.addEventListener('click',(event)=>{event.preventDefault();navigate(new URL(link.href).pathname,true)}));
addEventListener('popstate',()=>navigate(location.pathname,false));
syncShell();bootstrapSession();
scheduleRefresh();
window.BucketWardenUI={state,api,navigate,refresh,logout(){return logoutCurrent(true)},features:Array.from(document.querySelectorAll('[data-ui-feature]')).map((node)=>node.getAttribute('data-ui-feature'))};
"#
}

pub(super) fn browser_ui_feature_statuses() -> Vec<BrowserUiFeatureStatus> {
    browser_ui_feature_definitions()
        .into_iter()
        .map(
            |(feature_id, title, category, runtime_surface)| BrowserUiFeatureStatus {
                feature_id: feature_id.to_string(),
                title: title.to_string(),
                category: category.to_string(),
                runtime_surface: runtime_surface.to_string(),
                implementation_status: "implemented".to_string(),
                claim_tier: BROWSER_UI_CLAIM_TIER.to_string(),
            },
        )
        .collect()
}

pub(super) fn browser_ui_assets() -> Vec<BrowserUiAsset> {
    let manifest_bytes = browser_ui_manifest_json().len();
    browser_ui_assets_with_manifest_bytes(manifest_bytes)
}

fn browser_ui_assets_with_manifest_bytes(manifest_bytes: usize) -> Vec<BrowserUiAsset> {
    [
        ("/ui", "text/html; charset=utf-8", browser_ui_html().len()),
        (
            "/ui/assets/app.css",
            "text/css; charset=utf-8",
            browser_ui_css().len(),
        ),
        (
            "/ui/assets/app.js",
            "application/javascript; charset=utf-8",
            browser_ui_js().len(),
        ),
        (
            "/ui/assets/bucketwarden-monogram.png",
            "image/png",
            include_bytes!("../../assets/bucketwarden-monogram.png")
                .len(),
        ),
        (
            "/ui/assets/favicon.png",
            "image/png",
            include_bytes!("../../assets/bucketwarden-monogram.png")
                .len(),
        ),
        (
            "/ui/manifest.json",
            "application/json; charset=utf-8",
            manifest_bytes,
        ),
    ]
    .into_iter()
    .map(|(path, content_type, bytes)| BrowserUiAsset {
        path: path.to_string(),
        content_type: content_type.to_string(),
        bytes,
    })
    .collect()
}