feat(web): cyberpunk vue 3 dashboard with primal/amber/extension login
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>
This commit is contained in:
68
apps/web/src/services/signer.ts
Normal file
68
apps/web/src/services/signer.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user