Files
nebula/_migrate/device.html

1311 lines
42 KiB
HTML

<!DOCTYPE html>
<!--
================================================================================
TEMPL MIGRATION GUIDE: device.html → views/device.templ
================================================================================
PAGE OVERVIEW:
- Device detail page showing comprehensive information about a linked passkey/device
- Hero section: Device icon, name, type, status badge, last used, action buttons
- Security info: Authentication type, credential ID, public key algorithm, attestation
- Activity timeline: Recent authentication events for this device
- Session history: Active and past sessions initiated from this device
- Risk assessment: Security score, anomaly detection, location history
- Similar to password manager device detail or 1Password device view
MAIN TEMPL COMPONENT:
templ DevicePage(device DeviceData) {
@layouts.DashboardLayout("settings") {
@DeviceHero(device.Info, device.Status)
@DeviceStatsGrid(device.Stats)
@SecurityInfoCard(device.Security)
<div class="wa-grid">
@ActivityTimelineCard(device.Activity)
@SessionHistoryCard(device.Sessions)
</div>
@RiskAssessmentCard(device.Risk)
}
}
HTMX INTEGRATION:
- Rename device: hx-post="/api/devices/{id}/rename" hx-vals='{"name":"New Name"}' hx-target="#device-name"
- Revoke device: hx-delete="/api/devices/{id}" hx-confirm="Remove this device?" hx-target="body" hx-push-url="/settings?tab=devices"
- Revoke session: hx-delete="/api/sessions/{sessionId}" hx-target="closest .session-item" hx-swap="delete"
- Revoke all sessions: hx-delete="/api/devices/{id}/sessions" hx-target="#sessions-list" hx-confirm="Sign out all sessions?"
- Activity refresh: hx-get="/api/devices/{id}/activity" hx-trigger="every 30s" hx-target="#activity-timeline"
- Mark trusted: hx-post="/api/devices/{id}/trust" hx-target="#trust-status"
- Block device: hx-post="/api/devices/{id}/block" hx-target="#device-status"
SUB-COMPONENTS TO EXTRACT:
- DeviceHero(info DeviceInfo, status DeviceStatus)
- DeviceIcon(type string, os string, size string)
- DeviceStatusBadge(status string, lastUsed time.Time)
- DeviceStatsGrid(stats DeviceStats)
- StatItem(label string, value string, icon string)
- SecurityInfoCard(security SecurityInfo)
- CredentialDisplay(credentialId string, publicKey string)
- AttestationInfo(attestation AttestationData)
- ActivityTimelineCard(events []ActivityEvent)
- ActivityItem(event ActivityEvent)
- SessionHistoryCard(sessions []Session)
- SessionItem(session Session, isCurrent bool)
- RiskAssessmentCard(risk RiskData)
- SecurityScoreGauge(score int)
- LocationHistoryMap(locations []Location)
STATE/PROPS:
type DeviceData struct {
Info DeviceInfo
Status DeviceStatus
Stats DeviceStats
Security SecurityInfo
Activity []ActivityEvent
Sessions []Session
Risk RiskData
}
type DeviceInfo struct {
ID string
Name string
Type string // "laptop", "mobile", "tablet", "desktop", "security-key"
Browser string
BrowserVer string
OS string
OSVersion string
AuthType string // "Touch ID", "Face ID", "FIDO2", "Windows Hello", "PIN"
AddedAt time.Time
AddedBy string // User agent or registration method
}
type DeviceStatus struct {
IsActive bool
IsCurrent bool
IsTrusted bool
IsBlocked bool
LastUsed time.Time
LastLocation string
LastIP string
}
type DeviceStats struct {
TotalLogins int
SuccessfulLogins int
FailedAttempts int
SessionsCreated int
ActiveSessions int
DaysSinceAdded int
}
type SecurityInfo struct {
CredentialID string
PublicKeyAlg string // "ES256", "RS256", "EdDSA"
AttestationType string // "none", "self", "packed", "tpm", "android-key"
AAGUID string
Transports []string // "internal", "usb", "nfc", "ble", "hybrid"
BackupEligible bool
BackupState bool
UserVerified bool
CreatedAt time.Time
}
type ActivityEvent struct {
ID string
Type string // "login", "logout", "failed", "session_refresh", "permission_grant"
Timestamp time.Time
IP string
Location string
UserAgent string
Success bool
Details string
}
type Session struct {
ID string
StartedAt time.Time
LastActive time.Time
ExpiresAt time.Time
IP string
Location string
IsCurrent bool
IsExpired bool
}
type RiskData struct {
SecurityScore int // 0-100
RiskLevel string // "low", "medium", "high"
Anomalies []Anomaly
TrustedLocations []Location
RecentLocations []Location
}
type Anomaly struct {
Type string // "new_location", "unusual_time", "rapid_geo_change", "failed_attempts"
Description string
DetectedAt time.Time
Resolved bool
}
type Location struct {
City string
Country string
IP string
LastSeen time.Time
}
HTMX PATTERNS:
// Device rename inline edit
<form hx-post="/api/devices/{id}/rename"
hx-target="#device-name-display"
hx-swap="outerHTML">
<wa-input name="name" value="{device.Name}"></wa-input>
</form>
// Device revocation with redirect
<wa-button hx-delete="/api/devices/{id}"
hx-confirm="Remove this device? You'll need to re-register to use it again."
hx-target="body"
hx-push-url="/settings?tab=devices">
Remove Device
</wa-button>
// Session revocation
<wa-button hx-delete="/api/sessions/{session.ID}"
hx-target="closest .session-item"
hx-swap="delete"
hx-confirm="End this session?">
Revoke
</wa-button>
// Activity auto-refresh
<div id="activity-timeline"
hx-get="/api/devices/{id}/activity"
hx-trigger="every 30s"
hx-swap="innerHTML">
// Trust toggle
<wa-button hx-post="/api/devices/{id}/trust"
hx-target="#trust-badge"
hx-swap="outerHTML">
Mark as Trusted
</wa-button>
================================================================================
-->
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MacBook Pro - Sonr Motr Wallet</title>
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
<style>
:root {
--wa-color-primary: #17c2ff;
--device-color: #6366f1;
}
html, body {
min-height: 100%;
padding: 0;
margin: 0;
}
.dashboard-layout {
display: grid;
grid-template-columns: 240px 1fr;
min-height: 100vh;
}
@media (max-width: 768px) {
.dashboard-layout {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
}
.sidebar {
border-right: 1px solid var(--wa-color-neutral-200);
padding: var(--wa-space-m);
background: var(--wa-color-surface);
}
.sidebar-header {
padding: var(--wa-space-s) var(--wa-space-xs);
margin-bottom: var(--wa-space-m);
}
.sidebar-nav {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-nav li {
margin-bottom: var(--wa-space-2xs);
}
.sidebar-nav a {
display: flex;
align-items: center;
gap: var(--wa-space-s);
padding: var(--wa-space-s) var(--wa-space-m);
border-radius: var(--wa-radius-m);
text-decoration: none;
color: var(--wa-color-neutral-700);
font-size: var(--wa-font-size-s);
transition: background 0.15s;
}
.sidebar-nav a:hover {
background: var(--wa-color-surface-alt);
}
.sidebar-nav a.active {
background: var(--wa-color-primary-subtle);
color: var(--wa-color-primary);
font-weight: 500;
}
.main-content {
padding: var(--wa-space-xl);
background: var(--wa-color-surface-alt);
overflow-y: auto;
}
/* Device Hero Section */
.device-hero {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
gap: var(--wa-space-xl);
margin-bottom: var(--wa-space-xl);
padding: var(--wa-space-xl);
background: var(--wa-color-surface);
border-radius: var(--wa-radius-l);
}
.device-identity {
display: flex;
align-items: center;
gap: var(--wa-space-l);
}
.device-icon {
width: 72px;
height: 72px;
border-radius: var(--wa-radius-l);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: white;
background: linear-gradient(135deg, var(--device-color) 0%, color-mix(in srgb, var(--device-color) 70%, black) 100%);
}
.device-name-block {
display: flex;
flex-direction: column;
gap: var(--wa-space-2xs);
}
.device-badges {
display: flex;
gap: var(--wa-space-xs);
margin-top: var(--wa-space-2xs);
}
.status-block {
text-align: right;
}
.last-used {
font-size: var(--wa-font-size-s);
color: var(--wa-color-neutral-500);
margin-top: var(--wa-space-xs);
}
.last-used strong {
color: var(--wa-color-neutral-700);
}
.status-details {
display: flex;
flex-direction: column;
gap: var(--wa-space-2xs);
margin-top: var(--wa-space-m);
padding-top: var(--wa-space-m);
border-top: 1px solid var(--wa-color-neutral-100);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
text-align: right;
}
.hero-actions {
display: flex;
gap: var(--wa-space-s);
width: 100%;
margin-top: var(--wa-space-l);
padding-top: var(--wa-space-l);
border-top: 1px solid var(--wa-color-neutral-100);
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: var(--wa-space-m);
margin-bottom: var(--wa-space-xl);
}
.stat-item {
background: var(--wa-color-surface);
padding: var(--wa-space-m);
border-radius: var(--wa-radius-m);
}
.stat-icon {
width: 36px;
height: 36px;
border-radius: var(--wa-radius-s);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
margin-bottom: var(--wa-space-s);
}
.stat-icon.success { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
.stat-icon.danger { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
.stat-icon.warning { background: var(--wa-color-warning-subtle); color: var(--wa-color-warning); }
.stat-icon.info { background: var(--wa-color-primary-subtle); color: var(--wa-color-primary); }
.stat-icon.neutral { background: var(--wa-color-neutral-100); color: var(--wa-color-neutral-600); }
.stat-label {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
margin-bottom: var(--wa-space-2xs);
}
.stat-value {
font-size: var(--wa-font-size-l);
font-weight: 600;
}
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--wa-space-xl);
margin-bottom: var(--wa-space-xl);
}
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: 1fr;
}
}
/* Security Info Card */
.security-info {
margin-bottom: var(--wa-space-xl);
}
.credential-display {
background: var(--wa-color-surface-alt);
padding: var(--wa-space-m);
border-radius: var(--wa-radius-m);
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
word-break: break-all;
margin-bottom: var(--wa-space-m);
}
.credential-label {
font-family: var(--wa-font-sans);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
margin-bottom: var(--wa-space-2xs);
display: block;
}
.security-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--wa-space-m);
}
.security-item {
display: flex;
align-items: center;
gap: var(--wa-space-s);
padding: var(--wa-space-s);
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-s);
}
.security-item wa-icon {
font-size: 18px;
color: var(--wa-color-neutral-500);
}
.security-item-label {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
}
.security-item-value {
font-size: var(--wa-font-size-s);
font-weight: 500;
}
/* Activity Timeline */
.activity-list {
list-style: none;
padding: 0;
margin: 0;
}
.activity-item {
display: flex;
gap: var(--wa-space-m);
padding: var(--wa-space-m) 0;
border-bottom: 1px solid var(--wa-color-neutral-100);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.activity-icon.success { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
.activity-icon.danger { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
.activity-icon.warning { background: var(--wa-color-warning-subtle); color: var(--wa-color-warning); }
.activity-icon.info { background: var(--wa-color-primary-subtle); color: var(--wa-color-primary); }
.activity-content {
flex: 1;
min-width: 0;
}
.activity-title {
font-size: var(--wa-font-size-s);
font-weight: 500;
margin-bottom: var(--wa-space-3xs);
}
.activity-meta {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
}
.activity-details {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-600);
margin-top: var(--wa-space-2xs);
}
/* Session List */
.session-list {
list-style: none;
padding: 0;
margin: 0;
}
.session-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--wa-space-m);
padding: var(--wa-space-m);
border-bottom: 1px solid var(--wa-color-neutral-100);
}
.session-item:last-child {
border-bottom: none;
}
.session-item.current {
background: var(--wa-color-primary-subtle);
margin: 0 calc(-1 * var(--wa-space-m));
padding-left: var(--wa-space-m);
padding-right: var(--wa-space-m);
}
.session-info {
flex: 1;
min-width: 0;
}
.session-title {
font-size: var(--wa-font-size-s);
font-weight: 500;
display: flex;
align-items: center;
gap: var(--wa-space-xs);
}
.session-meta {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
margin-top: var(--wa-space-3xs);
}
/* Risk Assessment */
.risk-card {
margin-bottom: var(--wa-space-xl);
}
.risk-header {
display: flex;
align-items: center;
gap: var(--wa-space-xl);
margin-bottom: var(--wa-space-l);
}
.security-score {
text-align: center;
}
.score-gauge {
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--wa-font-size-2xl);
font-weight: 700;
margin-bottom: var(--wa-space-xs);
}
.score-gauge.high { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
.score-gauge.medium { background: var(--wa-color-warning-subtle); color: var(--wa-color-warning); }
.score-gauge.low { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
.score-label {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
}
.risk-summary {
flex: 1;
}
.anomaly-list {
list-style: none;
padding: 0;
margin: 0;
}
.anomaly-item {
display: flex;
align-items: flex-start;
gap: var(--wa-space-s);
padding: var(--wa-space-s);
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-s);
margin-bottom: var(--wa-space-xs);
}
.anomaly-item:last-child {
margin-bottom: 0;
}
.anomaly-item wa-icon {
color: var(--wa-color-warning);
font-size: 16px;
margin-top: 2px;
}
.anomaly-item.resolved wa-icon {
color: var(--wa-color-success);
}
.location-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--wa-space-s);
margin-top: var(--wa-space-m);
}
.location-item {
display: flex;
align-items: center;
gap: var(--wa-space-s);
padding: var(--wa-space-s);
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-s);
font-size: var(--wa-font-size-xs);
}
.location-item wa-icon {
color: var(--wa-color-neutral-500);
}
/* Back link */
.back-link {
display: inline-flex;
align-items: center;
gap: var(--wa-space-xs);
color: var(--wa-color-neutral-500);
text-decoration: none;
font-size: var(--wa-font-size-s);
margin-bottom: var(--wa-space-m);
transition: color 0.15s;
}
.back-link:hover {
color: var(--wa-color-primary);
}
/* Transports badges */
.transports-list {
display: flex;
flex-wrap: wrap;
gap: var(--wa-space-xs);
margin-top: var(--wa-space-s);
}
</style>
</head>
<body>
<wa-page>
<div class="dashboard-layout">
<aside class="sidebar">
<div class="sidebar-header">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="S" style="--size: 32px; background: var(--wa-color-primary);"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Sonr Wallet</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">sonr1x9f...7k2m</span>
</div>
</div>
</div>
<nav>
<ul class="sidebar-nav">
<li>
<a href="accounts.html">
<wa-icon name="wallet"></wa-icon>
Accounts
</a>
</li>
<li>
<a href="transactions.html">
<wa-icon name="arrow-right-arrow-left"></wa-icon>
Transactions
</a>
</li>
<li>
<a href="tokens.html">
<wa-icon name="coins"></wa-icon>
Tokens
</a>
</li>
<li>
<a href="nfts.html">
<wa-icon name="image"></wa-icon>
NFTs
</a>
</li>
<li>
<a href="activity.html">
<wa-icon name="chart-line"></wa-icon>
Activity
</a>
</li>
</ul>
</nav>
<wa-divider style="margin: var(--wa-space-l) 0;"></wa-divider>
<ul class="sidebar-nav">
<li>
<a href="connections.html">
<wa-icon name="plug"></wa-icon>
Connections
</a>
</li>
<li>
<a href="device.html" class="active">
<wa-icon name="mobile"></wa-icon>
Devices
</a>
</li>
<li>
<a href="service.html">
<wa-icon name="server"></wa-icon>
Services
</a>
</li>
<li>
<a href="settings.html">
<wa-icon name="gear"></wa-icon>
Settings
</a>
</li>
</ul>
</aside>
<main class="main-content">
<a href="settings.html?tab=devices" class="back-link">
<wa-icon name="arrow-left"></wa-icon>
Back to Devices
</a>
<!-- Device Hero -->
<div class="device-hero">
<div class="device-identity">
<div class="device-icon" id="device-icon">
<wa-icon name="laptop"></wa-icon>
</div>
<div class="device-name-block">
<div class="wa-cluster wa-gap-s wa-align-items-center">
<span class="wa-heading-2xl" id="device-name">MacBook Pro</span>
<wa-icon-button name="pen" variant="regular" label="Rename device" size="small" id="rename-btn"></wa-icon-button>
</div>
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);" id="device-type">macOS Sequoia · Chrome 120</span>
<div class="device-badges">
<wa-badge variant="success" pill id="status-badge">
<wa-icon name="circle" variant="solid" style="font-size: 6px;"></wa-icon>
Active
</wa-badge>
<wa-badge variant="primary" pill id="trust-badge">
<wa-icon name="shield-check"></wa-icon>
Trusted
</wa-badge>
<wa-badge variant="neutral" pill>
<wa-icon name="fingerprint"></wa-icon>
Touch ID
</wa-badge>
</div>
</div>
</div>
<div class="status-block">
<wa-badge variant="success" size="large">Current Device</wa-badge>
<div class="last-used">
Last used: <strong>Just now</strong>
</div>
<div class="status-details">
<span>Added: <strong>Dec 15, 2024</strong></span>
<span>IP: <strong>192.168.1.42</strong></span>
<span>Location: <strong>San Francisco, CA</strong></span>
</div>
</div>
<div class="hero-actions">
<wa-button variant="neutral" appearance="outlined" id="rename-device-btn">
<wa-icon slot="start" name="pen"></wa-icon>
Rename
</wa-button>
<wa-button variant="neutral" appearance="outlined" id="revoke-sessions-btn">
<wa-icon slot="start" name="arrow-right-from-bracket"></wa-icon>
Sign Out All Sessions
</wa-button>
<div style="flex: 1;"></div>
<wa-button variant="danger" appearance="outlined" id="remove-device-btn">
<wa-icon slot="start" name="trash"></wa-icon>
Remove Device
</wa-button>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-item">
<div class="stat-icon success">
<wa-icon name="check"></wa-icon>
</div>
<div class="stat-label">Successful Logins</div>
<div class="stat-value">47</div>
</div>
<div class="stat-item">
<div class="stat-icon danger">
<wa-icon name="xmark"></wa-icon>
</div>
<div class="stat-label">Failed Attempts</div>
<div class="stat-value">2</div>
</div>
<div class="stat-item">
<div class="stat-icon info">
<wa-icon name="clock"></wa-icon>
</div>
<div class="stat-label">Active Sessions</div>
<div class="stat-value">3</div>
</div>
<div class="stat-item">
<div class="stat-icon warning">
<wa-icon name="calendar"></wa-icon>
</div>
<div class="stat-label">Days Since Added</div>
<div class="stat-value">20</div>
</div>
<div class="stat-item">
<div class="stat-icon neutral">
<wa-icon name="key"></wa-icon>
</div>
<div class="stat-label">Total Sessions</div>
<div class="stat-value">156</div>
</div>
<div class="stat-item">
<div class="stat-icon info">
<wa-icon name="signature"></wa-icon>
</div>
<div class="stat-label">Transactions Signed</div>
<div class="stat-value">89</div>
</div>
</div>
<!-- Security Information -->
<wa-card class="security-info">
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Security Information</span>
<wa-badge variant="success">
<wa-icon name="lock"></wa-icon>
Secure
</wa-badge>
</div>
<div class="credential-display">
<span class="credential-label">Credential ID</span>
<div id="credential-id">YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODkw</div>
</div>
<div class="security-grid">
<div class="security-item">
<wa-icon name="key"></wa-icon>
<div>
<div class="security-item-label">Public Key Algorithm</div>
<div class="security-item-value">ES256 (P-256)</div>
</div>
</div>
<div class="security-item">
<wa-icon name="certificate"></wa-icon>
<div>
<div class="security-item-label">Attestation Type</div>
<div class="security-item-value">Packed (Apple)</div>
</div>
</div>
<div class="security-item">
<wa-icon name="fingerprint"></wa-icon>
<div>
<div class="security-item-label">User Verification</div>
<div class="security-item-value">Required (UV)</div>
</div>
</div>
<div class="security-item">
<wa-icon name="cloud-arrow-up"></wa-icon>
<div>
<div class="security-item-label">Backup Status</div>
<div class="security-item-value">Eligible, Synced</div>
</div>
</div>
<div class="security-item">
<wa-icon name="microchip"></wa-icon>
<div>
<div class="security-item-label">AAGUID</div>
<div class="security-item-value" style="font-family: var(--wa-font-mono); font-size: var(--wa-font-size-xs);">adce0002-35bc...</div>
</div>
</div>
<div class="security-item">
<wa-icon name="calendar-check"></wa-icon>
<div>
<div class="security-item-label">Registered</div>
<div class="security-item-value">Dec 15, 2024</div>
</div>
</div>
</div>
<div class="transports-list">
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500); width: 100%;">Supported Transports:</span>
<wa-badge variant="neutral" pill>
<wa-icon name="laptop"></wa-icon>
Internal
</wa-badge>
<wa-badge variant="neutral" pill>
<wa-icon name="link"></wa-icon>
Hybrid
</wa-badge>
</div>
</wa-card>
<!-- Activity & Sessions Grid -->
<div class="content-grid">
<!-- Activity Timeline -->
<wa-card>
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Recent Activity</span>
<wa-button appearance="plain" size="small" id="refresh-activity-btn">
<wa-icon name="rotate"></wa-icon>
</wa-button>
</div>
<ul class="activity-list" id="activity-timeline">
<li class="activity-item">
<div class="activity-icon success">
<wa-icon name="check"></wa-icon>
</div>
<div class="activity-content">
<div class="activity-title">Successful login</div>
<div class="activity-meta">Just now · 192.168.1.42</div>
<div class="activity-details">San Francisco, CA · Chrome 120</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon info">
<wa-icon name="signature"></wa-icon>
</div>
<div class="activity-content">
<div class="activity-title">Transaction signed</div>
<div class="activity-meta">2 hours ago · 192.168.1.42</div>
<div class="activity-details">Sent 0.5 ETH to 0x7a2b...4f9c</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon success">
<wa-icon name="plug"></wa-icon>
</div>
<div class="activity-content">
<div class="activity-title">App connected</div>
<div class="activity-meta">5 hours ago · 192.168.1.42</div>
<div class="activity-details">Granted access to Uniswap</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon danger">
<wa-icon name="xmark"></wa-icon>
</div>
<div class="activity-content">
<div class="activity-title">Failed login attempt</div>
<div class="activity-meta">Yesterday · 45.67.89.123</div>
<div class="activity-details">Unknown location · User verification failed</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon success">
<wa-icon name="check"></wa-icon>
</div>
<div class="activity-content">
<div class="activity-title">Successful login</div>
<div class="activity-meta">Yesterday · 192.168.1.42</div>
<div class="activity-details">San Francisco, CA · Chrome 120</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon warning">
<wa-icon name="key"></wa-icon>
</div>
<div class="activity-content">
<div class="activity-title">Session refreshed</div>
<div class="activity-meta">2 days ago · 192.168.1.42</div>
<div class="activity-details">Token renewed for 7 days</div>
</div>
</li>
</ul>
<div slot="footer">
<a href="#" class="wa-cluster wa-gap-xs wa-caption-s" style="color: var(--wa-color-primary);">
<span>View all activity</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
<!-- Session History -->
<wa-card>
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Active Sessions</span>
<wa-badge variant="neutral">3 active</wa-badge>
</div>
<ul class="session-list" id="sessions-list">
<li class="session-item current">
<div class="session-info">
<div class="session-title">
<wa-icon name="circle" variant="solid" style="font-size: 8px; color: var(--wa-color-success);"></wa-icon>
Current Session
</div>
<div class="session-meta">Started just now · Expires in 7 days</div>
<div class="session-meta">192.168.1.42 · San Francisco, CA</div>
</div>
</li>
<li class="session-item">
<div class="session-info">
<div class="session-title">
<wa-icon name="circle" variant="solid" style="font-size: 8px; color: var(--wa-color-success);"></wa-icon>
Chrome Extension
</div>
<div class="session-meta">Started 2 days ago · Expires in 5 days</div>
<div class="session-meta">192.168.1.42 · San Francisco, CA</div>
</div>
<wa-button size="small" variant="neutral" appearance="outlined" class="revoke-session-btn">
Revoke
</wa-button>
</li>
<li class="session-item">
<div class="session-info">
<div class="session-title">
<wa-icon name="circle" variant="solid" style="font-size: 8px; color: var(--wa-color-warning);"></wa-icon>
API Access
</div>
<div class="session-meta">Started 5 days ago · Expires in 2 days</div>
<div class="session-meta">192.168.1.42 · San Francisco, CA</div>
</div>
<wa-button size="small" variant="neutral" appearance="outlined" class="revoke-session-btn">
Revoke
</wa-button>
</li>
</ul>
<wa-divider></wa-divider>
<div class="wa-stack wa-gap-s" style="margin-top: var(--wa-space-m);">
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Past Sessions (Last 30 days)</span>
<div class="wa-cluster wa-gap-m" style="font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-500);">
<span><strong>153</strong> sessions</span>
<span><strong>47</strong> logins</span>
<span><strong>89</strong> transactions</span>
</div>
</div>
</wa-card>
</div>
<!-- Risk Assessment -->
<wa-card class="risk-card">
<div slot="header">
<span class="wa-heading-m">Risk Assessment</span>
</div>
<div class="risk-header">
<div class="security-score">
<div class="score-gauge high" id="security-score">92</div>
<div class="score-label">Security Score</div>
</div>
<div class="risk-summary">
<div class="wa-stack wa-gap-xs">
<div class="wa-cluster wa-gap-s wa-align-items-center">
<wa-badge variant="success">Low Risk</wa-badge>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">No critical issues detected</span>
</div>
<ul class="anomaly-list">
<li class="anomaly-item resolved">
<wa-icon name="circle-check"></wa-icon>
<div>
<div class="wa-caption-s" style="font-weight: 500;">Failed login attempt resolved</div>
<div class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Yesterday · Marked as legitimate after successful login</div>
</div>
</li>
<li class="anomaly-item">
<wa-icon name="triangle-exclamation"></wa-icon>
<div>
<div class="wa-caption-s" style="font-weight: 500;">New location detected</div>
<div class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">3 days ago · Oakland, CA (12 miles from usual)</div>
</div>
</li>
</ul>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-stack wa-gap-s" style="margin-top: var(--wa-space-m);">
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Trusted Locations</span>
<div class="location-grid">
<div class="location-item">
<wa-icon name="location-dot"></wa-icon>
<div>
<div class="wa-caption-s" style="font-weight: 500;">San Francisco, CA</div>
<div class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Primary · 45 logins</div>
</div>
</div>
<div class="location-item">
<wa-icon name="location-dot"></wa-icon>
<div>
<div class="wa-caption-s" style="font-weight: 500;">Oakland, CA</div>
<div class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">2 logins</div>
</div>
</div>
<div class="location-item">
<wa-icon name="location-dot"></wa-icon>
<div>
<div class="wa-caption-s" style="font-weight: 500;">Palo Alto, CA</div>
<div class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">1 login</div>
</div>
</div>
</div>
</div>
</wa-card>
</main>
</div>
</wa-page>
<!-- Rename Device Dialog -->
<wa-dialog id="rename-dialog" label="Rename Device">
<form id="rename-form">
<div class="wa-stack wa-gap-m">
<wa-input label="Device Name" name="device-name" value="MacBook Pro" required>
<wa-icon slot="start" name="laptop"></wa-icon>
</wa-input>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
Choose a name that helps you identify this device. This name is only visible to you.
</p>
</div>
</form>
<div slot="footer" class="wa-cluster wa-gap-s wa-justify-content-end">
<wa-button variant="neutral" appearance="outlined" id="rename-cancel-btn">Cancel</wa-button>
<wa-button variant="brand" id="rename-save-btn">Save</wa-button>
</div>
</wa-dialog>
<!-- Remove Device Dialog -->
<wa-dialog id="remove-dialog" label="Remove Device">
<div class="wa-stack wa-gap-m">
<wa-callout variant="danger">
<wa-icon slot="icon" name="triangle-exclamation"></wa-icon>
<strong>This action cannot be undone</strong>
<p style="margin: var(--wa-space-xs) 0 0 0;">
Removing this device will immediately sign out all active sessions and revoke its access to your wallet.
</p>
</wa-callout>
<p class="wa-caption-s">
To use this device again, you'll need to re-register it through the WebAuthn flow. Any pending transactions from this device will be cancelled.
</p>
</div>
<div slot="footer" class="wa-cluster wa-gap-s wa-justify-content-end">
<wa-button variant="neutral" appearance="outlined" id="remove-cancel-btn">Cancel</wa-button>
<wa-button variant="danger" id="remove-confirm-btn">Remove Device</wa-button>
</div>
</wa-dialog>
<script>
// Rename dialog handlers
const renameDialog = document.getElementById('rename-dialog');
const renameBtn = document.getElementById('rename-device-btn');
const renameBtnIcon = document.getElementById('rename-btn');
const renameCancelBtn = document.getElementById('rename-cancel-btn');
const renameSaveBtn = document.getElementById('rename-save-btn');
const deviceNameEl = document.getElementById('device-name');
[renameBtn, renameBtnIcon].forEach(btn => {
btn?.addEventListener('click', () => {
renameDialog.open = true;
});
});
renameCancelBtn?.addEventListener('click', () => {
renameDialog.open = false;
});
renameSaveBtn?.addEventListener('click', () => {
const input = document.querySelector('[name="device-name"]');
if (input?.value) {
deviceNameEl.textContent = input.value;
document.title = `${input.value} - Sonr Motr Wallet`;
renameDialog.open = false;
}
});
// Remove dialog handlers
const removeDialog = document.getElementById('remove-dialog');
const removeBtn = document.getElementById('remove-device-btn');
const removeCancelBtn = document.getElementById('remove-cancel-btn');
const removeConfirmBtn = document.getElementById('remove-confirm-btn');
removeBtn?.addEventListener('click', () => {
removeDialog.open = true;
});
removeCancelBtn?.addEventListener('click', () => {
removeDialog.open = false;
});
removeConfirmBtn?.addEventListener('click', () => {
// In production: hx-delete="/api/devices/{id}" hx-push-url="/settings?tab=devices"
window.location.href = 'settings.html?tab=devices';
});
// Revoke all sessions
const revokeAllBtn = document.getElementById('revoke-sessions-btn');
revokeAllBtn?.addEventListener('click', () => {
if (confirm('Sign out of all sessions on this device? You will remain signed in on your current session.')) {
// In production: hx-delete="/api/devices/{id}/sessions"
console.log('Revoking all sessions...');
}
});
// Individual session revocation
document.querySelectorAll('.revoke-session-btn').forEach(btn => {
btn.addEventListener('click', function() {
const sessionItem = this.closest('.session-item');
if (confirm('End this session?')) {
// In production: hx-delete="/api/sessions/{id}" hx-target="closest .session-item" hx-swap="delete"
sessionItem.style.opacity = '0.5';
setTimeout(() => sessionItem.remove(), 300);
}
});
});
// Refresh activity
const refreshActivityBtn = document.getElementById('refresh-activity-btn');
refreshActivityBtn?.addEventListener('click', function() {
const icon = this.querySelector('wa-icon');
icon.style.animation = 'spin 0.5s linear';
setTimeout(() => {
icon.style.animation = '';
}, 500);
// In production: hx-get="/api/devices/{id}/activity" hx-target="#activity-timeline"
});
// Sidebar navigation
document.querySelectorAll('.sidebar-nav a').forEach(link => {
link.addEventListener('click', function(e) {
if (this.getAttribute('href') === '#') {
e.preventDefault();
}
});
});
// Add spin animation
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
</script>
</body>
</html>