refactor(webui): replace blocked page with unified error page component
- commit
- ac16f2b23d02413bfa63da1e1e32af74c4bdbe41
- author
- bnema · · 2025-12-21 13:53 UTC
- parent
- 899908d
- signature
- unsigned
4 files · +196 -346
D webui/src/pages/BlockedPage.svelte +0 -334
| @@ -1,334 +0,0 @@ | |||
| 1 | - | <script lang="ts"> | |
| 2 | - | import { onMount } from 'svelte'; | |
| 3 | - | ||
| 4 | - | let blockedUrl = $state(''); | |
| 5 | - | let blockedDomain = $state(''); | |
| 6 | - | let reason = $state('Content blocked by filter'); | |
| 7 | - | let isLoading = $state(false); | |
| 8 | - | let error = $state(''); | |
| 9 | - | ||
| 10 | - | onMount(() => { | |
| 11 | - | // Parse URL parameters | |
| 12 | - | const params = new URLSearchParams(window.location.search); | |
| 13 | - | blockedUrl = params.get('url') || ''; | |
| 14 | - | reason = params.get('reason') || 'Content blocked by filter'; | |
| 15 | - | ||
| 16 | - | // Extract domain from URL | |
| 17 | - | try { | |
| 18 | - | if (blockedUrl) { | |
| 19 | - | const url = new URL(blockedUrl); | |
| 20 | - | blockedDomain = url.hostname; | |
| 21 | - | } | |
| 22 | - | } catch { | |
| 23 | - | blockedDomain = blockedUrl; | |
| 24 | - | } | |
| 25 | - | }); | |
| 26 | - | ||
| 27 | - | function sendMessage(type: string, payload: Record<string, unknown>): Promise<unknown> { | |
| 28 | - | return new Promise((resolve, reject) => { | |
| 29 | - | const requestId = `blocked_${Date.now()}_${Math.random().toString(36).slice(2)}`; | |
| 30 | - | ||
| 31 | - | const handler = (event: MessageEvent) => { | |
| 32 | - | if (event.data?.requestId === requestId) { | |
| 33 | - | window.removeEventListener('message', handler); | |
| 34 | - | if (event.data.success) { | |
| 35 | - | resolve(event.data.data); | |
| 36 | - | } else { | |
| 37 | - | reject(new Error(event.data.error || 'Unknown error')); | |
| 38 | - | } | |
| 39 | - | } | |
| 40 | - | }; | |
| 41 | - | ||
| 42 | - | window.addEventListener('message', handler); | |
| 43 | - | ||
| 44 | - | // Send to WebKit message handler | |
| 45 | - | if (window.webkit?.messageHandlers?.dumber) { | |
| 46 | - | window.webkit.messageHandlers.dumber.postMessage( | |
| 47 | - | JSON.stringify({ type, requestId, payload }) | |
| 48 | - | ); | |
| 49 | - | } else { | |
| 50 | - | reject(new Error('WebKit message handler not available')); | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | // Timeout after 10 seconds | |
| 54 | - | setTimeout(() => { | |
| 55 | - | window.removeEventListener('message', handler); | |
| 56 | - | reject(new Error('Request timeout')); | |
| 57 | - | }, 10000); | |
| 58 | - | }); | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | async function addToWhitelist() { | |
| 62 | - | if (!blockedDomain) return; | |
| 63 | - | ||
| 64 | - | isLoading = true; | |
| 65 | - | error = ''; | |
| 66 | - | ||
| 67 | - | try { | |
| 68 | - | await sendMessage('addToWhitelist', { domain: blockedDomain }); | |
| 69 | - | // Reload the original URL after whitelisting | |
| 70 | - | window.location.href = blockedUrl; | |
| 71 | - | } catch (e) { | |
| 72 | - | error = e instanceof Error ? e.message : 'Failed to add to whitelist'; | |
| 73 | - | isLoading = false; | |
| 74 | - | } | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | async function bypassOnce() { | |
| 78 | - | if (!blockedUrl) return; | |
| 79 | - | ||
| 80 | - | isLoading = true; | |
| 81 | - | error = ''; | |
| 82 | - | ||
| 83 | - | try { | |
| 84 | - | await sendMessage('bypassOnce', { url: blockedUrl }); | |
| 85 | - | // Reload the original URL after bypass | |
| 86 | - | window.location.href = blockedUrl; | |
| 87 | - | } catch (e) { | |
| 88 | - | error = e instanceof Error ? e.message : 'Failed to bypass'; | |
| 89 | - | isLoading = false; | |
| 90 | - | } | |
| 91 | - | } | |
| 92 | - | ||
| 93 | - | function goBack() { | |
| 94 | - | window.history.back(); | |
| 95 | - | } | |
| 96 | - | </script> | |
| 97 | - | ||
| 98 | - | <svelte:head> | |
| 99 | - | <title>Page Blocked</title> | |
| 100 | - | {@html `<style> | |
| 101 | - | html, body { | |
| 102 | - | background: var(--background, #0a0a0a); | |
| 103 | - | margin: 0; | |
| 104 | - | padding: 0; | |
| 105 | - | min-height: 100vh; | |
| 106 | - | } | |
| 107 | - | </style>`} | |
| 108 | - | </svelte:head> | |
| 109 | - | ||
| 110 | - | <div class="blocked-container"> | |
| 111 | - | <div class="blocked-card"> | |
| 112 | - | <div class="icon-container"> | |
| 113 | - | <svg class="shield-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |
| 114 | - | <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/> | |
| 115 | - | <path d="M9 12l2 2 4-4"/> | |
| 116 | - | </svg> | |
| 117 | - | </div> | |
| 118 | - | ||
| 119 | - | <h1 class="title">Page Blocked</h1> | |
| 120 | - | ||
| 121 | - | <p class="reason">{reason}</p> | |
| 122 | - | ||
| 123 | - | {#if blockedUrl} | |
| 124 | - | <div class="url-display"> | |
| 125 | - | <span class="url-label">Blocked URL:</span> | |
| 126 | - | <code class="url-value">{blockedUrl}</code> | |
| 127 | - | </div> | |
| 128 | - | {/if} | |
| 129 | - | ||
| 130 | - | {#if error} | |
| 131 | - | <div class="error-message">{error}</div> | |
| 132 | - | {/if} | |
| 133 | - | ||
| 134 | - | <div class="actions"> | |
| 135 | - | <button | |
| 136 | - | class="btn btn-primary" | |
| 137 | - | onclick={addToWhitelist} | |
| 138 | - | disabled={isLoading || !blockedDomain} | |
| 139 | - | > | |
| 140 | - | {#if isLoading} | |
| 141 | - | <span class="loading-spinner"></span> | |
| 142 | - | {:else} | |
| 143 | - | Add to Whitelist | |
| 144 | - | {/if} | |
| 145 | - | </button> | |
| 146 | - | ||
| 147 | - | <button | |
| 148 | - | class="btn btn-secondary" | |
| 149 | - | onclick={bypassOnce} | |
| 150 | - | disabled={isLoading || !blockedUrl} | |
| 151 | - | > | |
| 152 | - | Go Anyway (Once) | |
| 153 | - | </button> | |
| 154 | - | ||
| 155 | - | <button | |
| 156 | - | class="btn btn-ghost" | |
| 157 | - | onclick={goBack} | |
| 158 | - | disabled={isLoading} | |
| 159 | - | > | |
| 160 | - | Go Back | |
| 161 | - | </button> | |
| 162 | - | </div> | |
| 163 | - | ||
| 164 | - | {#if blockedDomain} | |
| 165 | - | <p class="domain-note"> | |
| 166 | - | Adding <strong>{blockedDomain}</strong> to whitelist will disable content filtering for this site. | |
| 167 | - | </p> | |
| 168 | - | {/if} | |
| 169 | - | </div> | |
| 170 | - | </div> | |
| 171 | - | ||
| 172 | - | <style> | |
| 173 | - | .blocked-container { | |
| 174 | - | min-height: 100vh; | |
| 175 | - | display: flex; | |
| 176 | - | align-items: center; | |
| 177 | - | justify-content: center; | |
| 178 | - | padding: 2rem; | |
| 179 | - | background: var(--background, #0a0a0a); | |
| 180 | - | color: var(--foreground, #e5e5e5); | |
| 181 | - | font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
| 182 | - | } | |
| 183 | - | ||
| 184 | - | .blocked-card { | |
| 185 | - | max-width: 480px; | |
| 186 | - | width: 100%; | |
| 187 | - | padding: 2.5rem 2rem; | |
| 188 | - | background: var(--card, #141414); | |
| 189 | - | border-width: 1px; | |
| 190 | - | border-style: solid; | |
| 191 | - | border-color: var(--border, #262626); | |
| 192 | - | text-align: center; | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | .icon-container { | |
| 196 | - | margin-bottom: 1.5rem; | |
| 197 | - | } | |
| 198 | - | ||
| 199 | - | .shield-icon { | |
| 200 | - | width: 64px; | |
| 201 | - | height: 64px; | |
| 202 | - | color: var(--primary, #3b82f6); | |
| 203 | - | opacity: 0.9; | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | .title { | |
| 207 | - | font-size: 1.5rem; | |
| 208 | - | font-weight: 600; | |
| 209 | - | margin: 0 0 0.75rem 0; | |
| 210 | - | color: var(--foreground, #e5e5e5); | |
| 211 | - | } | |
| 212 | - | ||
| 213 | - | .reason { | |
| 214 | - | font-size: 0.9375rem; | |
| 215 | - | color: var(--muted-foreground, #737373); | |
| 216 | - | margin: 0 0 1.5rem 0; | |
| 217 | - | } | |
| 218 | - | ||
| 219 | - | .url-display { | |
| 220 | - | background: var(--background, #0a0a0a); | |
| 221 | - | border-width: 1px; | |
| 222 | - | border-style: solid; | |
| 223 | - | border-color: var(--border, #262626); | |
| 224 | - | padding: 0.75rem 1rem; | |
| 225 | - | margin-bottom: 1.5rem; | |
| 226 | - | text-align: left; | |
| 227 | - | } | |
| 228 | - | ||
| 229 | - | .url-label { | |
| 230 | - | display: block; | |
| 231 | - | font-size: 0.75rem; | |
| 232 | - | color: var(--muted-foreground, #737373); | |
| 233 | - | margin-bottom: 0.25rem; | |
| 234 | - | text-transform: uppercase; | |
| 235 | - | letter-spacing: 0.05em; | |
| 236 | - | } | |
| 237 | - | ||
| 238 | - | .url-value { | |
| 239 | - | font-family: 'JetBrains Mono NF', monospace; | |
| 240 | - | font-size: 0.8125rem; | |
| 241 | - | color: var(--foreground, #e5e5e5); | |
| 242 | - | word-break: break-all; | |
| 243 | - | display: block; | |
| 244 | - | } | |
| 245 | - | ||
| 246 | - | .error-message { | |
| 247 | - | background: rgba(239, 68, 68, 0.1); | |
| 248 | - | border: 1px solid rgba(239, 68, 68, 0.3); | |
| 249 | - | color: #ef4444; | |
| 250 | - | padding: 0.75rem 1rem; | |
| 251 | - | margin-bottom: 1.5rem; | |
| 252 | - | font-size: 0.875rem; | |
| 253 | - | } | |
| 254 | - | ||
| 255 | - | .actions { | |
| 256 | - | display: flex; | |
| 257 | - | flex-direction: column; | |
| 258 | - | gap: 0.75rem; | |
| 259 | - | margin-bottom: 1.5rem; | |
| 260 | - | } | |
| 261 | - | ||
| 262 | - | .btn { | |
| 263 | - | padding: 0.75rem 1.5rem; | |
| 264 | - | font-size: 0.875rem; | |
| 265 | - | font-weight: 500; | |
| 266 | - | border: 1px solid transparent; | |
| 267 | - | cursor: pointer; | |
| 268 | - | transition: all 150ms ease; | |
| 269 | - | display: inline-flex; | |
| 270 | - | align-items: center; | |
| 271 | - | justify-content: center; | |
| 272 | - | gap: 0.5rem; | |
| 273 | - | } | |
| 274 | - | ||
| 275 | - | .btn:disabled { | |
| 276 | - | opacity: 0.5; | |
| 277 | - | cursor: not-allowed; | |
| 278 | - | } | |
| 279 | - | ||
| 280 | - | .btn-primary { | |
| 281 | - | background: var(--primary, #3b82f6); | |
| 282 | - | color: white; | |
| 283 | - | border-color: var(--primary, #3b82f6); | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | .btn-primary:hover:not(:disabled) { | |
| 287 | - | filter: brightness(1.1); | |
| 288 | - | } | |
| 289 | - | ||
| 290 | - | .btn-secondary { | |
| 291 | - | background: transparent; | |
| 292 | - | color: var(--foreground, #e5e5e5); | |
| 293 | - | border-color: var(--border, #262626); | |
| 294 | - | } | |
| 295 | - | ||
| 296 | - | .btn-secondary:hover:not(:disabled) { | |
| 297 | - | background: var(--secondary, #1a1a1a); | |
| 298 | - | border-color: var(--muted-foreground, #737373); | |
| 299 | - | } | |
| 300 | - | ||
| 301 | - | .btn-ghost { | |
| 302 | - | background: transparent; | |
| 303 | - | color: var(--muted-foreground, #737373); | |
| 304 | - | border-color: transparent; | |
| 305 | - | } | |
| 306 | - | ||
| 307 | - | .btn-ghost:hover:not(:disabled) { | |
| 308 | - | color: var(--foreground, #e5e5e5); | |
| 309 | - | } | |
| 310 | - | ||
| 311 | - | .loading-spinner { | |
| 312 | - | width: 1rem; | |
| 313 | - | height: 1rem; | |
| 314 | - | border: 2px solid transparent; | |
| 315 | - | border-top-color: currentColor; | |
| 316 | - | border-radius: 50%; | |
| 317 | - | animation: spin 0.6s linear infinite; | |
| 318 | - | } | |
| 319 | - | ||
| 320 | - | @keyframes spin { | |
| 321 | - | to { transform: rotate(360deg); } | |
| 322 | - | } | |
| 323 | - | ||
| 324 | - | .domain-note { | |
| 325 | - | font-size: 0.75rem; | |
| 326 | - | color: var(--muted-foreground, #737373); | |
| 327 | - | margin: 0; | |
| 328 | - | } | |
| 329 | - | ||
| 330 | - | .domain-note strong { | |
| 331 | - | color: var(--foreground, #e5e5e5); | |
| 332 | - | font-weight: 500; | |
| 333 | - | } | |
| 334 | - | </style> | |
| @@ -1,334 +0,0 @@ | |||||
| 1 | - | <script lang="ts"> | |||
| 2 | - | import { onMount } from 'svelte'; | |||
| 3 | - | ||||
| 4 | - | let blockedUrl = $state(''); | |||
| 5 | - | let blockedDomain = $state(''); | |||
| 6 | - | let reason = $state('Content blocked by filter'); | |||
| 7 | - | let isLoading = $state(false); | |||
| 8 | - | let error = $state(''); | |||
| 9 | - | ||||
| 10 | - | onMount(() => { | |||
| 11 | - | // Parse URL parameters | |||
| 12 | - | const params = new URLSearchParams(window.location.search); | |||
| 13 | - | blockedUrl = params.get('url') || ''; | |||
| 14 | - | reason = params.get('reason') || 'Content blocked by filter'; | |||
| 15 | - | ||||
| 16 | - | // Extract domain from URL | |||
| 17 | - | try { | |||
| 18 | - | if (blockedUrl) { | |||
| 19 | - | const url = new URL(blockedUrl); | |||
| 20 | - | blockedDomain = url.hostname; | |||
| 21 | - | } | |||
| 22 | - | } catch { | |||
| 23 | - | blockedDomain = blockedUrl; | |||
| 24 | - | } | |||
| 25 | - | }); | |||
| 26 | - | ||||
| 27 | - | function sendMessage(type: string, payload: Record<string, unknown>): Promise<unknown> { | |||
| 28 | - | return new Promise((resolve, reject) => { | |||
| 29 | - | const requestId = `blocked_${Date.now()}_${Math.random().toString(36).slice(2)}`; | |||
| 30 | - | ||||
| 31 | - | const handler = (event: MessageEvent) => { | |||
| 32 | - | if (event.data?.requestId === requestId) { | |||
| 33 | - | window.removeEventListener('message', handler); | |||
| 34 | - | if (event.data.success) { | |||
| 35 | - | resolve(event.data.data); | |||
| 36 | - | } else { | |||
| 37 | - | reject(new Error(event.data.error || 'Unknown error')); | |||
| 38 | - | } | |||
| 39 | - | } | |||
| 40 | - | }; | |||
| 41 | - | ||||
| 42 | - | window.addEventListener('message', handler); | |||
| 43 | - | ||||
| 44 | - | // Send to WebKit message handler | |||
| 45 | - | if (window.webkit?.messageHandlers?.dumber) { | |||
| 46 | - | window.webkit.messageHandlers.dumber.postMessage( | |||
| 47 | - | JSON.stringify({ type, requestId, payload }) | |||
| 48 | - | ); | |||
| 49 | - | } else { | |||
| 50 | - | reject(new Error('WebKit message handler not available')); | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | // Timeout after 10 seconds | |||
| 54 | - | setTimeout(() => { | |||
| 55 | - | window.removeEventListener('message', handler); | |||
| 56 | - | reject(new Error('Request timeout')); | |||
| 57 | - | }, 10000); | |||
| 58 | - | }); | |||
| 59 | - | } | |||
| 60 | - | ||||
| 61 | - | async function addToWhitelist() { | |||
| 62 | - | if (!blockedDomain) return; | |||
| 63 | - | ||||
| 64 | - | isLoading = true; | |||
| 65 | - | error = ''; | |||
| 66 | - | ||||
| 67 | - | try { | |||
| 68 | - | await sendMessage('addToWhitelist', { domain: blockedDomain }); | |||
| 69 | - | // Reload the original URL after whitelisting | |||
| 70 | - | window.location.href = blockedUrl; | |||
| 71 | - | } catch (e) { | |||
| 72 | - | error = e instanceof Error ? e.message : 'Failed to add to whitelist'; | |||
| 73 | - | isLoading = false; | |||
| 74 | - | } | |||
| 75 | - | } | |||
| 76 | - | ||||
| 77 | - | async function bypassOnce() { | |||
| 78 | - | if (!blockedUrl) return; | |||
| 79 | - | ||||
| 80 | - | isLoading = true; | |||
| 81 | - | error = ''; | |||
| 82 | - | ||||
| 83 | - | try { | |||
| 84 | - | await sendMessage('bypassOnce', { url: blockedUrl }); | |||
| 85 | - | // Reload the original URL after bypass | |||
| 86 | - | window.location.href = blockedUrl; | |||
| 87 | - | } catch (e) { | |||
| 88 | - | error = e instanceof Error ? e.message : 'Failed to bypass'; | |||
| 89 | - | isLoading = false; | |||
| 90 | - | } | |||
| 91 | - | } | |||
| 92 | - | ||||
| 93 | - | function goBack() { | |||
| 94 | - | window.history.back(); | |||
| 95 | - | } | |||
| 96 | - | </script> | |||
| 97 | - | ||||
| 98 | - | <svelte:head> | |||
| 99 | - | <title>Page Blocked</title> | |||
| 100 | - | {@html `<style> | |||
| 101 | - | html, body { | |||
| 102 | - | background: var(--background, #0a0a0a); | |||
| 103 | - | margin: 0; | |||
| 104 | - | padding: 0; | |||
| 105 | - | min-height: 100vh; | |||
| 106 | - | } | |||
| 107 | - | </style>`} | |||
| 108 | - | </svelte:head> | |||
| 109 | - | ||||
| 110 | - | <div class="blocked-container"> | |||
| 111 | - | <div class="blocked-card"> | |||
| 112 | - | <div class="icon-container"> | |||
| 113 | - | <svg class="shield-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |||
| 114 | - | <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/> | |||
| 115 | - | <path d="M9 12l2 2 4-4"/> | |||
| 116 | - | </svg> | |||
| 117 | - | </div> | |||
| 118 | - | ||||
| 119 | - | <h1 class="title">Page Blocked</h1> | |||
| 120 | - | ||||
| 121 | - | <p class="reason">{reason}</p> | |||
| 122 | - | ||||
| 123 | - | {#if blockedUrl} | |||
| 124 | - | <div class="url-display"> | |||
| 125 | - | <span class="url-label">Blocked URL:</span> | |||
| 126 | - | <code class="url-value">{blockedUrl}</code> | |||
| 127 | - | </div> | |||
| 128 | - | {/if} | |||
| 129 | - | ||||
| 130 | - | {#if error} | |||
| 131 | - | <div class="error-message">{error}</div> | |||
| 132 | - | {/if} | |||
| 133 | - | ||||
| 134 | - | <div class="actions"> | |||
| 135 | - | <button | |||
| 136 | - | class="btn btn-primary" | |||
| 137 | - | onclick={addToWhitelist} | |||
| 138 | - | disabled={isLoading || !blockedDomain} | |||
| 139 | - | > | |||
| 140 | - | {#if isLoading} | |||
| 141 | - | <span class="loading-spinner"></span> | |||
| 142 | - | {:else} | |||
| 143 | - | Add to Whitelist | |||
| 144 | - | {/if} | |||
| 145 | - | </button> | |||
| 146 | - | ||||
| 147 | - | <button | |||
| 148 | - | class="btn btn-secondary" | |||
| 149 | - | onclick={bypassOnce} | |||
| 150 | - | disabled={isLoading || !blockedUrl} | |||
| 151 | - | > | |||
| 152 | - | Go Anyway (Once) | |||
| 153 | - | </button> | |||
| 154 | - | ||||
| 155 | - | <button | |||
| 156 | - | class="btn btn-ghost" | |||
| 157 | - | onclick={goBack} | |||
| 158 | - | disabled={isLoading} | |||
| 159 | - | > | |||
| 160 | - | Go Back | |||
| 161 | - | </button> | |||
| 162 | - | </div> | |||
| 163 | - | ||||
| 164 | - | {#if blockedDomain} | |||
| 165 | - | <p class="domain-note"> | |||
| 166 | - | Adding <strong>{blockedDomain}</strong> to whitelist will disable content filtering for this site. | |||
| 167 | - | </p> | |||
| 168 | - | {/if} | |||
| 169 | - | </div> | |||
| 170 | - | </div> | |||
| 171 | - | ||||
| 172 | - | <style> | |||
| 173 | - | .blocked-container { | |||
| 174 | - | min-height: 100vh; | |||
| 175 | - | display: flex; | |||
| 176 | - | align-items: center; | |||
| 177 | - | justify-content: center; | |||
| 178 | - | padding: 2rem; | |||
| 179 | - | background: var(--background, #0a0a0a); | |||
| 180 | - | color: var(--foreground, #e5e5e5); | |||
| 181 | - | font-family: 'Inter', system-ui, -apple-system, sans-serif; | |||
| 182 | - | } | |||
| 183 | - | ||||
| 184 | - | .blocked-card { | |||
| 185 | - | max-width: 480px; | |||
| 186 | - | width: 100%; | |||
| 187 | - | padding: 2.5rem 2rem; | |||
| 188 | - | background: var(--card, #141414); | |||
| 189 | - | border-width: 1px; | |||
| 190 | - | border-style: solid; | |||
| 191 | - | border-color: var(--border, #262626); | |||
| 192 | - | text-align: center; | |||
| 193 | - | } | |||
| 194 | - | ||||
| 195 | - | .icon-container { | |||
| 196 | - | margin-bottom: 1.5rem; | |||
| 197 | - | } | |||
| 198 | - | ||||
| 199 | - | .shield-icon { | |||
| 200 | - | width: 64px; | |||
| 201 | - | height: 64px; | |||
| 202 | - | color: var(--primary, #3b82f6); | |||
| 203 | - | opacity: 0.9; | |||
| 204 | - | } | |||
| 205 | - | ||||
| 206 | - | .title { | |||
| 207 | - | font-size: 1.5rem; | |||
| 208 | - | font-weight: 600; | |||
| 209 | - | margin: 0 0 0.75rem 0; | |||
| 210 | - | color: var(--foreground, #e5e5e5); | |||
| 211 | - | } | |||
| 212 | - | ||||
| 213 | - | .reason { | |||
| 214 | - | font-size: 0.9375rem; | |||
| 215 | - | color: var(--muted-foreground, #737373); | |||
| 216 | - | margin: 0 0 1.5rem 0; | |||
| 217 | - | } | |||
| 218 | - | ||||
| 219 | - | .url-display { | |||
| 220 | - | background: var(--background, #0a0a0a); | |||
| 221 | - | border-width: 1px; | |||
| 222 | - | border-style: solid; | |||
| 223 | - | border-color: var(--border, #262626); | |||
| 224 | - | padding: 0.75rem 1rem; | |||
| 225 | - | margin-bottom: 1.5rem; | |||
| 226 | - | text-align: left; | |||
| 227 | - | } | |||
| 228 | - | ||||
| 229 | - | .url-label { | |||
| 230 | - | display: block; | |||
| 231 | - | font-size: 0.75rem; | |||
| 232 | - | color: var(--muted-foreground, #737373); | |||
| 233 | - | margin-bottom: 0.25rem; | |||
| 234 | - | text-transform: uppercase; | |||
| 235 | - | letter-spacing: 0.05em; | |||
| 236 | - | } | |||
| 237 | - | ||||
| 238 | - | .url-value { | |||
| 239 | - | font-family: 'JetBrains Mono NF', monospace; | |||
| 240 | - | font-size: 0.8125rem; | |||
| 241 | - | color: var(--foreground, #e5e5e5); | |||
| 242 | - | word-break: break-all; | |||
| 243 | - | display: block; | |||
| 244 | - | } | |||
| 245 | - | ||||
| 246 | - | .error-message { | |||
| 247 | - | background: rgba(239, 68, 68, 0.1); | |||
| 248 | - | border: 1px solid rgba(239, 68, 68, 0.3); | |||
| 249 | - | color: #ef4444; | |||
| 250 | - | padding: 0.75rem 1rem; | |||
| 251 | - | margin-bottom: 1.5rem; | |||
| 252 | - | font-size: 0.875rem; | |||
| 253 | - | } | |||
| 254 | - | ||||
| 255 | - | .actions { | |||
| 256 | - | display: flex; | |||
| 257 | - | flex-direction: column; | |||
| 258 | - | gap: 0.75rem; | |||
| 259 | - | margin-bottom: 1.5rem; | |||
| 260 | - | } | |||
| 261 | - | ||||
| 262 | - | .btn { | |||
| 263 | - | padding: 0.75rem 1.5rem; | |||
| 264 | - | font-size: 0.875rem; | |||
| 265 | - | font-weight: 500; | |||
| 266 | - | border: 1px solid transparent; | |||
| 267 | - | cursor: pointer; | |||
| 268 | - | transition: all 150ms ease; | |||
| 269 | - | display: inline-flex; | |||
| 270 | - | align-items: center; | |||
| 271 | - | justify-content: center; | |||
| 272 | - | gap: 0.5rem; | |||
| 273 | - | } | |||
| 274 | - | ||||
| 275 | - | .btn:disabled { | |||
| 276 | - | opacity: 0.5; | |||
| 277 | - | cursor: not-allowed; | |||
| 278 | - | } | |||
| 279 | - | ||||
| 280 | - | .btn-primary { | |||
| 281 | - | background: var(--primary, #3b82f6); | |||
| 282 | - | color: white; | |||
| 283 | - | border-color: var(--primary, #3b82f6); | |||
| 284 | - | } | |||
| 285 | - | ||||
| 286 | - | .btn-primary:hover:not(:disabled) { | |||
| 287 | - | filter: brightness(1.1); | |||
| 288 | - | } | |||
| 289 | - | ||||
| 290 | - | .btn-secondary { | |||
| 291 | - | background: transparent; | |||
| 292 | - | color: var(--foreground, #e5e5e5); | |||
| 293 | - | border-color: var(--border, #262626); | |||
| 294 | - | } | |||
| 295 | - | ||||
| 296 | - | .btn-secondary:hover:not(:disabled) { | |||
| 297 | - | background: var(--secondary, #1a1a1a); | |||
| 298 | - | border-color: var(--muted-foreground, #737373); | |||
| 299 | - | } | |||
| 300 | - | ||||
| 301 | - | .btn-ghost { | |||
| 302 | - | background: transparent; | |||
| 303 | - | color: var(--muted-foreground, #737373); | |||
| 304 | - | border-color: transparent; | |||
| 305 | - | } | |||
| 306 | - | ||||
| 307 | - | .btn-ghost:hover:not(:disabled) { | |||
| 308 | - | color: var(--foreground, #e5e5e5); | |||
| 309 | - | } | |||
| 310 | - | ||||
| 311 | - | .loading-spinner { | |||
| 312 | - | width: 1rem; | |||
| 313 | - | height: 1rem; | |||
| 314 | - | border: 2px solid transparent; | |||
| 315 | - | border-top-color: currentColor; | |||
| 316 | - | border-radius: 50%; | |||
| 317 | - | animation: spin 0.6s linear infinite; | |||
| 318 | - | } | |||
| 319 | - | ||||
| 320 | - | @keyframes spin { | |||
| 321 | - | to { transform: rotate(360deg); } | |||
| 322 | - | } | |||
| 323 | - | ||||
| 324 | - | .domain-note { | |||
| 325 | - | font-size: 0.75rem; | |||
| 326 | - | color: var(--muted-foreground, #737373); | |||
| 327 | - | margin: 0; | |||
| 328 | - | } | |||
| 329 | - | ||||
| 330 | - | .domain-note strong { | |||
| 331 | - | color: var(--foreground, #e5e5e5); | |||
| 332 | - | font-weight: 500; | |||
| 333 | - | } | |||
| 334 | - | </style> | |||
A webui/src/pages/ErrorPage.svelte +185 -0
| @@ -0,0 +1,185 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { onMount } from 'svelte'; | |
| 3 | + | import { Button } from '$lib/components/ui/button'; | |
| 4 | + | import * as Card from '$lib/components/ui/card'; | |
| 5 | + | ||
| 6 | + | // Error configurations | |
| 7 | + | const errorConfigs: Record<string, { code: number; title: string; message: string; icon: string; actions: string[] }> = { | |
| 8 | + | blocked: { code: 0, title: 'Page Blocked', message: 'Content blocked by filter', icon: 'shield', actions: ['whitelist', 'bypass', 'back'] }, | |
| 9 | + | '404': { code: 404, title: 'Page Not Found', message: 'The page you are looking for does not exist.', icon: 'warning', actions: ['back'] }, | |
| 10 | + | '500': { code: 500, title: 'Server Error', message: 'Something went wrong on the server.', icon: 'error', actions: ['retry', 'back'] }, | |
| 11 | + | '502': { code: 502, title: 'Bad Gateway', message: 'The server received an invalid response.', icon: 'error', actions: ['retry', 'back'] }, | |
| 12 | + | '503': { code: 503, title: 'Service Unavailable', message: 'The server is temporarily unavailable.', icon: 'error', actions: ['retry', 'back'] }, | |
| 13 | + | offline: { code: 0, title: 'You Are Offline', message: 'Check your internet connection.', icon: 'offline', actions: ['retry', 'back'] }, | |
| 14 | + | ssl: { code: 0, title: 'Connection Not Secure', message: 'The security certificate is invalid.', icon: 'lock', actions: ['bypass', 'back'] }, | |
| 15 | + | timeout: { code: 0, title: 'Connection Timed Out', message: 'The server took too long to respond.', icon: 'warning', actions: ['retry', 'back'] }, | |
| 16 | + | }; | |
| 17 | + | ||
| 18 | + | let config = $state(errorConfigs['404']); | |
| 19 | + | let targetUrl = $state(''); | |
| 20 | + | let targetDomain = $state(''); | |
| 21 | + | let customMessage = $state(''); | |
| 22 | + | let isLoading = $state(false); | |
| 23 | + | let actionError = $state(''); | |
| 24 | + | ||
| 25 | + | onMount(() => { | |
| 26 | + | const params = new URLSearchParams(window.location.search); | |
| 27 | + | const typeParam = params.get('type') || params.get('code') || '404'; | |
| 28 | + | ||
| 29 | + | config = errorConfigs[typeParam] || { | |
| 30 | + | code: parseInt(typeParam) || 0, | |
| 31 | + | title: `Error ${typeParam}`, | |
| 32 | + | message: 'An unexpected error occurred.', | |
| 33 | + | icon: 'error', | |
| 34 | + | actions: ['retry', 'back'], | |
| 35 | + | }; | |
| 36 | + | ||
| 37 | + | targetUrl = params.get('url') || ''; | |
| 38 | + | customMessage = params.get('reason') || params.get('message') || ''; | |
| 39 | + | ||
| 40 | + | if (targetUrl) { | |
| 41 | + | try { | |
| 42 | + | targetDomain = new URL(targetUrl).hostname; | |
| 43 | + | } catch { | |
| 44 | + | targetDomain = targetUrl; | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | }); | |
| 48 | + | ||
| 49 | + | function sendMessage(type: string, payload: Record<string, unknown>): Promise<unknown> { | |
| 50 | + | return new Promise((resolve, reject) => { | |
| 51 | + | const requestId = `error_${Date.now()}_${Math.random().toString(36).slice(2)}`; | |
| 52 | + | const handler = (event: MessageEvent) => { | |
| 53 | + | if (event.data?.requestId === requestId) { | |
| 54 | + | window.removeEventListener('message', handler); | |
| 55 | + | event.data.success ? resolve(event.data.data) : reject(new Error(event.data.error || 'Unknown error')); | |
| 56 | + | } | |
| 57 | + | }; | |
| 58 | + | window.addEventListener('message', handler); | |
| 59 | + | if (window.webkit?.messageHandlers?.dumber) { | |
| 60 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ type, requestId, payload })); | |
| 61 | + | } else { | |
| 62 | + | reject(new Error('WebKit message handler not available')); | |
| 63 | + | } | |
| 64 | + | setTimeout(() => { window.removeEventListener('message', handler); reject(new Error('Timeout')); }, 10000); | |
| 65 | + | }); | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | async function addToWhitelist() { | |
| 69 | + | if (!targetDomain) return; | |
| 70 | + | isLoading = true; | |
| 71 | + | actionError = ''; | |
| 72 | + | try { | |
| 73 | + | await sendMessage('addToWhitelist', { domain: targetDomain }); | |
| 74 | + | window.location.href = targetUrl; | |
| 75 | + | } catch (e) { | |
| 76 | + | actionError = e instanceof Error ? e.message : 'Failed'; | |
| 77 | + | isLoading = false; | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | async function bypassOnce() { | |
| 82 | + | if (!targetUrl) return; | |
| 83 | + | isLoading = true; | |
| 84 | + | actionError = ''; | |
| 85 | + | try { | |
| 86 | + | await sendMessage('bypassOnce', { url: targetUrl }); | |
| 87 | + | window.location.href = targetUrl; | |
| 88 | + | } catch (e) { | |
| 89 | + | actionError = e instanceof Error ? e.message : 'Failed'; | |
| 90 | + | isLoading = false; | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | function retry() { | |
| 95 | + | targetUrl ? window.location.href = targetUrl : window.location.reload(); | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | function goBack() { | |
| 99 | + | window.history.back(); | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | const icons: Record<string, string> = { | |
| 103 | + | shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>', | |
| 104 | + | warning: '<path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>', | |
| 105 | + | error: '<circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6"/><path d="M9 9l6 6"/>', | |
| 106 | + | offline: '<line x1="1" y1="1" x2="23" y2="23"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.58 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/>', | |
| 107 | + | lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>', | |
| 108 | + | }; | |
| 109 | + | ||
| 110 | + | const iconColors: Record<string, string> = { | |
| 111 | + | shield: 'text-primary', | |
| 112 | + | warning: 'text-yellow-500', | |
| 113 | + | error: 'text-destructive', | |
| 114 | + | offline: 'text-muted-foreground', | |
| 115 | + | lock: 'text-yellow-500', | |
| 116 | + | }; | |
| 117 | + | </script> | |
| 118 | + | ||
| 119 | + | <svelte:head> | |
| 120 | + | <title>{config.title}</title> | |
| 121 | + | </svelte:head> | |
| 122 | + | ||
| 123 | + | <div class="min-h-screen flex items-center justify-center p-8 bg-background"> | |
| 124 | + | <Card.Root class="w-full max-w-md"> | |
| 125 | + | <Card.Header class="text-center"> | |
| 126 | + | <div class="flex justify-center mb-4"> | |
| 127 | + | <svg class="w-16 h-16 {iconColors[config.icon] || 'text-muted-foreground'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |
| 128 | + | {@html icons[config.icon] || icons.error} | |
| 129 | + | </svg> | |
| 130 | + | </div> | |
| 131 | + | {#if config.code > 0} | |
| 132 | + | <div class="text-6xl font-bold text-muted-foreground/30 mb-2">{config.code}</div> | |
| 133 | + | {/if} | |
| 134 | + | <Card.Title class="text-xl">{config.title}</Card.Title> | |
| 135 | + | <Card.Description>{customMessage || config.message}</Card.Description> | |
| 136 | + | </Card.Header> | |
| 137 | + | ||
| 138 | + | <Card.Content class="space-y-4"> | |
| 139 | + | {#if targetUrl} | |
| 140 | + | <div class="bg-muted p-3 rounded-md"> | |
| 141 | + | <p class="text-xs text-muted-foreground uppercase tracking-wide mb-1">URL</p> | |
| 142 | + | <code class="text-sm break-all">{targetUrl}</code> | |
| 143 | + | </div> | |
| 144 | + | {/if} | |
| 145 | + | ||
| 146 | + | {#if actionError} | |
| 147 | + | <div class="bg-destructive/10 border border-destructive/30 text-destructive p-3 rounded-md text-sm"> | |
| 148 | + | {actionError} | |
| 149 | + | </div> | |
| 150 | + | {/if} | |
| 151 | + | </Card.Content> | |
| 152 | + | ||
| 153 | + | <Card.Footer class="flex flex-col gap-3"> | |
| 154 | + | {#if config.actions.includes('whitelist') && targetDomain} | |
| 155 | + | <Button class="w-full" onclick={addToWhitelist} disabled={isLoading}> | |
| 156 | + | {isLoading ? 'Adding...' : 'Add to Whitelist'} | |
| 157 | + | </Button> | |
| 158 | + | {/if} | |
| 159 | + | ||
| 160 | + | {#if config.actions.includes('bypass') && targetUrl} | |
| 161 | + | <Button variant="outline" class="w-full" onclick={bypassOnce} disabled={isLoading}> | |
| 162 | + | Proceed Anyway | |
| 163 | + | </Button> | |
| 164 | + | {/if} | |
| 165 | + | ||
| 166 | + | {#if config.actions.includes('retry')} | |
| 167 | + | <Button class="w-full" onclick={retry} disabled={isLoading}> | |
| 168 | + | Try Again | |
| 169 | + | </Button> | |
| 170 | + | {/if} | |
| 171 | + | ||
| 172 | + | {#if config.actions.includes('back')} | |
| 173 | + | <Button variant="ghost" class="w-full" onclick={goBack} disabled={isLoading}> | |
| 174 | + | Go Back | |
| 175 | + | </Button> | |
| 176 | + | {/if} | |
| 177 | + | ||
| 178 | + | {#if config.actions.includes('whitelist') && targetDomain} | |
| 179 | + | <p class="text-xs text-muted-foreground text-center"> | |
| 180 | + | Adding <strong class="text-foreground">{targetDomain}</strong> to whitelist will disable content filtering. | |
| 181 | + | </p> | |
| 182 | + | {/if} | |
| 183 | + | </Card.Footer> | |
| 184 | + | </Card.Root> | |
| 185 | + | </div> | |
| @@ -0,0 +1,185 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { onMount } from 'svelte'; | |||
| 3 | + | import { Button } from '$lib/components/ui/button'; | |||
| 4 | + | import * as Card from '$lib/components/ui/card'; | |||
| 5 | + | ||||
| 6 | + | // Error configurations | |||
| 7 | + | const errorConfigs: Record<string, { code: number; title: string; message: string; icon: string; actions: string[] }> = { | |||
| 8 | + | blocked: { code: 0, title: 'Page Blocked', message: 'Content blocked by filter', icon: 'shield', actions: ['whitelist', 'bypass', 'back'] }, | |||
| 9 | + | '404': { code: 404, title: 'Page Not Found', message: 'The page you are looking for does not exist.', icon: 'warning', actions: ['back'] }, | |||
| 10 | + | '500': { code: 500, title: 'Server Error', message: 'Something went wrong on the server.', icon: 'error', actions: ['retry', 'back'] }, | |||
| 11 | + | '502': { code: 502, title: 'Bad Gateway', message: 'The server received an invalid response.', icon: 'error', actions: ['retry', 'back'] }, | |||
| 12 | + | '503': { code: 503, title: 'Service Unavailable', message: 'The server is temporarily unavailable.', icon: 'error', actions: ['retry', 'back'] }, | |||
| 13 | + | offline: { code: 0, title: 'You Are Offline', message: 'Check your internet connection.', icon: 'offline', actions: ['retry', 'back'] }, | |||
| 14 | + | ssl: { code: 0, title: 'Connection Not Secure', message: 'The security certificate is invalid.', icon: 'lock', actions: ['bypass', 'back'] }, | |||
| 15 | + | timeout: { code: 0, title: 'Connection Timed Out', message: 'The server took too long to respond.', icon: 'warning', actions: ['retry', 'back'] }, | |||
| 16 | + | }; | |||
| 17 | + | ||||
| 18 | + | let config = $state(errorConfigs['404']); | |||
| 19 | + | let targetUrl = $state(''); | |||
| 20 | + | let targetDomain = $state(''); | |||
| 21 | + | let customMessage = $state(''); | |||
| 22 | + | let isLoading = $state(false); | |||
| 23 | + | let actionError = $state(''); | |||
| 24 | + | ||||
| 25 | + | onMount(() => { | |||
| 26 | + | const params = new URLSearchParams(window.location.search); | |||
| 27 | + | const typeParam = params.get('type') || params.get('code') || '404'; | |||
| 28 | + | ||||
| 29 | + | config = errorConfigs[typeParam] || { | |||
| 30 | + | code: parseInt(typeParam) || 0, | |||
| 31 | + | title: `Error ${typeParam}`, | |||
| 32 | + | message: 'An unexpected error occurred.', | |||
| 33 | + | icon: 'error', | |||
| 34 | + | actions: ['retry', 'back'], | |||
| 35 | + | }; | |||
| 36 | + | ||||
| 37 | + | targetUrl = params.get('url') || ''; | |||
| 38 | + | customMessage = params.get('reason') || params.get('message') || ''; | |||
| 39 | + | ||||
| 40 | + | if (targetUrl) { | |||
| 41 | + | try { | |||
| 42 | + | targetDomain = new URL(targetUrl).hostname; | |||
| 43 | + | } catch { | |||
| 44 | + | targetDomain = targetUrl; | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | }); | |||
| 48 | + | ||||
| 49 | + | function sendMessage(type: string, payload: Record<string, unknown>): Promise<unknown> { | |||
| 50 | + | return new Promise((resolve, reject) => { | |||
| 51 | + | const requestId = `error_${Date.now()}_${Math.random().toString(36).slice(2)}`; | |||
| 52 | + | const handler = (event: MessageEvent) => { | |||
| 53 | + | if (event.data?.requestId === requestId) { | |||
| 54 | + | window.removeEventListener('message', handler); | |||
| 55 | + | event.data.success ? resolve(event.data.data) : reject(new Error(event.data.error || 'Unknown error')); | |||
| 56 | + | } | |||
| 57 | + | }; | |||
| 58 | + | window.addEventListener('message', handler); | |||
| 59 | + | if (window.webkit?.messageHandlers?.dumber) { | |||
| 60 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ type, requestId, payload })); | |||
| 61 | + | } else { | |||
| 62 | + | reject(new Error('WebKit message handler not available')); | |||
| 63 | + | } | |||
| 64 | + | setTimeout(() => { window.removeEventListener('message', handler); reject(new Error('Timeout')); }, 10000); | |||
| 65 | + | }); | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | async function addToWhitelist() { | |||
| 69 | + | if (!targetDomain) return; | |||
| 70 | + | isLoading = true; | |||
| 71 | + | actionError = ''; | |||
| 72 | + | try { | |||
| 73 | + | await sendMessage('addToWhitelist', { domain: targetDomain }); | |||
| 74 | + | window.location.href = targetUrl; | |||
| 75 | + | } catch (e) { | |||
| 76 | + | actionError = e instanceof Error ? e.message : 'Failed'; | |||
| 77 | + | isLoading = false; | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | async function bypassOnce() { | |||
| 82 | + | if (!targetUrl) return; | |||
| 83 | + | isLoading = true; | |||
| 84 | + | actionError = ''; | |||
| 85 | + | try { | |||
| 86 | + | await sendMessage('bypassOnce', { url: targetUrl }); | |||
| 87 | + | window.location.href = targetUrl; | |||
| 88 | + | } catch (e) { | |||
| 89 | + | actionError = e instanceof Error ? e.message : 'Failed'; | |||
| 90 | + | isLoading = false; | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | function retry() { | |||
| 95 | + | targetUrl ? window.location.href = targetUrl : window.location.reload(); | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | function goBack() { | |||
| 99 | + | window.history.back(); | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | const icons: Record<string, string> = { | |||
| 103 | + | shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>', | |||
| 104 | + | warning: '<path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>', | |||
| 105 | + | error: '<circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6"/><path d="M9 9l6 6"/>', | |||
| 106 | + | offline: '<line x1="1" y1="1" x2="23" y2="23"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.58 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/>', | |||
| 107 | + | lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>', | |||
| 108 | + | }; | |||
| 109 | + | ||||
| 110 | + | const iconColors: Record<string, string> = { | |||
| 111 | + | shield: 'text-primary', | |||
| 112 | + | warning: 'text-yellow-500', | |||
| 113 | + | error: 'text-destructive', | |||
| 114 | + | offline: 'text-muted-foreground', | |||
| 115 | + | lock: 'text-yellow-500', | |||
| 116 | + | }; | |||
| 117 | + | </script> | |||
| 118 | + | ||||
| 119 | + | <svelte:head> | |||
| 120 | + | <title>{config.title}</title> | |||
| 121 | + | </svelte:head> | |||
| 122 | + | ||||
| 123 | + | <div class="min-h-screen flex items-center justify-center p-8 bg-background"> | |||
| 124 | + | <Card.Root class="w-full max-w-md"> | |||
| 125 | + | <Card.Header class="text-center"> | |||
| 126 | + | <div class="flex justify-center mb-4"> | |||
| 127 | + | <svg class="w-16 h-16 {iconColors[config.icon] || 'text-muted-foreground'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |||
| 128 | + | {@html icons[config.icon] || icons.error} | |||
| 129 | + | </svg> | |||
| 130 | + | </div> | |||
| 131 | + | {#if config.code > 0} | |||
| 132 | + | <div class="text-6xl font-bold text-muted-foreground/30 mb-2">{config.code}</div> | |||
| 133 | + | {/if} | |||
| 134 | + | <Card.Title class="text-xl">{config.title}</Card.Title> | |||
| 135 | + | <Card.Description>{customMessage || config.message}</Card.Description> | |||
| 136 | + | </Card.Header> | |||
| 137 | + | ||||
| 138 | + | <Card.Content class="space-y-4"> | |||
| 139 | + | {#if targetUrl} | |||
| 140 | + | <div class="bg-muted p-3 rounded-md"> | |||
| 141 | + | <p class="text-xs text-muted-foreground uppercase tracking-wide mb-1">URL</p> | |||
| 142 | + | <code class="text-sm break-all">{targetUrl}</code> | |||
| 143 | + | </div> | |||
| 144 | + | {/if} | |||
| 145 | + | ||||
| 146 | + | {#if actionError} | |||
| 147 | + | <div class="bg-destructive/10 border border-destructive/30 text-destructive p-3 rounded-md text-sm"> | |||
| 148 | + | {actionError} | |||
| 149 | + | </div> | |||
| 150 | + | {/if} | |||
| 151 | + | </Card.Content> | |||
| 152 | + | ||||
| 153 | + | <Card.Footer class="flex flex-col gap-3"> | |||
| 154 | + | {#if config.actions.includes('whitelist') && targetDomain} | |||
| 155 | + | <Button class="w-full" onclick={addToWhitelist} disabled={isLoading}> | |||
| 156 | + | {isLoading ? 'Adding...' : 'Add to Whitelist'} | |||
| 157 | + | </Button> | |||
| 158 | + | {/if} | |||
| 159 | + | ||||
| 160 | + | {#if config.actions.includes('bypass') && targetUrl} | |||
| 161 | + | <Button variant="outline" class="w-full" onclick={bypassOnce} disabled={isLoading}> | |||
| 162 | + | Proceed Anyway | |||
| 163 | + | </Button> | |||
| 164 | + | {/if} | |||
| 165 | + | ||||
| 166 | + | {#if config.actions.includes('retry')} | |||
| 167 | + | <Button class="w-full" onclick={retry} disabled={isLoading}> | |||
| 168 | + | Try Again | |||
| 169 | + | </Button> | |||
| 170 | + | {/if} | |||
| 171 | + | ||||
| 172 | + | {#if config.actions.includes('back')} | |||
| 173 | + | <Button variant="ghost" class="w-full" onclick={goBack} disabled={isLoading}> | |||
| 174 | + | Go Back | |||
| 175 | + | </Button> | |||
| 176 | + | {/if} | |||
| 177 | + | ||||
| 178 | + | {#if config.actions.includes('whitelist') && targetDomain} | |||
| 179 | + | <p class="text-xs text-muted-foreground text-center"> | |||
| 180 | + | Adding <strong class="text-foreground">{targetDomain}</strong> to whitelist will disable content filtering. | |||
| 181 | + | </p> | |||
| 182 | + | {/if} | |||
| 183 | + | </Card.Footer> | |||
| 184 | + | </Card.Root> | |||
| 185 | + | </div> | |||
D webui/src/pages/blocked.ts +0 -12
| @@ -1,12 +0,0 @@ | |||
| 1 | - | import { mount } from "svelte"; | |
| 2 | - | import "../styles/app.css"; | |
| 3 | - | import BlockedPage from "./BlockedPage.svelte"; | |
| 4 | - | ||
| 5 | - | // Mount the BlockedPage component to the DOM | |
| 6 | - | console.log("[dumber] Mounting BlockedPage component to document.body"); | |
| 7 | - | try { | |
| 8 | - | const app = mount(BlockedPage, { target: document.body }); | |
| 9 | - | console.log("[dumber] BlockedPage component mounted successfully", app); | |
| 10 | - | } catch (error) { | |
| 11 | - | console.error("[dumber] Failed to mount BlockedPage component:", error); | |
| 12 | - | } | |
| @@ -1,12 +0,0 @@ | |||||
| 1 | - | import { mount } from "svelte"; | |||
| 2 | - | import "../styles/app.css"; | |||
| 3 | - | import BlockedPage from "./BlockedPage.svelte"; | |||
| 4 | - | ||||
| 5 | - | // Mount the BlockedPage component to the DOM | |||
| 6 | - | console.log("[dumber] Mounting BlockedPage component to document.body"); | |||
| 7 | - | try { | |||
| 8 | - | const app = mount(BlockedPage, { target: document.body }); | |||
| 9 | - | console.log("[dumber] BlockedPage component mounted successfully", app); | |||
| 10 | - | } catch (error) { | |||
| 11 | - | console.error("[dumber] Failed to mount BlockedPage component:", error); | |||
| 12 | - | } | |||
A webui/src/pages/error.ts +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | import { mount } from "svelte"; | |
| 2 | + | import "../styles/app.css"; | |
| 3 | + | import ErrorPage from "./ErrorPage.svelte"; | |
| 4 | + | ||
| 5 | + | console.log("[dumber] Mounting ErrorPage component"); | |
| 6 | + | try { | |
| 7 | + | const app = mount(ErrorPage, { target: document.body }); | |
| 8 | + | console.log("[dumber] ErrorPage mounted successfully", app); | |
| 9 | + | } catch (error) { | |
| 10 | + | console.error("[dumber] Failed to mount ErrorPage:", error); | |
| 11 | + | } | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | import { mount } from "svelte"; | |||
| 2 | + | import "../styles/app.css"; | |||
| 3 | + | import ErrorPage from "./ErrorPage.svelte"; | |||
| 4 | + | ||||
| 5 | + | console.log("[dumber] Mounting ErrorPage component"); | |||
| 6 | + | try { | |||
| 7 | + | const app = mount(ErrorPage, { target: document.body }); | |||
| 8 | + | console.log("[dumber] ErrorPage mounted successfully", app); | |||
| 9 | + | } catch (error) { | |||
| 10 | + | console.error("[dumber] Failed to mount ErrorPage:", error); | |||
| 11 | + | } | |||