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>
69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
// Two signing paths: NIP-07 browser extension and NIP-46 remote signer
|
|
// (covers Primal app + Amber via bunker:// URI). The active signer is held
|
|
// in module state — there's only one logged-in user at a time.
|
|
|
|
import type { Event as NostrEvent, EventTemplate } from "nostr-tools";
|
|
|
|
export type SignerKind = "extension" | "bunker";
|
|
|
|
export type Signer = {
|
|
kind: SignerKind;
|
|
getPublicKey(): Promise<string>;
|
|
signEvent(template: EventTemplate): Promise<NostrEvent>;
|
|
};
|
|
|
|
let activeSigner: Signer | null = null;
|
|
|
|
export function getActiveSigner(): Signer | null {
|
|
return activeSigner;
|
|
}
|
|
|
|
export function clearSigner(): void {
|
|
activeSigner = null;
|
|
}
|
|
|
|
export async function loginWithExtension(): Promise<string> {
|
|
if (!window.nostr) {
|
|
throw new Error("No NIP-07 extension found. Try Alby, nos2x, or Primal extension.");
|
|
}
|
|
const ext = window.nostr;
|
|
const pubkey = await ext.getPublicKey();
|
|
activeSigner = {
|
|
kind: "extension",
|
|
getPublicKey: async () => pubkey,
|
|
signEvent: async (template: EventTemplate) =>
|
|
(await ext.signEvent({
|
|
kind: template.kind,
|
|
created_at: template.created_at,
|
|
tags: template.tags,
|
|
content: template.content,
|
|
})) as NostrEvent,
|
|
};
|
|
return pubkey;
|
|
}
|
|
|
|
export async function loginWithBunker(bunkerUri: string): Promise<string> {
|
|
// Lazy-load applesauce-signers — only pulled when bunker login is attempted
|
|
// so the dashboard works for users who only use NIP-07.
|
|
const mod = await import("applesauce-signers");
|
|
const Ctor =
|
|
(mod as { NostrConnectSigner?: any }).NostrConnectSigner ??
|
|
((mod as { default?: { NostrConnectSigner?: any } }).default?.NostrConnectSigner);
|
|
if (!Ctor) {
|
|
throw new Error("applesauce-signers does not expose NostrConnectSigner");
|
|
}
|
|
const fromUri = Ctor.fromBunkerURI ?? Ctor.fromURI;
|
|
if (!fromUri) {
|
|
throw new Error("NostrConnectSigner has no fromBunkerURI / fromURI helper");
|
|
}
|
|
const signer = await fromUri.call(Ctor, bunkerUri);
|
|
const pubkey: string = await signer.getPublicKey();
|
|
activeSigner = {
|
|
kind: "bunker",
|
|
getPublicKey: async () => pubkey,
|
|
signEvent: async (template: EventTemplate) =>
|
|
(await signer.signEvent(template)) as NostrEvent,
|
|
};
|
|
return pubkey;
|
|
}
|