Settings page: themes (Dark, Light, Cyberpunk, Retro) with xterm palettes and hub-synced choice, change password, access tokens, about
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
254e0cdd58
commit
a4f39859b6
@@ -0,0 +1,107 @@
|
||||
import { FormEvent, useCallback, useEffect, useState } from 'react'
|
||||
import { api, Me } from './api'
|
||||
import { applyTheme, currentThemeId, THEMES } from './themes'
|
||||
import { Btn, Loading, toast } from './ui'
|
||||
|
||||
const fmtUptime = (s: number) => { const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m` }
|
||||
|
||||
// Small painted preview of a theme: rail, header, a tile and a bar, in its own colours.
|
||||
function Preview({ vars }: { vars: Record<string, string> }) {
|
||||
const v = (k: string) => vars[k]
|
||||
return (
|
||||
<div className="tprev" style={{ background: v('--bg'), borderColor: v('--border') }}>
|
||||
<div className="tprev-head" style={{ background: v('--bg2'), borderColor: v('--border') }}><span style={{ color: v('--accent') }}>Ham Lab</span><span className="tprev-dot" style={{ background: v('--ok') }} /></div>
|
||||
<div className="tprev-body">
|
||||
<div className="tprev-rail" style={{ background: v('--bg2'), borderColor: v('--border') }}>
|
||||
<span style={{ background: v('--bg3'), boxShadow: `inset 2px 0 0 ${v('--accent')}` }} /><span /><span />
|
||||
</div>
|
||||
<div className="tprev-main">
|
||||
<div className="tprev-tile" style={{ background: v('--bg2'), borderColor: v('--border') }}>
|
||||
<i style={{ color: v('--muted') }}>CPU</i><b style={{ color: v('--fg') }}>12%</b>
|
||||
<div className="tprev-bar" style={{ background: v('--track') }}><div style={{ width: '40%', background: v('--s1') }} /></div>
|
||||
</div>
|
||||
<div className="tprev-term" style={{ background: v('--term-bg'), color: v('--accent2') }}>$ _</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Settings({ me }: { me: Me }) {
|
||||
const [theme, setTheme] = useState(currentThemeId())
|
||||
const [tokens, setTokens] = useState<{ name: string; created: string }[] | null>(null)
|
||||
const [cur, setCur] = useState('')
|
||||
const [pw1, setPw1] = useState('')
|
||||
const [pw2, setPw2] = useState('')
|
||||
const [pwErr, setPwErr] = useState('')
|
||||
|
||||
const loadTokens = useCallback(async () => { try { setTokens(await api.tokens()) } catch { setTokens([]) } }, [])
|
||||
useEffect(() => { loadTokens() }, [loadTokens])
|
||||
|
||||
const pick = async (id: string) => {
|
||||
applyTheme(id); setTheme(id)
|
||||
try { await api.saveUIPrefs({ theme: id }) } catch { /* local still applied */ }
|
||||
}
|
||||
|
||||
async function changePassword(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
setPwErr('')
|
||||
if (pw1 !== pw2) { setPwErr('New passwords differ.'); return }
|
||||
if (pw1.length < 12) { setPwErr('At least 12 characters.'); return }
|
||||
try { await api.changePassword(cur, pw1); toast('password changed'); setCur(''); setPw1(''); setPw2('') }
|
||||
catch (err) { setPwErr(err instanceof Error ? err.message : 'failed') }
|
||||
}
|
||||
async function revoke(name: string) {
|
||||
if (!window.confirm(`Revoke the token "${name}"? The hub holding it loses access to this host until it re-adds it.`)) return
|
||||
try { await api.revokeToken(name); toast(`${name} revoked`); loadTokens() } catch (err) { toast(err instanceof Error ? err.message : 'failed', 'err') }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dash settings">
|
||||
<div className="dash-bar"><h2 className="section">Appearance</h2><span className="spacer" /><span className="muted small">follows you to every device you sign in on</span></div>
|
||||
<div className="themes">
|
||||
{THEMES.map((t) => (
|
||||
<button key={t.id} className={'theme-card' + (theme === t.id ? ' on' : '')} onClick={() => pick(t.id)}>
|
||||
<Preview vars={t.vars} />
|
||||
<div className="theme-name">{t.name}{theme === t.id && <span className="badge">active</span>}</div>
|
||||
<div className="muted small">{t.blurb}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="dash-bar"><h2 className="section">Account · {me.host}</h2></div>
|
||||
<form className="card addform" onSubmit={changePassword}>
|
||||
<label>Current password <input type="password" value={cur} onChange={(e) => setCur(e.target.value)} autoComplete="current-password" required /></label>
|
||||
<label>New password <input type="password" value={pw1} onChange={(e) => setPw1(e.target.value)} autoComplete="new-password" required /></label>
|
||||
<label>New password again <input type="password" value={pw2} onChange={(e) => setPw2(e.target.value)} autoComplete="new-password" required /></label>
|
||||
{pwErr && <div className="error">{pwErr}</div>}
|
||||
<div className="svc-actions"><button className="btn on" disabled={!cur || !pw1 || !pw2}>Change password</button><span className="muted small">Changes the login for this host only. Other machines keep their own password. Existing sessions stay signed in.</span></div>
|
||||
</form>
|
||||
|
||||
<div className="dash-bar"><h2 className="section">Access tokens</h2></div>
|
||||
<section className="card">
|
||||
<div className="muted small" style={{ marginBottom: 8 }}>Tokens let another Ham Lab (a hub) manage this host. They're issued when a hub adds this machine and revoked when it removes it.</div>
|
||||
{tokens === null && <Loading label="Loading…" />}
|
||||
{tokens && tokens.length === 0 && <div className="empty">No tokens. No hub manages this host.</div>}
|
||||
{tokens && tokens.map((t) => (
|
||||
<div className="row" key={t.name}>
|
||||
<span className="k mono">{t.name}</span>
|
||||
<span className="muted small">issued {new Date(t.created).toLocaleString()}</span>
|
||||
<Btn className="btn danger" onClick={() => revoke(t.name)}>Revoke</Btn>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<div className="dash-bar"><h2 className="section">About</h2></div>
|
||||
<section className="card">
|
||||
<div className="props">
|
||||
<div><span className="k">Version</span><span className="mono">{me.version}</span></div>
|
||||
<div><span className="k">Host</span><span>{me.host} · {me.arch}</span></div>
|
||||
<div><span className="k">Runs as</span><span>{me.user}</span></div>
|
||||
<div><span className="k">Uptime</span><span>{fmtUptime(me.uptime)}</span></div>
|
||||
{me.fingerprint && <div><span className="k">Certificate</span><span className="mono small">{me.fingerprint}</span></div>}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user