Frontend in @gashboard/web (Vue 3 + Vite + Pinia + TS, ~1.2k LoC):
Auth flow:
- Two signing paths: NIP-07 browser extension (Alby/nos2x/Primal
extension via window.nostr) and NIP-46 remote signer (Primal
app, Amber, nsecbunker via bunker:// URI).
- applesauce-signers lazy-loaded only on bunker login so users
with NIP-07 don't pay the cost.
- NIP-98 event built client-side, posted to /api/auth/login,
JWT persisted in localStorage. Pinia auth store handles
login/logout/state restore on reload.
Dashboard (composes the live /api/datum/stats poll, 5s):
- PoolHero — combined hashrate as the headline number,
block height, subscribed count, accepted/rejected shares.
- LotteryWidget — rotating self-deprecating odds copy
("you're 0.3× as likely to find a block as get hit by
lightning today"). Uses ~720 EH/s as the network-hashrate
constant (TODO: fetch live).
- ShareTicker — SVG sparkline of the last 60 polls.
- MinerCard ×N — nickname (QU4CK/P1XEL/N4N0/M1N1), live
hashrate, last share, lifetime tickets, reject %, status
glow (green hashing / amber stale / red idle), affectionate
roast subtitle per ASIC type.
- BlockCelebration — full-screen overlay with celebration
copy. Dormant for now (Datum's lastBlockFoundAt isn't
surfaced yet); preview via window.gashboardCelebrate().
Cyberpunk theme:
- Pure CSS vars, no Tailwind. Dark bg, neon cyan/magenta
accents, monospace, glow shadows.
- Optional CRT scanlines toggle (persists to localStorage).
- Mobile-aware grid breakpoints.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
61 lines
1.6 KiB
TypeScript
61 lines
1.6 KiB
TypeScript
import { defineStore } from "pinia";
|
|
import { computed, ref } from "vue";
|
|
import * as api from "../services/api";
|
|
import * as signer from "../services/signer";
|
|
|
|
export const useAuthStore = defineStore("auth", () => {
|
|
const stored = api.loadToken();
|
|
const npub = ref<string | null>(stored?.npub ?? null);
|
|
const token = ref<string | null>(stored?.token ?? null);
|
|
const error = ref<string | null>(null);
|
|
const busy = ref(false);
|
|
|
|
const isLoggedIn = computed(() => !!token.value);
|
|
|
|
async function loginExtension(): Promise<void> {
|
|
error.value = null;
|
|
busy.value = true;
|
|
try {
|
|
await signer.loginWithExtension();
|
|
const r = await api.login();
|
|
token.value = r.token;
|
|
npub.value = r.npub;
|
|
api.saveToken(r.token, r.npub);
|
|
} catch (e) {
|
|
signer.clearSigner();
|
|
error.value = e instanceof Error ? e.message : "Login failed";
|
|
throw e;
|
|
} finally {
|
|
busy.value = false;
|
|
}
|
|
}
|
|
|
|
async function loginBunker(uri: string): Promise<void> {
|
|
error.value = null;
|
|
busy.value = true;
|
|
try {
|
|
await signer.loginWithBunker(uri);
|
|
const r = await api.login();
|
|
token.value = r.token;
|
|
npub.value = r.npub;
|
|
api.saveToken(r.token, r.npub);
|
|
} catch (e) {
|
|
signer.clearSigner();
|
|
error.value = e instanceof Error ? e.message : "Login failed";
|
|
throw e;
|
|
} finally {
|
|
busy.value = false;
|
|
}
|
|
}
|
|
|
|
function logout(): void {
|
|
api.clearToken();
|
|
signer.clearSigner();
|
|
token.value = null;
|
|
npub.value = null;
|
|
error.value = null;
|
|
}
|
|
|
|
return { npub, token, error, busy, isLoggedIn, loginExtension, loginBunker, logout };
|
|
});
|