An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(identity-wallet): create AlertDetailScreen component

Create new screen showing unauthorized PLC operation details:
- Display signing key, detection timestamp, recovery deadline
- Real-time countdown with 60-second updates via setInterval
- Urgency badges (safe/warning/critical/expired) with color coding
- Disabled placeholder 'Review & Override' button for Phase 7
- Back button to return to home/identity list

Verifies: plc-key-management.AC6.5 (alert detail display)

+254
+254
apps/identity-wallet/src/lib/components/home/AlertDetailScreen.svelte
··· 1 + <script lang="ts"> 2 + import { onMount, onDestroy } from 'svelte'; 3 + import { getUrgency, getDeadline, type Urgency } from '$lib/utils/deadline'; 4 + import type { UnauthorizedChange } from '$lib/ipc'; 5 + import { truncateDid } from '$lib/did-doc-utils'; 6 + 7 + let { 8 + did, 9 + changes, 10 + onback, 11 + }: { 12 + did: string; 13 + changes: UnauthorizedChange[]; 14 + onback: () => void; 15 + } = $props(); 16 + 17 + let now = $state(Date.now()); 18 + let timer: ReturnType<typeof setInterval> | null = null; 19 + 20 + onMount(() => { 21 + timer = setInterval(() => { 22 + now = Date.now(); 23 + }, 60_000); 24 + }); 25 + 26 + onDestroy(() => { 27 + if (timer) clearInterval(timer); 28 + }); 29 + </script> 30 + 31 + <div class="screen"> 32 + <div class="header"> 33 + <button class="back-btn" onclick={onback} aria-label="Back">‹ Back</button> 34 + <h2 class="title">Security Alerts</h2> 35 + </div> 36 + 37 + <!-- Identity section --> 38 + <div class="section"> 39 + <p class="section-label">Identity</p> 40 + <p class="mono-value">{truncateDid(did)}</p> 41 + </div> 42 + 43 + <!-- Alert cards --> 44 + <div class="alerts-container"> 45 + {#each changes as change (change.cid)} 46 + {@const deadline = getDeadline(change.createdAt)} 47 + {@const remaining = deadline.getTime() - now} 48 + {@const urgency = getUrgency(deadline, now)} 49 + 50 + <div class="alert-card"> 51 + <div class="alert-header"> 52 + <span class="alert-urgency alert-urgency--{urgency}"> 53 + <span class="badge-dot"></span> 54 + {remaining <= 0 55 + ? 'Expired' 56 + : `${Math.floor(remaining / 3600000)}h ${Math.floor((remaining % 3600000) / 60000)}m remaining`} 57 + </span> 58 + </div> 59 + 60 + <div class="alert-field"> 61 + <span class="alert-label">Signing Key</span> 62 + <span class="alert-value monospace">{change.signingKey ?? 'Unknown key'}</span> 63 + </div> 64 + 65 + <div class="alert-field"> 66 + <span class="alert-label">Detected</span> 67 + <span class="alert-value">{new Date(change.createdAt).toLocaleString()}</span> 68 + </div> 69 + 70 + <div class="alert-field"> 71 + <span class="alert-label">Recovery Deadline</span> 72 + <span class="alert-value">{deadline.toLocaleString()}</span> 73 + </div> 74 + 75 + <button class="action-button" disabled> 76 + Review & Override 77 + </button> 78 + </div> 79 + {/each} 80 + </div> 81 + </div> 82 + 83 + <style> 84 + .screen { 85 + display: flex; 86 + flex-direction: column; 87 + height: 100%; 88 + padding: 2rem 1.5rem; 89 + gap: 1.25rem; 90 + overflow-y: auto; 91 + } 92 + 93 + .header { 94 + display: flex; 95 + align-items: center; 96 + gap: 0.75rem; 97 + } 98 + 99 + .back-btn { 100 + background: none; 101 + border: none; 102 + font-size: 1rem; 103 + color: #007aff; 104 + cursor: pointer; 105 + padding: 0; 106 + font-weight: 500; 107 + white-space: nowrap; 108 + } 109 + 110 + .title { 111 + font-size: 1.2rem; 112 + font-weight: 700; 113 + color: #111827; 114 + margin: 0; 115 + } 116 + 117 + .section { 118 + display: flex; 119 + flex-direction: column; 120 + gap: 0.5rem; 121 + } 122 + 123 + .section-label { 124 + font-size: 0.75rem; 125 + font-weight: 600; 126 + color: #374151; 127 + margin: 0; 128 + text-transform: uppercase; 129 + letter-spacing: 0.04em; 130 + } 131 + 132 + .mono-value { 133 + font-family: monospace; 134 + font-size: 0.8rem; 135 + color: #374151; 136 + margin: 0; 137 + word-break: break-all; 138 + } 139 + 140 + .alerts-container { 141 + display: flex; 142 + flex-direction: column; 143 + gap: 0.75rem; 144 + } 145 + 146 + .alert-card { 147 + background: #f9fafb; 148 + border: 1px solid #d1d5db; 149 + border-radius: 12px; 150 + padding: 1.25rem; 151 + display: flex; 152 + flex-direction: column; 153 + gap: 1rem; 154 + } 155 + 156 + .alert-header { 157 + display: flex; 158 + align-items: center; 159 + gap: 0.5rem; 160 + } 161 + 162 + .alert-urgency { 163 + display: flex; 164 + align-items: center; 165 + gap: 0.4rem; 166 + padding: 0.4rem 0.8rem; 167 + border-radius: 6px; 168 + font-size: 0.75rem; 169 + font-weight: 600; 170 + white-space: nowrap; 171 + } 172 + 173 + .badge-dot { 174 + width: 6px; 175 + height: 6px; 176 + border-radius: 50%; 177 + flex-shrink: 0; 178 + } 179 + 180 + .alert-urgency--safe { 181 + background: #dcfce7; 182 + color: #166534; 183 + } 184 + 185 + .alert-urgency--safe .badge-dot { 186 + background: #16a34a; 187 + } 188 + 189 + .alert-urgency--warning { 190 + background: #fef3c7; 191 + color: #92400e; 192 + } 193 + 194 + .alert-urgency--warning .badge-dot { 195 + background: #f59e0b; 196 + } 197 + 198 + .alert-urgency--critical, 199 + .alert-urgency--expired { 200 + background: #fef2f2; 201 + color: #991b1b; 202 + } 203 + 204 + .alert-urgency--critical .badge-dot, 205 + .alert-urgency--expired .badge-dot { 206 + background: #ef4444; 207 + } 208 + 209 + .alert-field { 210 + display: flex; 211 + flex-direction: column; 212 + gap: 0.25rem; 213 + } 214 + 215 + .alert-label { 216 + font-size: 0.75rem; 217 + font-weight: 600; 218 + color: #374151; 219 + margin: 0; 220 + text-transform: uppercase; 221 + letter-spacing: 0.04em; 222 + } 223 + 224 + .alert-value { 225 + font-size: 1rem; 226 + color: #374151; 227 + margin: 0; 228 + line-height: 1.4; 229 + } 230 + 231 + .monospace { 232 + font-family: monospace; 233 + font-size: 0.8rem; 234 + word-break: break-all; 235 + } 236 + 237 + .action-button { 238 + width: 100%; 239 + padding: 0.9rem; 240 + background: #007aff; 241 + color: #fff; 242 + border: none; 243 + border-radius: 12px; 244 + font-size: 0.9rem; 245 + font-weight: 600; 246 + cursor: not-allowed; 247 + opacity: 0.5; 248 + margin-top: 0.5rem; 249 + } 250 + 251 + .action-button:active:not(:disabled) { 252 + background: #0051d5; 253 + } 254 + </style>