1311 lines
42 KiB
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>
|