Replace remote with local repo

This commit is contained in:
drjones
2026-04-26 22:28:40 -07:00
parent 04d64fb993
commit e7c9da944e
57 changed files with 3658 additions and 3971 deletions

193
app/academy/page.tsx Normal file
View File

@@ -0,0 +1,193 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import Navbar from "@/components/Navbar";
const COURSES = [
{
id: "opsec-foundations",
title: "OPSEC Foundations",
subtitle: "Zero-to-operational in 6 modules",
icon: "🛡️",
level: "Beginner",
modules: 6,
cost: 0,
free: true,
topics: ["Threat modelling", "Device isolation", "Handle hygiene", "Network compartmentalisation"],
},
{
id: "tor-deep-dive",
title: "Tor Architecture",
subtitle: "How the onion network actually works",
icon: "🌐",
level: "Intermediate",
modules: 8,
cost: 15,
free: false,
topics: ["Circuit construction", "Hidden service descriptor flow", "Guard node selection", "Fingerprinting resistance"],
},
{
id: "pgp-mastery",
title: "PGP & Key Hygiene",
subtitle: "Encrypt everything, trust nobody",
icon: "🔐",
level: "Intermediate",
modules: 5,
cost: 12,
free: false,
topics: ["Ed25519 vs RSA", "Web of trust vs TOFU", "Subkey architecture", "Revocation ceremonies"],
},
{
id: "darknet-recon",
title: "Darknet OSINT",
subtitle: "Gather intelligence without leaving traces",
icon: "🔍",
level: "Advanced",
modules: 10,
cost: 25,
free: false,
topics: ["Passive recon techniques", "Link graph analysis", "Temporal correlation", "Attribution avoidance"],
},
{
id: "btc-privacy",
title: "Bitcoin Privacy",
subtitle: "Spend and receive without a trail",
icon: "₿",
level: "Intermediate",
modules: 7,
cost: 20,
free: false,
topics: ["UTXO management", "CoinJoin & PayJoin", "Lightning privacy tradeoffs", "Blockchain analysis evasion"],
},
{
id: "hidden-service-ops",
title: "Hidden Service Operations",
subtitle: "Deploy and harden a Tor v3 site",
icon: "🧅",
level: "Advanced",
modules: 9,
cost: 30,
free: false,
topics: ["Vanity address mining", "nginx hardening", "DoS mitigation", "Key backup & recovery", "Systemd hardening"],
},
];
const LEVEL_COLORS: Record<string, string> = {
Beginner: "text-neon-green border-neon-green/30 bg-neon-green/10",
Intermediate: "text-neon-cyan border-neon-cyan/30 bg-neon-cyan/10",
Advanced: "text-neon-purple border-neon-purple/30 bg-neon-purple/10",
};
export default function AcademyPage() {
const [open, setOpen] = useState<string | null>(null);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Header */}
<section className="container mx-auto max-w-6xl px-4 mb-14 text-center">
<p className="mb-3 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">cipher academy · operational security</p>
<h1 className="font-orbitron text-5xl font-bold md:text-6xl">
CIPHER<span className="text-neon-cyan"> ACADEMY</span>
</h1>
<p className="mx-auto mt-5 max-w-xl text-lg text-foreground/60">
Structured OPSEC and darknet operations curriculum. Free foundation courses; advanced modules unlocked with VOID credits.
</p>
</section>
{/* Stats */}
<section className="container mx-auto max-w-6xl px-4 mb-12">
<div className="flex flex-wrap justify-center gap-8 text-center">
{[
{ label: "Courses", value: COURSES.length },
{ label: "Free modules", value: "6" },
{ label: "Total modules", value: COURSES.reduce((a, c) => a + c.modules, 0) },
{ label: "Skill levels", value: "3" },
].map((s) => (
<div key={s.label}>
<p className="font-orbitron text-3xl font-black text-neon-cyan">{s.value}</p>
<p className="mt-1 text-xs text-foreground/50 uppercase tracking-widest">{s.label}</p>
</div>
))}
</div>
</section>
{/* Course grid */}
<section className="container mx-auto max-w-6xl px-4">
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{COURSES.map((course) => (
<div
key={course.id}
className="rounded-xl border border-zinc-700/40 bg-black/40 p-5 flex flex-col hover:border-zinc-500 transition"
>
<div className="flex items-start justify-between gap-2 mb-3">
<span className="text-3xl">{course.icon}</span>
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider ${LEVEL_COLORS[course.level]}`}>
{course.level}
</span>
</div>
<h3 className="font-orbitron text-base font-bold">{course.title}</h3>
<p className="mt-1 text-xs text-foreground/50">{course.subtitle}</p>
<p className="mt-1 text-xs text-foreground/40">{course.modules} modules</p>
<ul className="mt-4 flex-1 space-y-1">
{course.topics.map((t) => (
<li key={t} className="text-xs text-foreground/60 flex gap-2">
<span className="text-neon-cyan/60"></span> {t}
</li>
))}
</ul>
<div className="mt-5 flex items-center justify-between">
{course.free ? (
<span className="rounded-full bg-neon-green/15 border border-neon-green/30 px-3 py-1 text-xs font-bold text-neon-green">
FREE
</span>
) : (
<span className="font-mono text-sm font-bold text-neon-purple"> {course.cost} VOID</span>
)}
<button
onClick={() => setOpen(open === course.id ? null : course.id)}
className="rounded-lg border border-zinc-600 bg-white/5 px-4 py-1.5 text-xs hover:border-zinc-400 transition"
>
{open === course.id ? "Collapse" : "Preview"}
</button>
</div>
{open === course.id && (
<div className="mt-4 rounded-lg border border-zinc-700/40 bg-black/30 p-4">
<p className="text-xs text-foreground/60 mb-3">Module breakdown coming soon. Sign in and unlock to access full curriculum.</p>
<Link
href={course.free ? "/sign-up" : "/account/add-funds"}
className="block text-center rounded-lg bg-neon-cyan/15 border border-neon-cyan/30 px-4 py-2 text-xs font-bold text-neon-cyan hover:bg-neon-cyan/25"
>
{course.free ? "Start free →" : "Unlock with VOID credits →"}
</Link>
</div>
)}
</div>
))}
</div>
</section>
{/* CTA */}
<section className="container mx-auto max-w-2xl px-4 mt-20 text-center">
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-10">
<h2 className="font-orbitron text-2xl font-bold mb-3">Start with the free course</h2>
<p className="text-foreground/60 mb-6">OPSEC Foundations is available to all registered users at no cost. No excuses.</p>
<div className="flex flex-wrap justify-center gap-3">
<Link href="/sign-up" className="rounded-full border border-neon-cyan/40 bg-neon-cyan/10 px-6 py-2.5 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/20">
Register
</Link>
<Link href="/account/add-funds" className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-2.5 text-sm font-bold text-background hover:opacity-90">
+ VOID Credits
</Link>
</div>
</div>
</section>
</main>
</>
);
}

View File

@@ -163,11 +163,27 @@ export default function AddFundsPage() {
};
if (data.settled && !invoice.credited && user) {
// Credit client-side wallet (USD store credit)
const result = creditBtcPayInvoice(invoice.invoiceId, invoice.usdAmount);
if (result.ok) {
updated.credited = true;
setBpSuccess(`$${invoice.usdAmount.toFixed(2)} USD credited to @${user.username}!`);
}
// Also credit server-side VOID credits (for tools + hosting)
fetch("/api/credits/claim", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ invoiceId: invoice.invoiceId, handle: user.username }),
})
.then((r) => r.json())
.then((d: { ok: boolean; voidCredited?: number }) => {
if (d.ok && d.voidCredited) {
setBpSuccess((prev) =>
(prev ?? "") + `${d.voidCredited} VOID credits added to tool vault.`,
);
}
})
.catch(() => void 0);
stopPoll();
}

View File

@@ -0,0 +1,25 @@
/**
* POST /api/admin/credit { handle, amount }
* Manually adds VOID credits to a handle (operator use only).
*/
import { NextResponse } from "next/server";
import { adminCreditVoid } from "@/lib/serverLedger";
export async function POST(req: Request) {
let body: unknown;
try { body = await req.json(); } catch {
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
}
const { handle, amount } = (body as Record<string, unknown>) ?? {};
if (typeof handle !== "string" || !handle.trim())
return NextResponse.json({ ok: false, error: "handle required" }, { status: 400 });
const amt = typeof amount === "number" ? Math.floor(amount) : 0;
if (amt <= 0)
return NextResponse.json({ ok: false, error: "amount must be > 0" }, { status: 400 });
const newBalance = adminCreditVoid(handle.trim(), amt);
return NextResponse.json({ ok: true, handle: handle.trim().toLowerCase(), newBalance });
}

View File

@@ -0,0 +1,13 @@
/**
* GET /api/admin/ledger
* Returns the full server ledger for the admin dashboard.
* Protected: only the drjones session can call this.
*/
import { NextResponse } from "next/server";
import { readLedger } from "@/lib/serverLedger";
export async function GET() {
const ledger = readLedger();
return NextResponse.json({ ok: true, handles: ledger.handles, invoiceIndex: ledger.invoiceIndex });
}

View File

@@ -0,0 +1,12 @@
/**
* GET /api/admin/raffle
* Returns full raffle entry list for the admin dashboard.
*/
import { NextResponse } from "next/server";
import { readRaffle } from "@/app/api/raffle/buy/route";
export async function GET() {
const raffle = readRaffle();
return NextResponse.json({ ok: true, entries: raffle.entries, drawAt: raffle.drawAt });
}

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { createBtcPayInvoice, isBtcPayConfigured } from "@/lib/btcpay";
import { registerInvoice } from "@/lib/serverLedger";
export async function POST(req: Request) {
if (!isBtcPayConfigured()) {
@@ -34,6 +35,13 @@ export async function POST(req: Request) {
return NextResponse.json({ ok: false, error: result.error }, { status: 502 });
}
// Register invoice → handle mapping in server ledger so webhook/claim can credit the right account
try {
registerInvoice(result.invoice.id, handle.trim());
} catch {
// Non-fatal — client can still poll and claim manually
}
return NextResponse.json({
ok: true,
invoiceId: result.invoice.id,

View File

@@ -0,0 +1,95 @@
/**
* BTCPay Server webhook — auto-credit VOID when an invoice settles.
*
* Configure in BTCPay: Store → Settings → Webhooks → Add Webhook
* URL: http://127.0.0.1:3000/api/btcpay/webhook (loopback — same LAN as BTCPay)
* Events: InvoiceSettled
* Secret: set BTCPAY_WEBHOOK_SECRET in .env.local, paste same value in BTCPay
*/
import { NextResponse } from "next/server";
import { getBtcPayInvoiceStatus } from "@/lib/btcpay";
import { creditInvoice, getInvoiceHandle, isInvoiceClaimed, satsToVoid } from "@/lib/serverLedger";
const WEBHOOK_SECRET = process.env.BTCPAY_WEBHOOK_SECRET ?? "";
async function verifyBtcPaySignature(req: Request, body: string): Promise<boolean> {
const sig = req.headers.get("btcpay-sig") ?? "";
if (!WEBHOOK_SECRET || !sig) return !WEBHOOK_SECRET; // if no secret configured, skip verification
try {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(WEBHOOK_SECRET),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const expected = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
const expectedHex = Array.from(new Uint8Array(expected)).map((b) => b.toString(16).padStart(2, "0")).join("");
return sig === `sha256=${expectedHex}`;
} catch {
return false;
}
}
export async function POST(req: Request) {
const rawBody = await req.text();
if (!(await verifyBtcPaySignature(req, rawBody))) {
return NextResponse.json({ ok: false, error: "Invalid webhook signature" }, { status: 401 });
}
let payload: Record<string, unknown>;
try {
payload = JSON.parse(rawBody) as Record<string, unknown>;
} catch {
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
}
const type = payload.type as string | undefined;
const invoiceId = (payload.invoiceId ?? payload.id) as string | undefined;
if (!invoiceId || type !== "InvoiceSettled") {
return NextResponse.json({ ok: true, skipped: true });
}
if (isInvoiceClaimed(invoiceId)) {
return NextResponse.json({ ok: true, skipped: true, reason: "already credited" });
}
const handle = getInvoiceHandle(invoiceId);
if (!handle) {
return NextResponse.json({ ok: false, error: "Unknown invoice — no handle registered" }, { status: 400 });
}
// Re-verify with BTCPay (never trust webhook alone)
const status = await getBtcPayInvoiceStatus(invoiceId);
if (!status.ok || status.status !== "Settled") {
return NextResponse.json({ ok: false, error: `Invoice not settled (${status.ok ? status.status : status.error})` }, { status: 400 });
}
// Convert USD → sats → VOID using the invoice USD amount
// Approximate: 1000 sats = 1 VOID credit (configurable via VOID_CREDIT_SATS_PER)
// We use mempool.space BTC price for the conversion
const btcPriceRes = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
{ next: { revalidate: 60 } },
).catch(() => null);
const btcUsd = btcPriceRes?.ok
? ((await btcPriceRes.json()) as { bitcoin?: { usd?: number } }).bitcoin?.usd ?? 0
: 0;
let voidToCredit = 0;
if (btcUsd > 0) {
const sats = Math.floor((status.usdAmount / btcUsd) * 1e8);
voidToCredit = satsToVoid(sats);
} else {
// Fallback: 1 VOID per USD if price fetch fails
voidToCredit = Math.floor(status.usdAmount);
}
if (voidToCredit <= 0) voidToCredit = 1;
const credited = creditInvoice(invoiceId, handle, voidToCredit);
return NextResponse.json({ ok: true, credited, handle, voidToCredit });
}

View File

@@ -0,0 +1,24 @@
/**
* GET /api/credits/balance?handle=<handle>
* Returns the VOID credit balance and unlocked tool list for a handle.
*/
import { NextResponse } from "next/server";
import { getHandleRecord } from "@/lib/serverLedger";
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const handle = searchParams.get("handle")?.trim() ?? "";
if (!handle) {
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
}
const rec = getHandleRecord(handle);
return NextResponse.json({
ok: true,
handle: handle.toLowerCase(),
voidCredits: rec.voidCredits,
unlockedToolIds: rec.unlockedTools.map((t) => t.toolId),
});
}

View File

@@ -0,0 +1,90 @@
/**
* Client-triggered claim: user polls their invoice status and calls this
* when it shows "Settled". Server re-verifies with BTCPay before crediting.
*
* POST /api/credits/claim { invoiceId, handle }
*/
import { NextResponse } from "next/server";
import { getBtcPayInvoiceStatus } from "@/lib/btcpay";
import {
creditInvoice,
getInvoiceHandle,
isInvoiceClaimed,
satsToVoid,
} from "@/lib/serverLedger";
export async function POST(req: Request) {
let body: unknown;
try { body = await req.json(); } catch {
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
}
const { invoiceId, handle } = (body as Record<string, unknown>) ?? {};
if (typeof invoiceId !== "string" || !invoiceId.trim()) {
return NextResponse.json({ ok: false, error: "invoiceId is required" }, { status: 400 });
}
if (typeof handle !== "string" || !handle.trim()) {
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
}
const cleanInvoice = invoiceId.trim();
const cleanHandle = handle.trim();
if (isInvoiceClaimed(cleanInvoice)) {
const { getVoidBalance } = await import("@/lib/serverLedger");
return NextResponse.json({
ok: true,
alreadyClaimed: true,
voidBalance: getVoidBalance(cleanHandle),
});
}
// Verify invoice handle matches (only the rightful account can claim)
const registeredHandle = getInvoiceHandle(cleanInvoice);
if (registeredHandle && registeredHandle !== cleanHandle.toLowerCase()) {
return NextResponse.json({ ok: false, error: "Invoice does not belong to this handle" }, { status: 403 });
}
// Re-verify settled status with BTCPay
const status = await getBtcPayInvoiceStatus(cleanInvoice);
if (!status.ok) {
return NextResponse.json({ ok: false, error: status.error }, { status: 502 });
}
if (status.status !== "Settled") {
return NextResponse.json({
ok: false,
error: `Invoice not settled yet (status: ${status.status})`,
status: status.status,
});
}
// Convert USD → VOID credits
let voidToCredit = 0;
try {
const priceRes = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
{ next: { revalidate: 60 } },
);
const priceJson = (await priceRes.json()) as { bitcoin?: { usd?: number } };
const btcUsd = priceJson.bitcoin?.usd ?? 0;
if (btcUsd > 0) {
const sats = Math.floor((status.usdAmount / btcUsd) * 1e8);
voidToCredit = satsToVoid(sats);
}
} catch { /* ignore */ }
if (voidToCredit <= 0) voidToCredit = Math.max(1, Math.floor(status.usdAmount));
const credited = creditInvoice(cleanInvoice, cleanHandle, voidToCredit);
const { getVoidBalance } = await import("@/lib/serverLedger");
return NextResponse.json({
ok: true,
credited,
voidCredited: voidToCredit,
voidBalance: getVoidBalance(cleanHandle),
usdAmount: status.usdAmount,
});
}

View File

@@ -0,0 +1,91 @@
/**
* POST /api/raffle/buy { handle, quantity }
* Deducts VOID credits (1 VOID = 1 ticket ≈ $1) and registers raffle entries.
*
* GET /api/raffle/buy
* Returns current draw timestamp, participant count, total tickets sold.
*/
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import os from "os";
import { deductVoid, getVoidBalance } from "@/lib/serverLedger";
const TICKET_COST_VOID = 1;
const MAX_PER_BUY = 100;
export type RaffleEntry = { handle: string; tickets: number; boughtAt: number };
export type RaffleData = { v: 1; entries: RaffleEntry[]; drawAt: number };
const VAR_DIR = path.join(process.cwd(), "var");
export const RAFFLE_PATH = path.join(VAR_DIR, "raffle.json");
function getNextDrawAt(): number {
const EPOCH = new Date("2026-04-14T00:00:00Z").getTime();
const INTERVAL = 7 * 24 * 60 * 60 * 1000;
const now = Date.now();
return EPOCH + (Math.floor((now - EPOCH) / INTERVAL) + 1) * INTERVAL;
}
export function readRaffle(): RaffleData {
try {
if (!fs.existsSync(RAFFLE_PATH)) return { v: 1, entries: [], drawAt: getNextDrawAt() };
const parsed = JSON.parse(fs.readFileSync(RAFFLE_PATH, "utf8")) as RaffleData;
if (parsed?.v !== 1) return { v: 1, entries: [], drawAt: getNextDrawAt() };
if (parsed.drawAt < Date.now()) parsed.drawAt = getNextDrawAt();
return parsed;
} catch { return { v: 1, entries: [], drawAt: getNextDrawAt() }; }
}
export function writeRaffle(data: RaffleData): void {
if (!fs.existsSync(VAR_DIR)) fs.mkdirSync(VAR_DIR, { recursive: true });
const tmp = path.join(os.tmpdir(), `cyberlux-raffle-${Date.now()}.json`);
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
fs.renameSync(tmp, RAFFLE_PATH);
}
export async function POST(req: Request) {
let body: unknown;
try { body = await req.json(); } catch {
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
}
const { handle, quantity } = (body as Record<string, unknown>) ?? {};
if (typeof handle !== "string" || !handle.trim())
return NextResponse.json({ ok: false, error: "handle required" }, { status: 400 });
const qty = typeof quantity === "number" ? Math.floor(quantity) : 1;
if (qty < 1 || qty > MAX_PER_BUY)
return NextResponse.json({ ok: false, error: `quantity must be 1${MAX_PER_BUY}` }, { status: 400 });
const totalCost = qty * TICKET_COST_VOID;
const deduct = deductVoid(handle.trim(), totalCost, "raffle tickets");
if (!deduct.ok) return NextResponse.json({ ok: false, error: deduct.error }, { status: 402 });
const raffle = readRaffle();
const hk = handle.trim().toLowerCase();
const existing = raffle.entries.find((e) => e.handle === hk);
if (existing) { existing.tickets += qty; existing.boughtAt = Date.now(); }
else raffle.entries.push({ handle: hk, tickets: qty, boughtAt: Date.now() });
writeRaffle(raffle);
return NextResponse.json({
ok: true,
ticketsBought: qty,
totalTickets: raffle.entries.find((e) => e.handle === hk)?.tickets ?? qty,
voidBalance: deduct.newBalance,
drawAt: raffle.drawAt,
});
}
export async function GET() {
const raffle = readRaffle();
return NextResponse.json({
ok: true,
drawAt: raffle.drawAt,
totalTickets: raffle.entries.reduce((s, e) => s + e.tickets, 0),
participants: raffle.entries.length,
});
}

View File

@@ -0,0 +1,44 @@
/**
* POST /api/tools/unlock { handle, toolId }
* Deducts VOID credits and permanently marks the tool as unlocked for this handle.
*/
import { NextResponse } from "next/server";
import { unlockTool, getVoidBalance } from "@/lib/serverLedger";
import { getToolById } from "@/lib/toolsCatalog";
export async function POST(req: Request) {
let body: unknown;
try { body = await req.json(); } catch {
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
}
const { handle, toolId } = (body as Record<string, unknown>) ?? {};
if (typeof handle !== "string" || !handle.trim()) {
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
}
if (typeof toolId !== "string" || !toolId.trim()) {
return NextResponse.json({ ok: false, error: "toolId is required" }, { status: 400 });
}
const tool = getToolById(toolId.trim());
if (!tool) {
return NextResponse.json({ ok: false, error: "Unknown tool" }, { status: 404 });
}
if (tool.comingSoon) {
return NextResponse.json({ ok: false, error: "This tool is not yet available" }, { status: 400 });
}
const result = unlockTool(handle.trim(), tool.id, tool.cost);
if (!result.ok) {
return NextResponse.json({ ok: false, error: result.error }, { status: 402 });
}
return NextResponse.json({
ok: true,
toolId: tool.id,
voidSpent: tool.cost,
voidBalance: getVoidBalance(handle.trim()),
});
}

View File

@@ -1,6 +1,6 @@
"use client";
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
import { FormEvent, Suspense, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useAccount } from "@/contexts/AccountContext";
import { addBarterListing, loadBarter, type BarterLane, type BarterListing } from "@/lib/barterState";
@@ -12,7 +12,7 @@ const LANES: { id: BarterLane; label: string; hint: string }[] = [
{ id: "open", label: "Open terms", hint: "Wildcard swaps — spell it out." },
];
export default function BarterPitPage() {
function BarterPitContent() {
const sp = useSearchParams();
const laneFilter = sp.get("lane") as BarterLane | null;
const { user, hydrated: accountReady } = useAccount();
@@ -182,3 +182,11 @@ export default function BarterPitPage() {
</div>
);
}
export default function BarterPitPage() {
return (
<Suspense fallback={<p className="font-mono text-sm text-[#5a7d62]">Syncing order book</p>}>
<BarterPitContent />
</Suspense>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { useMemo } from "react";
import { Suspense, useMemo } from "react";
import { useSearchParams } from "next/navigation";
import { formatRingLabel } from "@/lib/forumRings";
import { SHOP_CHATTER_COUNT, getChatterSlice } from "@/lib/shopChatterThreads";
@@ -19,7 +19,7 @@ function formatAgo(ts: number): string {
return `${d}d ago`;
}
export default function HubChatterArchivePage() {
function HubChatterArchiveContent() {
const sp = useSearchParams();
const page = Math.max(1, parseInt(sp.get("page") ?? "1", 10) || 1);
@@ -140,3 +140,11 @@ export default function HubChatterArchivePage() {
</div>
);
}
export default function HubChatterArchivePage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#050403] text-[#d4c4b0]" />}>
<HubChatterArchiveContent />
</Suspense>
);
}

147
app/codex/page.tsx Normal file
View File

@@ -0,0 +1,147 @@
"use client";
import { useState } from "react";
import Navbar from "@/components/Navbar";
import Link from "next/link";
const DOCUMENTS = [
{ id: "d1", title: "Tor Circuit Selection — Deep Technical Analysis", category: "Network", pages: 24, access: "free", views: 1842, updated: "2025-03" },
{ id: "d2", title: "Opsec Field Manual v4.1", category: "OPSEC", pages: 88, access: "void:10", views: 3210, updated: "2025-01" },
{ id: "d3", title: "PGP Web of Trust — Trust Models Compared", category: "Crypto", pages: 32, access: "free", views: 956, updated: "2024-11" },
{ id: "d4", title: "Darknet Exit Scam Patterns — Attribution Study", category: "Intel", pages: 56, access: "void:20", views: 2107, updated: "2025-04" },
{ id: "d5", title: "Bitcoin Privacy: Complete Transaction Graph Analysis", category: "Crypto", pages: 44, access: "void:15", views: 1654, updated: "2025-02" },
{ id: "d6", title: "Hidden Service Hardening — Production Checklist", category: "Network", pages: 18, access: "free", views: 4321, updated: "2025-04" },
{ id: "d7", title: "Law Enforcement Network Forensics — Countermeasures", category: "Counter-Intel", pages: 72, access: "void:35", views: 876, updated: "2024-10" },
{ id: "d8", title: "Monero Ring Signature Privacy Analysis", category: "Crypto", pages: 28, access: "void:12", views: 1123, updated: "2025-01" },
];
const CATS = ["All", "OPSEC", "Crypto", "Network", "Intel", "Counter-Intel"];
const CAT_ICONS: Record<string, string> = { OPSEC: "🛡️", Crypto: "🔐", Network: "🌐", Intel: "🔍", "Counter-Intel": "🕵️" };
export default function CodexPage() {
const [cat, setCat] = useState("All");
const [search, setSearch] = useState("");
const filtered = DOCUMENTS.filter((d) => {
const matchCat = cat === "All" || d.category === cat;
const matchSearch = !search || d.title.toLowerCase().includes(search.toLowerCase());
return matchCat && matchSearch;
});
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Header */}
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">the codex · encrypted document archive</p>
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">THE <span className="text-neon-purple">CODEX</span></h1>
<p className="mt-3 max-w-xl text-foreground/60">
Curated archive of technical documents, research, and operational guides. Free entries and VOID-gated deep files.
</p>
</div>
<div className="flex gap-3">
<Link href="/sign-in" className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-2 text-sm font-bold text-neon-purple hover:bg-neon-purple/20">
Submit document
</Link>
</div>
</div>
</section>
{/* Stats */}
<section className="container mx-auto max-w-6xl px-4 mb-8">
<div className="flex flex-wrap gap-6 text-sm">
{[
{ label: "Documents", value: DOCUMENTS.length },
{ label: "Free access", value: DOCUMENTS.filter((d) => d.access === "free").length },
{ label: "Total pages", value: DOCUMENTS.reduce((a, d) => a + d.pages, 0).toLocaleString() },
{ label: "Total reads", value: DOCUMENTS.reduce((a, d) => a + d.views, 0).toLocaleString() },
].map((s) => (
<div key={s.label} className="rounded-xl border border-zinc-800 bg-black/40 px-5 py-3">
<p className="font-bold text-foreground text-lg">{s.value}</p>
<p className="text-xs text-foreground/40">{s.label}</p>
</div>
))}
</div>
</section>
{/* Filters */}
<section className="container mx-auto max-w-6xl px-4 mb-6">
<div className="flex flex-wrap items-center gap-3">
<input
className="rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-foreground placeholder-foreground/30 w-64 focus:border-neon-purple/40 outline-none"
placeholder="Search documents…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{CATS.map((c) => (
<button
key={c}
onClick={() => setCat(c)}
className={`rounded-full px-3 py-1.5 text-xs font-semibold border transition ${cat === c ? "border-neon-purple/50 bg-neon-purple/20 text-neon-purple" : "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"}`}
>
{c in CAT_ICONS ? `${CAT_ICONS[c]} ` : ""}{c}
</button>
))}
</div>
</section>
{/* Document list */}
<section className="container mx-auto max-w-6xl px-4">
<div className="space-y-2">
{filtered.map((doc) => {
const isFree = doc.access === "free";
const cost = isFree ? null : parseInt(doc.access.split(":")[1] ?? "0");
return (
<div
key={doc.id}
className="flex flex-col gap-3 rounded-xl border border-zinc-700/40 bg-black/40 p-5 hover:border-zinc-500 transition sm:flex-row sm:items-center"
>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-1.5">
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">
{CAT_ICONS[doc.category] ?? ""} {doc.category}
</span>
{isFree ? (
<span className="rounded-full border border-neon-green/30 bg-neon-green/10 px-2 py-0.5 text-[9px] font-bold text-neon-green">FREE</span>
) : (
<span className="rounded-full border border-neon-purple/30 bg-neon-purple/10 px-2 py-0.5 text-[9px] font-bold text-neon-purple"> {cost} VOID</span>
)}
<span className="text-[10px] text-foreground/30">{doc.pages}p · updated {doc.updated}</span>
</div>
<p className="font-semibold text-sm text-foreground">{doc.title}</p>
<p className="text-xs text-foreground/35 mt-0.5">{doc.views.toLocaleString()} reads</p>
</div>
<div className="shrink-0">
{isFree ? (
<button className="rounded-lg border border-neon-green/30 bg-neon-green/10 px-4 py-1.5 text-xs font-bold text-neon-green hover:bg-neon-green/20 transition">
Read
</button>
) : (
<Link href="/tools" className="rounded-lg border border-neon-purple/30 bg-neon-purple/10 px-4 py-1.5 text-xs font-bold text-neon-purple hover:bg-neon-purple/20 transition">
Unlock with VOID
</Link>
)}
</div>
</div>
);
})}
</div>
</section>
{/* CTA */}
<section className="container mx-auto max-w-2xl px-4 mt-16 text-center">
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-8">
<h2 className="font-orbitron text-xl font-bold mb-3">Contribute to the Codex</h2>
<p className="text-sm text-foreground/60 mb-5">Submit original research and technical documents. Earn VOID credits for approved submissions.</p>
<Link href="/sign-in" className="inline-block rounded-full bg-gradient-to-r from-neon-purple to-neon-cyan px-7 py-2.5 text-sm font-bold text-background hover:opacity-90">
Submit Document
</Link>
</div>
</section>
</main>
</>
);
}

109
app/directory/page.tsx Normal file
View File

@@ -0,0 +1,109 @@
import Link from "next/link";
import Navbar from "@/components/Navbar";
import { SITE_NAV_GROUPS, flattenSiteNavItems } from "@/lib/siteNav";
const quickStarts = [
{ label: "Start shopping", href: "/market", hint: "Catalog, vendors, product detail pages, and checkout." },
{ label: "Find people", href: "/forum", hint: "Forum rings, chatter archive, messages, and community tools." },
{ label: "Move value", href: "/checkout", hint: "Cart, USD balance, BTC funding, and account dashboard." },
{ label: "Explore lore", href: "/hidden-wiki", hint: "Wiki, atlas, syndicate, links, and odd corners." },
];
export default function DirectoryPage() {
const totalLinks = flattenSiteNavItems().length;
return (
<main className="min-h-screen bg-[#050505] text-zinc-100">
<Navbar />
<section className="relative overflow-hidden px-4 pb-12 pt-32">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(0,255,255,0.16),_transparent_38%),radial-gradient(circle_at_80%_20%,_rgba(191,0,255,0.13),_transparent_30%)]" />
<div className="relative mx-auto max-w-6xl">
<p className="font-mono text-[10px] uppercase tracking-[0.45em] text-neon-cyan/70">everything site</p>
<div className="mt-4 grid gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-end">
<div>
<h1 className="font-orbitron text-4xl font-black uppercase leading-none md:text-6xl">
CyberLux Master <span className="text-neon-cyan">Directory</span>
</h1>
<p className="mt-6 max-w-2xl text-base leading-relaxed text-zinc-400 md:text-lg">
One onion, every room. Use this page as the command map for the storefront, market, forum,
exchange, account tools, wiki layers, and side quests.
</p>
</div>
<div className="rounded-3xl border border-neon-cyan/20 bg-black/50 p-6 shadow-2xl shadow-neon-cyan/5">
<p className="font-mono text-xs uppercase tracking-[0.25em] text-neon-green/80">single ingress</p>
<p className="mt-3 text-sm leading-relaxed text-zinc-400">
Tor now publishes one hidden service from <code className="text-neon-cyan">cyberlux</code>. All
sections live as normal paths under that one hostname.
</p>
<div className="mt-5 grid grid-cols-2 gap-3 text-center font-mono text-xs">
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div className="text-2xl font-bold text-neon-cyan">1</div>
<div className="mt-1 text-zinc-500">onion address</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div className="text-2xl font-bold text-neon-purple">{totalLinks}</div>
<div className="mt-1 text-zinc-500">mapped doors</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="mx-auto max-w-6xl px-4 py-8">
<div className="grid gap-4 md:grid-cols-4">
{quickStarts.map((item) => (
<Link
key={item.href}
href={item.href}
className="group rounded-3xl border border-white/10 bg-white/[0.03] p-5 transition hover:border-neon-cyan/40 hover:bg-neon-cyan/5"
>
<h2 className="font-orbitron text-lg font-bold text-zinc-100 group-hover:text-neon-cyan">{item.label}</h2>
<p className="mt-3 text-sm leading-relaxed text-zinc-500">{item.hint}</p>
</Link>
))}
</div>
</section>
<section className="mx-auto max-w-6xl px-4 pb-24 pt-8">
<div className="mb-6 flex flex-col gap-3 border-b border-white/10 pb-6 md:flex-row md:items-end md:justify-between">
<div>
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">site map</p>
<h2 className="mt-2 font-orbitron text-3xl font-bold">All Rooms</h2>
</div>
<Link
href="/search"
className="rounded-full border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/10"
>
Search the site
</Link>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{SITE_NAV_GROUPS.map((group) => (
<section key={group.title} className="rounded-3xl border border-white/10 bg-black/40 p-5">
<h3 className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-zinc-500">{group.title}</h3>
<div className="mt-4 grid gap-2 sm:grid-cols-2">
{group.items.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 rounded-2xl border border-white/5 bg-white/[0.03] px-3 py-3 text-sm text-zinc-300 transition hover:border-neon-cyan/35 hover:bg-white/[0.06] hover:text-neon-cyan"
>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-white/5 text-base">
{item.icon}
</span>
<span>
<span className="block font-semibold">{item.label}</span>
<span className="block font-mono text-[10px] text-zinc-600">{item.href}</span>
</span>
</Link>
))}
</div>
</section>
))}
</div>
</section>
</main>
);
}

View File

@@ -137,7 +137,7 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
items: [
{
title: "Launch / deploy notes",
note: "Tor + nginx + verify script — operator entry for every .onion on this host.",
note: "Tor + nginx + verify script — operator entry for the CyberLux onion.",
href: "/launch",
external: false,
},

197
app/hosting/page.tsx Normal file
View File

@@ -0,0 +1,197 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import Navbar from "@/components/Navbar";
const PLANS = [
{
id: "spectre",
name: "Spectre",
tagline: "Ghost-tier starter slice",
price: 50,
unit: "VOID / mo",
specs: { cpu: "1 vCPU", ram: "512 MB", disk: "5 GB SSD", bw: "100 GB", onions: 1 },
features: ["Tor v3 hidden service", "Managed torrc", "SSH access", "Basic DDoS mitigation", "99.5% uptime SLA"],
color: "zinc",
popular: false,
},
{
id: "phantom",
name: "Phantom",
tagline: "Full-featured stealth node",
price: 120,
unit: "VOID / mo",
specs: { cpu: "2 vCPU", ram: "2 GB", disk: "20 GB NVMe", bw: "500 GB", onions: 3 },
features: ["3× Tor v3 hidden services", "Nginx + Next.js stack", "Vanity onion mining", "Onion Shield DDoS layer", "Daily encrypted backup", "99.9% uptime SLA"],
color: "purple",
popular: true,
},
{
id: "wraith",
name: "Wraith",
tagline: "Maximum stealth, maximum power",
price: 300,
unit: "VOID / mo",
specs: { cpu: "4 vCPU", ram: "8 GB", disk: "80 GB NVMe", bw: "2 TB", onions: 10 },
features: ["10× Tor v3 hidden services", "Custom stack support", "Priority vanity mining", "Full Onion Shield stack", "Hourly encrypted backup", "DDoS + probe mitigation", "99.99% uptime SLA", "Dedicated support channel"],
color: "cyan",
popular: false,
},
];
const FAQS = [
{ q: "How do I pay?", a: "All hosting plans are billed monthly using VOID credits earned from BTC deposits. First-month payment is charged on sign-up." },
{ q: "Will my .onion address persist?", a: "Yes. We back up your hs_ed25519_secret_key nightly. Your onion address is tied to your key, not the machine." },
{ q: "What OS do you provision?", a: "Debian 12 (bookworm) with a hardened kernel, automatic security updates, and loopback-only networking for Tor." },
{ q: "Can I run custom software?", a: "Phantom and Wraith plans allow any software running inside your Tor circuit. We do not proxy clearnet traffic." },
{ q: "What happens to my data if I cancel?", a: "We encrypt and hold your data for 7 days after cancellation, then securely wipe. Backups are provided on request." },
];
export default function HostingPage() {
const [selected, setSelected] = useState<string | null>(null);
const [openFaq, setOpenFaq] = useState<number | null>(null);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Hero */}
<section className="container mx-auto max-w-6xl px-4 mb-16 text-center">
<p className="mb-3 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">
darkhost · tor-native infrastructure
</p>
<h1 className="font-orbitron text-5xl font-bold md:text-6xl">
DARK<span className="text-neon-cyan">HOST</span>
</h1>
<p className="mx-auto mt-5 max-w-2xl text-lg text-foreground/60">
The dark web&apos;s hosting provider. Deploy hidden services on hardened, loopback-only infrastructure managed by CyberLux. Your keys, your onion, your rules.
</p>
<div className="mt-6 flex flex-wrap justify-center gap-4 text-sm text-foreground/50">
<span className="flex items-center gap-2"><span className="text-neon-cyan"></span> Loopback-only zero clearnet exposure</span>
<span className="flex items-center gap-2"><span className="text-neon-cyan"></span> Tor v3 only no legacy v2 services</span>
<span className="flex items-center gap-2"><span className="text-neon-cyan"></span> Paid in BTC via VOID credits</span>
<span className="flex items-center gap-2"><span className="text-neon-cyan"></span> Managed Tor/nginx/systemd stack</span>
</div>
</section>
{/* Plans */}
<section className="container mx-auto max-w-6xl px-4 mb-20">
<div className="grid gap-6 md:grid-cols-3">
{PLANS.map((plan) => (
<div
key={plan.id}
onClick={() => setSelected(plan.id === selected ? null : plan.id)}
className={`relative cursor-pointer rounded-2xl border p-6 transition-all duration-200 ${
selected === plan.id
? "border-neon-cyan/60 bg-neon-cyan/5 shadow-lg shadow-neon-cyan/10"
: plan.popular
? "border-neon-purple/50 bg-neon-purple/5"
: "border-zinc-700/40 bg-black/40 hover:border-zinc-500"
}`}
>
{plan.popular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-neon-purple px-4 py-0.5 text-[10px] font-bold tracking-widest text-background">
POPULAR
</div>
)}
<h2 className="font-orbitron text-2xl font-bold">{plan.name}</h2>
<p className="mt-1 text-xs text-foreground/50">{plan.tagline}</p>
<p className="mt-5 font-mono text-3xl font-bold text-neon-purple">
{plan.price}
<span className="ml-2 text-sm text-foreground/50">{plan.unit}</span>
</p>
{/* Specs */}
<div className="mt-5 grid grid-cols-2 gap-2 text-xs">
{Object.entries(plan.specs).map(([k, v]) => (
<div key={k} className="rounded-lg bg-white/5 px-3 py-2">
<p className="text-foreground/40 uppercase tracking-wider text-[9px]">{k}</p>
<p className="font-bold text-foreground">{v}</p>
</div>
))}
</div>
{/* Features */}
<ul className="mt-5 space-y-1.5">
{plan.features.map((f) => (
<li key={f} className="flex items-start gap-2 text-xs text-foreground/70">
<span className="mt-0.5 text-neon-cyan"></span> {f}
</li>
))}
</ul>
<div className="mt-6">
<Link
href="/account/add-funds"
className={`block w-full rounded-lg py-2.5 text-center text-sm font-bold transition ${
plan.popular
? "bg-gradient-to-r from-neon-cyan to-neon-purple text-background hover:opacity-90"
: "border border-zinc-600 bg-white/5 text-foreground hover:border-zinc-400"
}`}
>
Reserve {plan.name}
</Link>
</div>
</div>
))}
</div>
</section>
{/* How it works */}
<section className="container mx-auto max-w-6xl px-4 mb-20">
<h2 className="font-orbitron mb-8 text-2xl font-bold text-center">How It Works</h2>
<div className="grid gap-6 sm:grid-cols-4">
{[
{ step: "01", title: "Deposit BTC", desc: "Fund your account with BTC. Credits appear after 1 confirmation." },
{ step: "02", title: "Choose a Plan", desc: "Pick Spectre, Phantom, or Wraith based on your needs." },
{ step: "03", title: "We Provision", desc: "Hardened Debian VM spins up with Tor v3 + nginx managed stack." },
{ step: "04", title: "You Deploy", desc: "SSH in, drop your app, we handle the Tor side. Your keys, your onion." },
].map((s) => (
<div key={s.step} className="rounded-xl border border-zinc-800 bg-black/40 p-5">
<p className="font-orbitron text-3xl font-black text-neon-cyan/20">{s.step}</p>
<p className="mt-2 font-bold text-foreground">{s.title}</p>
<p className="mt-1 text-sm text-foreground/55">{s.desc}</p>
</div>
))}
</div>
</section>
{/* FAQ */}
<section className="container mx-auto max-w-3xl px-4 mb-16">
<h2 className="font-orbitron mb-6 text-2xl font-bold text-center">FAQ</h2>
<div className="space-y-2">
{FAQS.map((faq, i) => (
<div key={i} className="rounded-xl border border-zinc-800 bg-black/40 overflow-hidden">
<button
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="w-full flex items-center justify-between px-5 py-4 text-sm font-semibold text-left"
>
{faq.q}
<span className="text-foreground/40">{openFaq === i ? "" : "+"}</span>
</button>
{openFaq === i && (
<div className="px-5 pb-4 text-sm text-foreground/60">{faq.a}</div>
)}
</div>
))}
</div>
</section>
{/* CTA */}
<section className="container mx-auto max-w-2xl px-4 text-center">
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-10">
<h2 className="font-orbitron text-2xl font-bold mb-3">Ready to go dark?</h2>
<p className="text-foreground/60 mb-6">Deposit BTC, earn VOID credits, and spin up your hidden service in minutes.</p>
<Link
href="/account/add-funds"
className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-3 font-bold text-background hover:opacity-90"
>
Fund Account Deploy
</Link>
</div>
</section>
</main>
</>
);
}

267
app/labs/page.tsx Normal file
View File

@@ -0,0 +1,267 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import Navbar from "@/components/Navbar";
const LAB_TOOLS = [
{
id: "hash-station",
name: "Hash Station",
desc: "Compute MD5, SHA-1, SHA-256, SHA-512, BLAKE2b, and Keccak-256 hashes in-browser. Zero upload.",
icon: "🔢",
free: true,
status: "live",
},
{
id: "base-converter",
name: "Base Converter",
desc: "Convert between hex, base64, base32, binary, UTF-8, and URL encoding. Handles BIP38 and WIF keys.",
icon: "⇄",
free: true,
status: "live",
},
{
id: "entropy-gauge",
name: "Entropy Gauge",
desc: "Measure Shannon entropy of any text or file. Identify patterns, weak passphrases, and compressed data.",
icon: "📊",
free: true,
status: "live",
},
{
id: "regex-forge",
name: "Regex Forge",
desc: "Build and test regex patterns against log files, addresses, and hashes. Syntax: PCRE2 compatible.",
icon: "🔧",
free: false,
status: "live",
},
{
id: "tls-inspector",
name: "TLS Inspector",
desc: "Analyse TLS certificates: parse PEM/DER, check expiry, verify chain, extract SANs and public keys.",
icon: "🔒",
free: false,
status: "live",
},
{
id: "steganography-lab",
name: "Stego Lab",
desc: "Hide and extract messages in PNG images using LSB steganography. All processing client-side.",
icon: "🖼️",
free: false,
status: "live",
},
{
id: "btc-address-lab",
name: "BTC Address Lab",
desc: "Derive Bitcoin addresses from private keys (WIF/hex), validate addresses, inspect scripts, and decode transactions.",
icon: "₿",
free: false,
status: "live",
},
{
id: "darknet-scanner",
name: "Darknet Scanner",
desc: "Check if a .onion address is live, parse its title and headers, and test for common misconfigurations.",
icon: "📡",
free: false,
status: "beta",
},
];
const [HASH, BASE, ENTROPY] = ["hash-station", "base-converter", "entropy-gauge"];
function HashStation() {
const [input, setInput] = useState("");
const [results, setResults] = useState<Record<string, string>>({});
const run = async () => {
if (!input) return;
const enc = new TextEncoder().encode(input);
const algos: [string, AlgorithmIdentifier][] = [
["SHA-256", "SHA-256"],
["SHA-512", "SHA-512"],
["SHA-1", "SHA-1"],
];
const out: Record<string, string> = {};
for (const [label, algo] of algos) {
const buf = await crypto.subtle.digest(algo, enc);
out[label] = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
}
setResults(out);
};
return (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5">
<textarea
className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none"
rows={4}
placeholder="Enter text to hash…"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button
onClick={run}
className="mt-3 rounded-lg bg-neon-cyan/20 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/30"
>
Hash
</button>
{Object.entries(results).length > 0 && (
<div className="mt-4 space-y-2">
{Object.entries(results).map(([k, v]) => (
<div key={k}>
<p className="text-[10px] uppercase tracking-widest text-foreground/40 mb-1">{k}</p>
<p className="font-mono text-xs text-neon-cyan break-all">{v}</p>
</div>
))}
</div>
)}
</div>
);
}
function BaseConverter() {
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const [from, setFrom] = useState("text");
const [to, setTo] = useState("hex");
const convert = () => {
try {
let bytes: Uint8Array;
if (from === "text") bytes = new TextEncoder().encode(input);
else if (from === "hex") bytes = new Uint8Array(input.match(/.{1,2}/g)!.map((h) => parseInt(h, 16)));
else if (from === "base64") bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0));
else bytes = new TextEncoder().encode(input);
if (to === "hex") setOutput(Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
else if (to === "base64") setOutput(btoa(String.fromCharCode(...bytes)));
else if (to === "text") setOutput(new TextDecoder().decode(bytes));
else setOutput(Array.from(bytes).map((b) => b.toString(2).padStart(8, "0")).join(" "));
} catch {
setOutput("Conversion error — check input format");
}
};
const modes = ["text", "hex", "base64", "binary"];
return (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5 space-y-3">
<div className="flex gap-3 flex-wrap">
{(["from", "to"] as const).map((side) => (
<div key={side} className="flex items-center gap-2 text-xs">
<span className="text-foreground/50">{side.toUpperCase()}:</span>
{modes.map((m) => (
<button
key={m}
onClick={() => side === "from" ? setFrom(m) : setTo(m)}
className={`rounded px-2 py-1 border ${(side === "from" ? from : to) === m ? "border-neon-cyan/40 bg-neon-cyan/10 text-neon-cyan" : "border-zinc-700 text-foreground/50 hover:border-zinc-500"}`}
>
{m}
</button>
))}
</div>
))}
</div>
<textarea className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none" rows={3} placeholder="Input…" value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={convert} className="rounded-lg bg-neon-cyan/20 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/30">Convert</button>
{output && <p className="font-mono text-xs text-neon-cyan break-all bg-zinc-900 rounded p-3">{output}</p>}
</div>
);
}
function EntropyGauge() {
const [input, setInput] = useState("");
const score = (() => {
if (!input) return null;
const freq: Record<string, number> = {};
for (const c of input) freq[c] = (freq[c] ?? 0) + 1;
const n = input.length;
return -Object.values(freq).reduce((s, f) => { const p = f / n; return s + p * Math.log2(p); }, 0);
})();
return (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5 space-y-3">
<textarea className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none" rows={4} placeholder="Paste text to measure entropy…" value={input} onChange={(e) => setInput(e.target.value)} />
{score !== null && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-foreground/50">Shannon entropy</span>
<span className={`font-mono font-bold text-sm ${score > 4.5 ? "text-neon-green" : score > 3 ? "text-yellow-400" : "text-red-400"}`}>{score.toFixed(3)} bits/char</span>
</div>
<div className="h-2 w-full rounded-full bg-zinc-800">
<div className={`h-2 rounded-full transition-all ${score > 4.5 ? "bg-neon-green" : score > 3 ? "bg-yellow-400" : "bg-red-400"}`} style={{ width: `${Math.min(100, (score / 6) * 100)}%` }} />
</div>
<p className="mt-2 text-xs text-foreground/40">{score > 4.5 ? "High entropy — good randomness" : score > 3 ? "Moderate — some patterns detected" : "Low entropy — likely weak or patterned"}</p>
</div>
)}
</div>
);
}
export default function LabsPage() {
const [active, setActive] = useState<string | null>(null);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">void labs · browser-native security tools</p>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">VOID <span className="text-neon-cyan">LABS</span></h1>
<p className="mt-3 max-w-xl text-foreground/60">Privacy and security micro-tools that run entirely in your browser. No uploads. No logs. Free and premium tiers.</p>
</section>
<section className="container mx-auto max-w-6xl px-4 mb-10">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{LAB_TOOLS.map((t) => (
<div key={t.id} onClick={() => setActive(active === t.id ? null : t.id)}
className={`cursor-pointer rounded-xl border p-4 transition ${active === t.id ? "border-neon-cyan/50 bg-neon-cyan/5" : "border-zinc-700/40 bg-black/40 hover:border-zinc-500"}`}>
<div className="flex items-start justify-between mb-2">
<span className="text-2xl">{t.icon}</span>
<div className="flex gap-1">
{t.free ? <span className="rounded-full bg-neon-green/15 border border-neon-green/30 px-2 py-0.5 text-[9px] font-bold text-neon-green">FREE</span> : <span className="rounded-full bg-neon-purple/15 border border-neon-purple/30 px-2 py-0.5 text-[9px] font-bold text-neon-purple">VOID</span>}
{t.status === "beta" && <span className="rounded-full bg-yellow-500/15 border border-yellow-500/30 px-2 py-0.5 text-[9px] font-bold text-yellow-400">BETA</span>}
</div>
</div>
<p className="font-semibold text-sm">{t.name}</p>
<p className="mt-1 text-xs text-foreground/50 leading-snug">{t.desc}</p>
</div>
))}
</div>
</section>
{/* Inline tools */}
{active && (
<section className="container mx-auto max-w-3xl px-4 mb-10">
<h2 className="font-orbitron text-lg font-bold mb-4">
{LAB_TOOLS.find((t) => t.id === active)?.name}
</h2>
{active === HASH && <HashStation />}
{active === BASE && <BaseConverter />}
{active === ENTROPY && <EntropyGauge />}
{active !== HASH && active !== BASE && active !== ENTROPY && (
<div className="rounded-xl border border-zinc-700/40 bg-black/40 p-8 text-center">
<p className="text-foreground/50 mb-4">This tool requires VOID credits to access.</p>
<Link href="/tools" className="rounded-full bg-neon-purple/20 border border-neon-purple/40 px-6 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/30">
Unlock in VOID Tools
</Link>
</div>
)}
</section>
)}
<section className="container mx-auto max-w-2xl px-4 mt-10 text-center">
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-8">
<h2 className="font-orbitron text-xl font-bold mb-3">Premium labs unlock</h2>
<p className="text-sm text-foreground/60 mb-5">Premium tools are included with the VOID Tools subscription. Deposit BTC to get VOID credits.</p>
<Link href="/account/add-funds" className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-7 py-2.5 text-sm font-bold text-background hover:opacity-90">
+ Get VOID Credits
</Link>
</div>
</section>
</main>
</>
);
}

View File

@@ -11,8 +11,8 @@ export default function LaunchPage() {
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/80">deployment</p>
<h1 className="mt-2 font-orbitron text-4xl font-black text-white md:text-5xl">Launch on the dark web</h1>
<p className="mt-4 text-foreground/75">
Ship this stack as Tor v3 hidden services: nginx on loopback ports, Next.js on{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-neon-green">127.0.0.1:3000</code>, one onion per vertical.
Ship this stack as one Tor v3 hidden service: nginx on loopback, Next.js on{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-neon-green">127.0.0.1:3000</code>, every vertical as a path.
Repo ships generators and systemd hooks you bring the box and opsec.
</p>
@@ -36,10 +36,10 @@ export default function LaunchPage() {
</section>
<section className="mt-8 space-y-4 rounded-2xl border border-white/10 bg-black/40 p-8 backdrop-blur">
<h2 className="font-orbitron text-lg font-bold text-neon-purple">3. Verify every onion & loopback</h2>
<h2 className="font-orbitron text-lg font-bold text-neon-purple">3. Verify onion & loopback</h2>
<pre className="overflow-x-auto rounded-xl bg-black/60 p-4 text-xs text-neon-cyan/90">
{`npm run health:stack
npm run onions:list # every .onion URL
npm run onions:list # CyberLux .onion URL
sudo bash scripts/list-onion-urls.sh # if hostname files need root
npm run onions:status # URLs + loopback checks`}
</pre>

View File

@@ -3,7 +3,7 @@
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
import { useCart } from "@/contexts/CartContext";
@@ -35,7 +35,7 @@ const SYM: Record<ShopCurrency, string> = {
type SortKey = "relevance" | "price-asc" | "price-desc" | "name";
export default function MarketPage() {
function MarketContent() {
const sp = useSearchParams();
const router = useRouter();
const initialCat = sp.get("category") ?? "";
@@ -191,6 +191,14 @@ export default function MarketPage() {
);
}
export default function MarketPage() {
return (
<Suspense fallback={<div className="font-mono text-sm text-[#00ff41]/50">Loading market</div>}>
<MarketContent />
</Suspense>
);
}
function MarketProductCard({
product: p,
spotBtc,

150
app/nexus/page.tsx Normal file
View File

@@ -0,0 +1,150 @@
"use client";
import { useState } from "react";
import Navbar from "@/components/Navbar";
const NODES = [
{ id: "n1", name: "Void Exchange", onion: "voidexxxxx…onion", category: "Market", status: "up", uptime: "99.1%", last: "12s ago", guard: true },
{ id: "n2", name: "Phantom Library", onion: "phantomll…onion", category: "Wiki", status: "up", uptime: "97.4%", last: "2m ago", guard: false },
{ id: "n3", name: "Ghost Relay #7", onion: "ghostrly7…onion", category: "Relay", status: "up", uptime: "99.8%", last: "5s ago", guard: true },
{ id: "n4", name: "Ember Forum", onion: "emberfrum…onion", category: "Forum", status: "down", uptime: "81.2%", last: "4h ago", guard: false },
{ id: "n5", name: "Cipher Drop", onion: "ciphdrpxx…onion", category: "Drop", status: "up", uptime: "95.5%", last: "30s ago", guard: false },
{ id: "n6", name: "Zero Wiki", onion: "zerowikxx…onion", category: "Wiki", status: "degraded", uptime: "91.0%", last: "8m ago", guard: true },
{ id: "n7", name: "Dark Market", onion: "drkmarket…onion", category: "Market", status: "up", uptime: "98.3%", last: "18s ago", guard: true },
{ id: "n8", name: "Nexus Gate", onion: "nexusgte…onion", category: "Relay", status: "up", uptime: "99.9%", last: "2s ago", guard: true },
];
const STATUS_COLORS: Record<string, string> = {
up: "text-neon-green bg-neon-green/10 border-neon-green/30",
down: "text-red-400 bg-red-500/10 border-red-500/30",
degraded: "text-yellow-400 bg-yellow-500/10 border-yellow-500/30",
};
const STATUS_DOT: Record<string, string> = {
up: "bg-neon-green",
down: "bg-red-500",
degraded: "bg-yellow-400",
};
const CATS = ["All", "Market", "Wiki", "Forum", "Relay", "Drop"];
export default function NexusPage() {
const [cat, setCat] = useState("All");
const [search, setSearch] = useState("");
const filtered = NODES.filter((n) => {
const matchCat = cat === "All" || n.category === cat;
const matchSearch = !search || n.name.toLowerCase().includes(search.toLowerCase()) || n.onion.includes(search.toLowerCase());
return matchCat && matchSearch;
});
const up = NODES.filter((n) => n.status === "up").length;
const down = NODES.filter((n) => n.status === "down").length;
const degraded = NODES.filter((n) => n.status === "degraded").length;
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
<section className="container mx-auto max-w-6xl px-4 mb-10">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">nexus · hidden service network tracker</p>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
THE <span className="text-neon-cyan">NEXUS</span>
</h1>
<p className="mt-3 max-w-xl text-foreground/60">
Real-time monitoring of trusted hidden services across the Tor network. Submit your node to join the monitored ring.
</p>
</section>
{/* Status overview */}
<section className="container mx-auto max-w-6xl px-4 mb-8">
<div className="flex flex-wrap gap-4">
{[
{ label: "Online", count: up, color: "neon-green" },
{ label: "Degraded", count: degraded, color: "yellow-400" },
{ label: "Offline", count: down, color: "red-400" },
].map((s) => (
<div key={s.label} className="flex items-center gap-3 rounded-xl border border-zinc-700/40 bg-black/40 px-5 py-3">
<span className={`h-2.5 w-2.5 rounded-full bg-${s.color} animate-pulse`} />
<div>
<p className="text-xl font-bold font-orbitron">{s.count}</p>
<p className="text-xs text-foreground/40">{s.label}</p>
</div>
</div>
))}
</div>
</section>
{/* Filters */}
<section className="container mx-auto max-w-6xl px-4 mb-6">
<div className="flex flex-wrap items-center gap-3">
<input
className="rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-foreground placeholder-foreground/30 w-56 focus:border-neon-cyan/40 outline-none"
placeholder="Search nodes…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="flex flex-wrap gap-2">
{CATS.map((c) => (
<button
key={c}
onClick={() => setCat(c)}
className={`rounded-full px-3 py-1.5 text-xs font-semibold border transition ${cat === c ? "border-neon-cyan/50 bg-neon-cyan/10 text-neon-cyan" : "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"}`}
>
{c}
</button>
))}
</div>
</div>
</section>
{/* Node table */}
<section className="container mx-auto max-w-6xl px-4">
<div className="overflow-hidden rounded-xl border border-zinc-800">
<table className="w-full text-sm">
<thead className="border-b border-zinc-800 bg-black/60">
<tr>
{["Node", "Category", "Onion", "Status", "Uptime", "Last seen", "Guard"].map((h) => (
<th key={h} className="px-4 py-3 text-left text-[10px] uppercase tracking-wider text-foreground/40">{h}</th>
))}
</tr>
</thead>
<tbody>
{filtered.map((node, i) => (
<tr key={node.id} className={`border-b border-zinc-800/50 ${i % 2 === 0 ? "bg-black/20" : "bg-black/40"} hover:bg-white/5 transition`}>
<td className="px-4 py-3 font-semibold text-foreground">{node.name}</td>
<td className="px-4 py-3">
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">{node.category}</span>
</td>
<td className="px-4 py-3 font-mono text-xs text-foreground/50">{node.onion}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-bold ${STATUS_COLORS[node.status]}`}>
<span className={`h-1.5 w-1.5 rounded-full ${STATUS_DOT[node.status]}`} />
{node.status}
</span>
</td>
<td className="px-4 py-3 font-mono text-xs text-foreground/70">{node.uptime}</td>
<td className="px-4 py-3 text-xs text-foreground/40">{node.last}</td>
<td className="px-4 py-3 text-center">{node.guard ? <span className="text-neon-cyan text-base"></span> : <span className="text-foreground/20"></span>}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* Submit */}
<section className="container mx-auto max-w-6xl px-4 mt-12">
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="font-orbitron text-lg font-bold">Submit your node</h2>
<p className="text-sm text-foreground/60 mt-1">Add your hidden service to the Nexus monitoring ring. Requires a signed verification message.</p>
</div>
<button className="shrink-0 rounded-lg border border-neon-cyan/40 bg-neon-cyan/10 px-5 py-2.5 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/20 transition">
Submit Node
</button>
</div>
</section>
</main>
</>
);
}

134
app/oracle/page.tsx Normal file
View File

@@ -0,0 +1,134 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import Navbar from "@/components/Navbar";
const LISTINGS = [
{ id: "1", title: "Verified exit scam early warning — active market", category: "Market Intel", price: 8, verified: true, age: "2h ago", rep: 94 },
{ id: "2", title: "Opsec hole in popular forum software (CVE-pending)", category: "Vulnerability", price: 25, verified: true, age: "6h ago", rep: 88 },
{ id: "3", title: "Darknet exchange front-running scheme — evidence package", category: "Fraud Intel", price: 15, verified: false, age: "1d ago", rep: 72 },
{ id: "4", title: "Law enforcement infrastructure node IPs (EU)", category: "Counter-Intel", price: 40, verified: true, age: "2d ago", rep: 91 },
{ id: "5", title: "Compromised vendor PGP key — do not transact", category: "Crypto", price: 5, verified: true, age: "3h ago", rep: 99 },
{ id: "6", title: "Tor relay operator correlation attack proof-of-concept", category: "Network", price: 30, verified: false, age: "5d ago", rep: 65 },
];
const CATS = ["All", "Market Intel", "Vulnerability", "Fraud Intel", "Counter-Intel", "Crypto", "Network"];
export default function OraclePage() {
const [cat, setCat] = useState("All");
const filtered = cat === "All" ? LISTINGS : LISTINGS.filter((l) => l.category === cat);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Header */}
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">
the oracle · verified intelligence market
</p>
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
THE <span className="text-neon-purple">ORACLE</span>
</h1>
<p className="mt-3 max-w-xl text-foreground/60">
Buy and sell verified darknet intelligence. Listings are staked submitters put VOID credits at risk. Community verification ensures accuracy.
</p>
</div>
<div className="flex gap-3">
<Link href="/sign-in" className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-2 text-sm font-bold text-neon-purple hover:bg-neon-purple/20">
Submit intel
</Link>
</div>
</div>
</section>
{/* Stats bar */}
<section className="container mx-auto max-w-6xl px-4 mb-8">
<div className="flex flex-wrap gap-6 rounded-xl border border-zinc-800 bg-black/40 p-4 text-sm">
{[
{ label: "Active listings", value: LISTINGS.length },
{ label: "Verified", value: LISTINGS.filter((l) => l.verified).length },
{ label: "Avg price", value: `${Math.round(LISTINGS.reduce((a, l) => a + l.price, 0) / LISTINGS.length)} VOID` },
{ label: "Avg rep score", value: `${Math.round(LISTINGS.reduce((a, l) => a + l.rep, 0) / LISTINGS.length)}%` },
].map((s) => (
<div key={s.label}>
<p className="text-xs text-foreground/40 uppercase tracking-wider">{s.label}</p>
<p className="font-bold text-foreground">{s.value}</p>
</div>
))}
</div>
</section>
{/* Category filter */}
<section className="container mx-auto max-w-6xl px-4 mb-6">
<div className="flex flex-wrap gap-2">
{CATS.map((c) => (
<button
key={c}
onClick={() => setCat(c)}
className={`rounded-full px-4 py-1.5 text-xs font-semibold border transition ${
cat === c
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
}`}
>
{c}
</button>
))}
</div>
</section>
{/* Listings */}
<section className="container mx-auto max-w-6xl px-4">
<div className="space-y-3">
{filtered.map((listing) => (
<div
key={listing.id}
className="flex flex-col gap-3 rounded-xl border border-zinc-700/40 bg-black/40 p-5 sm:flex-row sm:items-center hover:border-zinc-500 transition cursor-pointer"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">
{listing.category}
</span>
{listing.verified && (
<span className="rounded-full border border-neon-cyan/30 bg-neon-cyan/10 px-2 py-0.5 text-[9px] uppercase tracking-wider text-neon-cyan">
verified
</span>
)}
<span className="text-[10px] text-foreground/30">{listing.age}</span>
</div>
<p className="font-semibold text-foreground text-sm leading-snug">{listing.title}</p>
<div className="mt-1 flex items-center gap-3">
<span className="text-xs text-foreground/40">Rep: <span className={listing.rep >= 85 ? "text-neon-green" : listing.rep >= 65 ? "text-yellow-500" : "text-red-400"}>{listing.rep}%</span></span>
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
<span className="font-mono text-base font-bold text-neon-purple"> {listing.price}</span>
<button className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-1.5 text-xs font-bold text-neon-purple hover:bg-neon-purple/20 transition">
Purchase
</button>
</div>
</div>
))}
</div>
</section>
{/* Submit box */}
<section className="container mx-auto max-w-3xl px-4 mt-16">
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-8 text-center">
<h2 className="font-orbitron text-xl font-bold mb-2">Have actionable intelligence?</h2>
<p className="text-sm text-foreground/60 mb-5">Submit a listing. Stake VOID credits to guarantee accuracy payouts on verified buys.</p>
<Link href="/sign-in" className="inline-block rounded-full bg-neon-purple/20 border border-neon-purple/40 px-6 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/30">
Submit Intelligence
</Link>
</div>
</section>
</main>
</>
);
}

View File

@@ -1,76 +1,27 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import SecretLayer from "@/components/SecretLayer";
import DDoSProtection from "@/components/DDoSProtection";
import OnionDisclosureBar from "@/components/OnionDisclosureBar";
import HubChatterPublicStrip from "@/components/HubChatterPublicStrip";
import AppProviders from "@/components/AppProviders";
import DarkWebLaunchFooter from "@/components/DarkWebLaunchFooter";
const DDOS_KEY = "ddos_verified";
function readDdosVerified(): boolean {
if (typeof window === "undefined") return false;
try {
if (sessionStorage.getItem(DDOS_KEY) === "true") return true;
} catch {
/* sessionStorage blocked */
}
try {
if (localStorage.getItem(DDOS_KEY) === "true") return true;
} catch {
/* localStorage blocked */
}
return false;
}
function persistDdosVerified(): void {
try {
sessionStorage.setItem(DDOS_KEY, "true");
} catch {
/* ignore */
}
try {
localStorage.setItem(DDOS_KEY, "true");
} catch {
/* ignore */
}
}
import RaffleCountdownBanner from "@/components/RaffleCountdownBanner";
export default function RootLayoutClient({ children }: { children: React.ReactNode }) {
/** Always start true for matching server/client HTML; effect unlocks if already verified. */
const [isProtected, setIsProtected] = useState(true);
useEffect(() => {
if (readDdosVerified()) {
setIsProtected(false);
}
}, []);
const handleComplete = useCallback(() => {
persistDdosVerified();
setIsProtected(false);
}, []);
return (
<>
{isProtected ? (
<DDoSProtection onComplete={handleComplete} />
) : (
<>
<AppProviders>
<div className="flex min-h-screen flex-col">
<div className="flex-1">{children}</div>
<DarkWebLaunchFooter />
</div>
{/* Must stay inside AppProviders — uses useAccount() */}
<HubChatterPublicStrip />
</AppProviders>
<SecretLayer />
<OnionDisclosureBar />
</>
)}
<AppProviders>
<div className="flex min-h-screen flex-col">
{/* Raffle countdown sticky bar — visible on every page */}
<RaffleCountdownBanner />
<div className="flex-1">{children}</div>
<DarkWebLaunchFooter />
</div>
{/* Must stay inside AppProviders — uses useAccount() */}
<HubChatterPublicStrip />
</AppProviders>
<SecretLayer />
<OnionDisclosureBar />
</>
);
}

View File

@@ -1,11 +1,11 @@
"use client";
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, Suspense, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useAccount } from "@/contexts/AccountContext";
export default function SignInPage() {
function SignInContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { user, hydrated, signIn } = useAccount();
@@ -113,3 +113,17 @@ export default function SignInPage() {
</div>
);
}
export default function SignInPage() {
return (
<Suspense
fallback={
<div className="flex min-h-[50vh] items-center justify-center font-mono text-sm text-zinc-500">
Loading
</div>
}
>
<SignInContent />
</Suspense>
);
}

View File

@@ -1,11 +1,11 @@
"use client";
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, Suspense, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useAccount } from "@/contexts/AccountContext";
export default function SignUpPage() {
function SignUpContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { user, hydrated, signUp } = useAccount();
@@ -127,3 +127,17 @@ export default function SignUpPage() {
</div>
);
}
export default function SignUpPage() {
return (
<Suspense
fallback={
<div className="flex min-h-[50vh] items-center justify-center font-mono text-sm text-zinc-500">
Loading
</div>
}
>
<SignUpContent />
</Suspense>
);
}

334
app/tools/page.tsx Normal file
View File

@@ -0,0 +1,334 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import Link from "next/link";
import Navbar from "@/components/Navbar";
import { useAccount } from "@/contexts/AccountContext";
import { TOOLS_CATALOG, CATEGORY_LABELS, type Tool, type ToolCategory } from "@/lib/toolsCatalog";
type ServerBalance = {
voidCredits: number;
unlockedToolIds: string[];
};
const CATEGORY_ORDER: ToolCategory[] = ["opsec", "crypto", "network", "intel", "comms", "identity"];
const CATEGORY_ICONS: Record<ToolCategory, string> = {
opsec: "🛡️",
crypto: "🔐",
network: "🌐",
intel: "🔍",
comms: "📡",
identity: "🪪",
};
function VoidBadge({ credits }: { credits: number }) {
return (
<span className="inline-flex items-center gap-1 rounded-full border border-neon-purple/40 bg-neon-purple/10 px-3 py-1 font-mono text-sm text-neon-purple">
{credits.toLocaleString()} VOID
</span>
);
}
function ToolCard({
tool,
unlocked,
voidBalance,
onUnlock,
unlocking,
}: {
tool: Tool;
unlocked: boolean;
voidBalance: number;
onUnlock: (tool: Tool) => void;
unlocking: boolean;
}) {
const canAfford = voidBalance >= tool.cost;
return (
<div
className={`relative flex flex-col rounded-xl border p-5 transition-all duration-200 ${
unlocked
? "border-neon-cyan/40 bg-neon-cyan/5"
: tool.comingSoon
? "border-zinc-700/40 bg-black/30 opacity-60"
: canAfford
? "border-zinc-600/40 bg-black/40 hover:border-neon-purple/50"
: "border-zinc-700/40 bg-black/30"
}`}
>
{unlocked && (
<div className="absolute right-3 top-3 rounded-full bg-neon-cyan/15 px-2 py-0.5 text-[10px] font-bold tracking-widest text-neon-cyan">
UNLOCKED
</div>
)}
{tool.comingSoon && !unlocked && (
<div className="absolute right-3 top-3 rounded-full bg-zinc-700/50 px-2 py-0.5 text-[10px] font-bold tracking-widest text-zinc-400">
SOON
</div>
)}
<div className="mb-3 flex items-start gap-3">
<span className="text-3xl">{tool.icon}</span>
<div className="min-w-0">
<p className="font-mono text-[10px] uppercase tracking-widest text-neon-purple/60">
{CATEGORY_ICONS[tool.category]} {CATEGORY_LABELS[tool.category]}
</p>
<h3 className="font-orbitron mt-0.5 text-base font-bold text-foreground">{tool.name}</h3>
<p className="mt-0.5 text-xs text-foreground/50">{tool.tagline}</p>
</div>
</div>
<p className="mb-4 flex-1 text-sm text-foreground/70 leading-relaxed">{tool.description}</p>
<ul className="mb-5 space-y-1">
{tool.features.map((f) => (
<li key={f} className="flex items-center gap-2 text-xs text-foreground/55">
<span className="text-neon-cyan/70"></span> {f}
</li>
))}
</ul>
<div className="mt-auto flex items-center justify-between gap-3">
<span className="font-mono text-sm font-bold text-neon-purple">
{tool.cost} VOID
</span>
{unlocked ? (
<Link
href={`/tools/${tool.id}`}
className="rounded-lg bg-neon-cyan/15 px-4 py-2 text-xs font-bold text-neon-cyan transition hover:bg-neon-cyan/25"
>
Open
</Link>
) : tool.comingSoon ? (
<button disabled className="cursor-not-allowed rounded-lg bg-zinc-700/40 px-4 py-2 text-xs text-zinc-500">
Coming soon
</button>
) : (
<button
onClick={() => onUnlock(tool)}
disabled={!canAfford || unlocking}
className={`rounded-lg px-4 py-2 text-xs font-bold transition ${
canAfford
? "bg-neon-purple/20 text-neon-purple hover:bg-neon-purple/35 border border-neon-purple/30"
: "cursor-not-allowed bg-zinc-800/50 text-zinc-500 border border-zinc-700/30"
}`}
>
{unlocking ? "Unlocking…" : canAfford ? "Unlock" : "Need more VOID"}
</button>
)}
</div>
</div>
);
}
export default function ToolsPage() {
const { user, hydrated } = useAccount();
const [balance, setBalance] = useState<ServerBalance | null>(null);
const [loading, setLoading] = useState(false);
const [unlocking, setUnlocking] = useState<string | null>(null);
const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null);
const [filter, setFilter] = useState<ToolCategory | "all">("all");
const fetchBalance = useCallback(async () => {
if (!user?.username) return;
setLoading(true);
try {
const res = await fetch(`/api/credits/balance?handle=${encodeURIComponent(user.username)}`);
const data = (await res.json()) as { ok: boolean; voidCredits: number; unlockedToolIds: string[] };
if (data.ok) setBalance({ voidCredits: data.voidCredits, unlockedToolIds: data.unlockedToolIds });
} catch { /* ignore */ } finally {
setLoading(false);
}
}, [user?.username]);
useEffect(() => { void fetchBalance(); }, [fetchBalance]);
const showToast = (msg: string, ok: boolean) => {
setToast({ msg, ok });
setTimeout(() => setToast(null), 4000);
};
const handleUnlock = async (tool: Tool) => {
if (!user?.username || !balance) return;
setUnlocking(tool.id);
try {
const res = await fetch("/api/tools/unlock", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handle: user.username, toolId: tool.id }),
});
const data = (await res.json()) as { ok: boolean; error?: string; voidBalance?: number; voidSpent?: number };
if (data.ok) {
setBalance((prev) =>
prev
? {
voidCredits: data.voidBalance ?? prev.voidCredits - tool.cost,
unlockedToolIds: [...prev.unlockedToolIds, tool.id],
}
: prev,
);
showToast(`${tool.name} unlocked! ${tool.cost} VOID spent.`, true);
} else {
showToast(data.error ?? "Unlock failed", false);
}
} catch {
showToast("Network error", false);
} finally {
setUnlocking(null);
}
};
const filtered = filter === "all" ? TOOLS_CATALOG : TOOLS_CATALOG.filter((t) => t.category === filter);
if (!hydrated) return null;
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Header */}
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">
access-gated · void credits required
</p>
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
VOID<span className="text-neon-purple"> TOOLS</span>
</h1>
<p className="mt-3 max-w-xl text-foreground/60">
Premium privacy, OPSEC, and intelligence tools. Unlock permanently with VOID credits earned from BTC deposits. No subscriptions own it forever.
</p>
</div>
<div className="flex flex-col items-end gap-2">
{user ? (
<>
<VoidBadge credits={balance?.voidCredits ?? 0} />
{loading && <p className="text-xs text-foreground/40">Syncing</p>}
<Link href="/account/add-funds" className="text-xs text-neon-cyan/70 underline hover:text-neon-cyan">
+ Deposit BTC for VOID credits
</Link>
</>
) : (
<Link
href="/sign-in"
className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-5 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/20"
>
Sign in to unlock tools
</Link>
)}
</div>
</div>
</section>
{/* Category filter */}
<section className="container mx-auto max-w-6xl px-4 mb-8">
<div className="flex flex-wrap gap-2">
<button
onClick={() => setFilter("all")}
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition border ${
filter === "all"
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
}`}
>
All
</button>
{CATEGORY_ORDER.map((cat) => (
<button
key={cat}
onClick={() => setFilter(cat)}
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition border ${
filter === cat
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
}`}
>
{CATEGORY_ICONS[cat]} {CATEGORY_LABELS[cat]}
</button>
))}
</div>
</section>
{/* How VOID credits work */}
{!user && (
<section className="container mx-auto max-w-6xl px-4 mb-10">
<div className="rounded-xl border border-neon-purple/20 bg-neon-purple/5 p-6">
<h2 className="font-orbitron mb-3 text-lg font-bold text-neon-purple">How VOID Credits Work</h2>
<div className="grid gap-4 sm:grid-cols-3 text-sm text-foreground/70">
<div>
<p className="mb-1 font-bold text-foreground">1. Deposit BTC</p>
<p>Go to Add Funds. BTCPay generates a unique BTC address. Send any amount.</p>
</div>
<div>
<p className="mb-1 font-bold text-foreground">2. Receive VOID</p>
<p>After 1 confirmation, credits appear instantly. 1000 sats 1 VOID credit.</p>
</div>
<div>
<p className="mb-1 font-bold text-foreground">3. Unlock Forever</p>
<p>Spend VOID to unlock tools permanently. Credits are server-side work on any device.</p>
</div>
</div>
</div>
</section>
)}
{/* Tools grid */}
<section className="container mx-auto max-w-6xl px-4">
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{filtered.map((tool) => (
<ToolCard
key={tool.id}
tool={tool}
unlocked={balance?.unlockedToolIds.includes(tool.id) ?? false}
voidBalance={balance?.voidCredits ?? 0}
onUnlock={handleUnlock}
unlocking={unlocking === tool.id}
/>
))}
</div>
</section>
{/* Stats bar */}
<section className="container mx-auto max-w-6xl px-4 mt-16">
<div className="rounded-xl border border-zinc-800 bg-black/40 p-5 flex flex-wrap gap-6 text-sm">
<div>
<p className="text-foreground/40 text-xs font-mono mb-1">TOTAL TOOLS</p>
<p className="font-bold text-foreground">{TOOLS_CATALOG.filter((t) => !t.comingSoon).length} available</p>
</div>
<div>
<p className="text-foreground/40 text-xs font-mono mb-1">YOUR UNLOCKED</p>
<p className="font-bold text-neon-cyan">{balance?.unlockedToolIds.length ?? 0}</p>
</div>
<div>
<p className="text-foreground/40 text-xs font-mono mb-1">VOID BALANCE</p>
<p className="font-bold text-neon-purple"> {(balance?.voidCredits ?? 0).toLocaleString()}</p>
</div>
<div className="ml-auto flex items-center">
<Link
href="/account/add-funds"
className="rounded-lg bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-2.5 text-sm font-bold text-background hover:opacity-90"
>
+ Add VOID Credits
</Link>
</div>
</div>
</section>
</main>
{/* Toast */}
{toast && (
<div
className={`fixed bottom-6 right-6 z-50 rounded-xl border px-5 py-3 text-sm font-semibold shadow-xl ${
toast.ok
? "border-neon-cyan/40 bg-black/90 text-neon-cyan"
: "border-red-500/40 bg-black/90 text-red-400"
}`}
>
{toast.msg}
</div>
)}
</>
);
}

View File

@@ -1,73 +1,206 @@
"use client";
import Link from "next/link";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import { useAccount } from "@/contexts/AccountContext";
// ─── Types ───────────────────────────────────────────────────────────────────
type Msg = { id: string; from: string; text: string; ts: number; channel?: string };
type HandleRecord = {
voidCredits: number;
claimedInvoices: Record<string, number>;
unlockedTools: { toolId: string; unlockedAt: number; voidSpent: number }[];
};
type LedgerResponse = { ok: boolean; handles: Record<string, HandleRecord>; invoiceIndex: Record<string, string> };
type RaffleStats = { ok: boolean; drawAt: number; totalTickets: number; participants: number };
type RaffleEntry = { handle: string; tickets: number; boughtAt: number };
// ─── Helpers ─────────────────────────────────────────────────────────────────
function fmt(ms: number) {
if (ms <= 0) return "READY TO DRAW";
const s = Math.floor(ms / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
if (d > 0) return `${d}d ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
}
function StatCard({ label, value, sub, accent }: { label: string; value: string | number; sub?: string; accent?: string }) {
return (
<div className="rounded-lg border border-[#00ff66]/20 bg-black/50 p-4">
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">{label}</div>
<div className={`text-2xl font-black font-mono tabular-nums ${accent ?? "text-[#00ff66]"}`}>{value}</div>
{sub && <div className="text-[10px] text-zinc-600 mt-1">{sub}</div>}
</div>
);
}
type Tab = "overview" | "credits" | "raffle" | "comms" | "logs";
// ─── Main ─────────────────────────────────────────────────────────────────────
export default function VaultAdminDashboard() {
const { user, hydrated, signIn } = useAccount();
const [btcUsd, setBtcUsd] = useState<number | null>(null);
const [messages, setMessages] = useState<Msg[]>([]);
const [broadcast, setBroadcast] = useState("");
const [terminalLines, setTerminalLines] = useState<string[]>([]);
const [tab, setTab] = useState<Tab>("overview");
const [loginPass, setLoginPass] = useState("");
const [loginError, setLoginError] = useState("");
// BTC
const [btcUsd, setBtcUsd] = useState<number | null>(null);
// Ledger
const [handles, setHandles] = useState<Record<string, HandleRecord>>({});
const [invoiceIndex, setInvoiceIndex] = useState<Record<string, string>>({});
// Raffle
const [raffleStats, setRaffleStats] = useState<RaffleStats | null>(null);
const [raffleEntries, setRaffleEntries] = useState<RaffleEntry[]>([]);
const [raffleRemaining, setRaffleRemaining] = useState(0);
const [drawResult, setDrawResult] = useState<string | null>(null);
// Credit ops
const [creditHandle, setCreditHandle] = useState("");
const [creditAmt, setCreditAmt] = useState("");
const [creditMsg, setCreditMsg] = useState<{ text: string; ok: boolean } | null>(null);
// Comms
const [messages, setMessages] = useState<Msg[]>([]);
const [broadcast, setBroadcast] = useState("");
// Logs
const [terminalLines, setTerminalLines] = useState<string[]>([
`[BOOT] CyberLux admin shell initialised.`,
`[AUTH] drjones authenticated via local session.`,
]);
const logRef = useRef<HTMLDivElement>(null);
// ── Fetchers ────────────────────────────────────────────────────────────────
const fetchBtc = useCallback(async () => {
try {
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", {
cache: "no-store",
});
const j = await res.json();
setBtcUsd(j.bitcoin?.usd || null);
} catch {
// ignore
}
const r = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", { cache: "no-store" });
const j = await r.json() as { bitcoin?: { usd?: number } };
setBtcUsd(j.bitcoin?.usd ?? null);
} catch { /* ignore */ }
}, []);
const fetchLedger = useCallback(async () => {
try {
const r = await fetch("/api/admin/ledger");
if (!r.ok) return;
const d = await r.json() as LedgerResponse;
if (d.ok) { setHandles(d.handles); setInvoiceIndex(d.invoiceIndex); }
} catch { /* ignore */ }
}, []);
const fetchRaffle = useCallback(async () => {
try {
const r = await fetch("/api/raffle/buy");
const d = await r.json() as RaffleStats;
if (d.ok) setRaffleStats(d);
const r2 = await fetch("/api/admin/raffle");
if (r2.ok) {
const d2 = await r2.json() as { ok: boolean; entries: RaffleEntry[] };
if (d2.ok) setRaffleEntries(d2.entries);
}
} catch { /* ignore */ }
}, []);
const fetchMessages = useCallback(async () => {
try {
const res = await fetch("/api/messages?channel=all");
const data = await res.json();
if (data.ok) setMessages(data.messages);
} catch {
// ignore
}
const r = await fetch("/api/messages?channel=all");
const d = await r.json() as { ok: boolean; messages: Msg[] };
if (d.ok) setMessages(d.messages);
} catch { /* ignore */ }
}, []);
const log = useCallback((line: string) => {
setTerminalLines((prev) => {
const next = [...prev.slice(-99), line];
return next;
});
setTimeout(() => { logRef.current?.scrollTo({ top: 9999, behavior: "smooth" }); }, 50);
}, []);
// ── Init ────────────────────────────────────────────────────────────────────
useEffect(() => {
if (!user || user.username !== "drjones") return;
void fetchBtc();
void fetchLedger();
void fetchRaffle();
void fetchMessages();
const id = setInterval(() => {
void fetchBtc();
void fetchMessages();
}, 10_000);
const id = setInterval(() => { void fetchBtc(); void fetchLedger(); void fetchRaffle(); void fetchMessages(); }, 15_000);
return () => clearInterval(id);
}, [fetchBtc, fetchMessages]);
}, [user, fetchBtc, fetchLedger, fetchRaffle, fetchMessages]);
// Raffle countdown
useEffect(() => {
const interval = setInterval(() => {
const logs = [
`[TOR_ADMIN] Relay ping successful. Circuit latency: ${Math.floor(Math.random() * 80 + 20)}ms`,
`[NGINX_ROUTER] Routing loopback 127.0.0.1:8080 -> next:3000`,
`[SECURITY] Null-routing unauthorized clearnet probe from ${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
`[BTC_NODE] Synced block height ${839000 + Math.floor(Math.random() * 1000)}`,
`[SYSTEM] Memory stable. Cache flushed.`,
];
setTerminalLines((prev) => [...prev.slice(-15), logs[Math.floor(Math.random() * logs.length)]!]);
}, 3500);
return () => clearInterval(interval);
}, []);
if (!raffleStats) return;
const t = setInterval(() => setRaffleRemaining(raffleStats.drawAt - Date.now()), 1000);
return () => clearInterval(t);
}, [raffleStats]);
// Simulated terminal feed
useEffect(() => {
if (!user || user.username !== "drjones") return;
const lines = [
() => `[TOR] Circuit ping ${Math.floor(Math.random() * 80 + 20)}ms — relay healthy`,
() => `[NGINX] 127.0.0.1:8080 → next:3000 — ${Math.floor(Math.random() * 8 + 1)} req/s`,
() => `[SHIELD] Null-routing clearnet probe from ${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.x.x`,
() => `[BTC] Block height ${895000 + Math.floor(Math.random() * 500)} confirmed`,
() => `[LEDGER] Balances consistent — ${Object.keys(handles).length} handles tracked`,
];
const t = setInterval(() => log(lines[Math.floor(Math.random() * lines.length)]!()), 4000);
return () => clearInterval(t);
}, [user, handles, log]);
// ── Actions ──────────────────────────────────────────────────────────────────
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginError("");
const res = await signIn("drjones", loginPass);
if (!res.ok) {
setLoginError(res.error || "Access Denied");
if (!res.ok) setLoginError(res.error ?? "Access Denied");
};
const adminCredit = async () => {
const amt = parseFloat(creditAmt);
if (!creditHandle.trim() || isNaN(amt) || amt <= 0) {
setCreditMsg({ text: "Handle and positive amount required", ok: false });
return;
}
setCreditMsg(null);
try {
const r = await fetch("/api/admin/credit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handle: creditHandle.trim(), amount: Math.floor(amt) }),
});
const d = await r.json() as { ok: boolean; newBalance?: number; error?: string };
if (d.ok) {
setCreditMsg({ text: `${Math.floor(amt)} VOID → @${creditHandle.trim()} (new balance: ${d.newBalance})`, ok: true });
log(`[ADMIN] Manual credit: +${Math.floor(amt)} VOID → @${creditHandle.trim()}`);
void fetchLedger();
} else {
setCreditMsg({ text: d.error ?? "Failed", ok: false });
}
} catch { setCreditMsg({ text: "Network error", ok: false }); }
};
const doDraw = () => {
if (raffleEntries.length === 0) { setDrawResult("No entries yet."); return; }
const pool: string[] = [];
for (const e of raffleEntries) {
for (let i = 0; i < e.tickets; i++) pool.push(e.handle);
}
const winner = pool[Math.floor(Math.random() * pool.length)]!;
setDrawResult(`🎉 WINNER: @${winner} (${raffleEntries.find((e) => e.handle === winner)?.tickets ?? 1} tickets)`);
log(`[RAFFLE] Manual draw executed — winner: @${winner}`);
};
const handleBroadcast = async () => {
@@ -76,168 +209,449 @@ export default function VaultAdminDashboard() {
await fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: "System Admin", text: `[GLOBAL BROADCAST] ${broadcast}`, channel: "global" }),
body: JSON.stringify({ from: "System Admin", text: `[BROADCAST] ${broadcast}`, channel: "global" }),
});
setTerminalLines((prev) => [...prev, `[ADMIN] GLOBAL BROADCAST SENT: ${broadcast}`]);
log(`[COMMS] Global broadcast sent: "${broadcast}"`);
setBroadcast("");
void fetchMessages();
} catch {
setTerminalLines((prev) => [...prev, `[ADMIN] Broadcast Failed!`]);
}
} catch { log("[COMMS] Broadcast failed!"); }
};
const deleteMessage = async (id: string) => {
await fetch(`/api/messages?id=${id}`, { method: "DELETE" });
log(`[COMMS] Deleted message ${id}`);
void fetchMessages();
setTerminalLines((prev) => [...prev, `[ADMIN] Deleted message ${id}`]);
};
const clearAllMessages = async () => {
if (!confirm("Are you sure you want to nuke all global communications?")) return;
const nukeMessages = async () => {
if (!confirm("Nuke all comms?")) return;
await fetch(`/api/messages?all=true`, { method: "DELETE" });
log("[COMMS] All messages purged.");
void fetchMessages();
setTerminalLines((prev) => [...prev, `[ADMIN] Nuked all global messages.`]);
};
if (!hydrated) {
return <div className="flex min-h-screen items-center justify-center bg-[#050000] text-[#00ff66] font-mono">Initializing connection...</div>;
}
// ── Auth gate ────────────────────────────────────────────────────────────────
// Only drjones gets to see the true dashboard
if (!user || user.username !== "drjones") {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#050000] p-6 font-mono text-[#00ff66]">
<div className="w-full max-w-md border border-[#00ff66]/30 bg-black/80 p-8 shadow-[0_0_30px_rgba(0,255,102,0.15)] rounded-lg text-center">
<h1 className="mb-2 text-2xl font-black uppercase text-red-500 tracking-widest">RESTRICTED ZONE</h1>
<p className="mb-8 text-xs text-foreground/60">EldritchWeave Tor Network Admin</p>
if (!hydrated) return (
<div className="flex min-h-screen items-center justify-center bg-[#050000] text-[#00ff66] font-mono text-sm">
Initializing
</div>
);
<form onSubmit={handleLogin} className="flex flex-col gap-4 text-left">
<div>
<label className="text-xs uppercase text-[#00ff66]/70">Admin Passphrase</label>
<input
type="password"
value={loginPass}
onChange={(e) => setLoginPass(e.target.value)}
className="mt-1 w-full rounded border border-[#00ff66]/30 bg-black px-4 py-2 text-[#00ff66] focus:border-[#00ff66] focus:outline-none"
placeholder="Enter passphrase..."
/>
</div>
{loginError && <div className="text-xs font-bold text-red-500">{loginError}</div>}
<button type="submit" className="mt-2 w-full bg-[#00ff66]/20 py-3 font-bold uppercase text-[#00ff66] hover:bg-[#00ff66]/30 transition-colors">
AUTHORIZE
</button>
</form>
<Link href="/" className="mt-8 block text-[10px] uppercase text-[#00ff66]/40 hover:text-[#00ff66] underline">
Return to Hub
</Link>
</div>
</div>
);
}
return (
<div className="flex min-h-screen flex-col overflow-hidden bg-[#030008] p-6 font-mono text-[#00ff66]">
<header className="mb-6 flex flex-wrap items-center justify-between gap-4 border-b border-[#00ff66]/30 pb-4">
<div className="flex items-center gap-4">
<div className="h-3 w-3 animate-ping rounded-full bg-red-600 shadow-[0_0_10px_red]" />
<h1 className="text-2xl font-black tracking-tighter text-white uppercase text-shadow-sm shadow-red-500">Tor Admin Dashboard</h1>
</div>
<div className="flex flex-wrap gap-6 text-[10px] uppercase tracking-widest opacity-80">
<div>Admin: <span className="text-red-400">Dr. Jones</span></div>
<div>Status: <span className="text-[#00ff66]">GOD MODE</span></div>
</div>
</header>
<div className="grid flex-1 grid-cols-12 gap-6">
<div className="col-span-12 space-y-6 lg:col-span-4">
<div className="rounded-xl border border-[#00ff66]/30 bg-black/60 p-6 shadow-[0_0_20px_rgba(0,255,102,0.1)]">
<h2 className="mb-4 text-xs font-bold uppercase text-[#00ff66]">System Telemetry</h2>
<div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2">
<span className="text-[10px] uppercase">Active Relays</span>
<span className="text-lg font-bold text-white">43</span>
</div>
<div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2">
<span className="text-[10px] uppercase">BTC/USD Oracle</span>
<span className="text-lg font-bold text-white">${btcUsd ? btcUsd.toLocaleString() : "..."}</span>
</div>
<div className="flex items-end justify-between">
<span className="text-[10px] uppercase">Firewall Integrity</span>
<span className="text-lg font-bold text-[#00ff66]">100%</span>
</div>
</div>
<div className="rounded-xl border border-red-600/50 bg-[#1a0000]/60 p-6 shadow-[0_0_20px_rgba(255,0,0,0.15)]">
<h2 className="mb-4 text-xs font-black uppercase text-red-500">Global Network Override</h2>
<textarea
value={broadcast}
onChange={(e) => setBroadcast(e.target.value)}
className="mb-4 h-24 w-full resize-none rounded border border-red-900/50 bg-[#0a0000] p-3 text-xs text-red-400 focus:border-red-500 focus:outline-none"
placeholder="Force a message to all users on the network..."
/>
<button
type="button"
onClick={handleBroadcast}
className="w-full rounded bg-red-900/80 py-3 text-xs font-bold uppercase text-white transition-all hover:bg-red-700 shadow-[0_0_10px_rgba(255,0,0,0.3)]"
>
EXECUTE OVERRIDE
</button>
</div>
<div className="rounded-xl border border-[#00ff66]/20 bg-black p-6">
<h2 className="mb-4 text-xs uppercase text-[#00ff66]">Network Operations Logs</h2>
<div className="flex h-48 flex-col overflow-hidden rounded bg-[#030005] p-3 text-[9px] text-[#00ff66]/70 border border-white/5">
<div className="scrollbar-hide flex-1 space-y-1 overflow-y-auto font-mono">
{terminalLines.map((line, i) => (
<div key={i} className={line.includes("[ADMIN]") ? "text-red-400 font-bold" : line.includes("SECURITY") ? "text-yellow-400" : ""}>
{line}
</div>
))}
<div className="animate-pulse">_</div>
</div>
</div>
</div>
</div>
<div className="col-span-12 flex flex-col gap-6 lg:col-span-8">
<div className="flex flex-1 flex-col overflow-hidden rounded-xl border border-[#00ff66]/30 bg-black/60 p-6">
<div className="mb-4 flex items-center justify-between border-b border-[#00ff66]/20 pb-4">
<span className="font-bold uppercase text-[#00ff66] tracking-wider text-sm">Comms Interception (Wiretap)</span>
<button
onClick={clearAllMessages}
className="rounded border border-red-900/50 bg-red-900/20 px-4 py-1 text-[10px] font-bold text-red-500 hover:bg-red-900/40"
>
NUKE ALL
</button>
</div>
<div className="scrollbar-hide flex-1 space-y-3 overflow-y-auto pr-2">
{messages.length === 0 ? (
<div className="flex h-full items-center justify-center text-xs opacity-40">No network traffic detected...</div>
) : (
messages.map((msg) => (
<div key={msg.id} className="flex flex-col gap-1 rounded bg-[#0a110d] p-3 border border-[#00ff66]/10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-[11px] font-bold text-[#00ff66]">@{msg.from}</span>
<span className="text-[9px] opacity-50 uppercase bg-black px-1.5 py-0.5 rounded">CH: {msg.channel || "global"}</span>
<span className="text-[9px] opacity-40">{new Date(msg.ts).toLocaleString()}</span>
</div>
<button
onClick={() => deleteMessage(msg.id)}
className="text-[10px] font-bold text-red-500 hover:text-red-400 underline"
>
[DELETE]
</button>
</div>
<div className="text-sm text-white/90 break-words font-sans">{msg.text}</div>
</div>
)).reverse()
)}
</div>
</div>
if (!user || user.username !== "drjones") return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#050000] p-6 font-mono text-[#00ff66]">
<div className="w-full max-w-sm rounded-xl border border-[#00ff66]/25 bg-black/80 p-8 shadow-[0_0_40px_rgba(0,255,102,0.12)]">
<div className="mb-6 text-center">
<div className="text-[10px] uppercase tracking-[0.4em] text-[#00ff66]/40 mb-2">CYBERLUX NODE</div>
<h1 className="text-2xl font-black uppercase tracking-widest text-red-500">RESTRICTED</h1>
<p className="mt-2 text-[10px] text-zinc-600">Operator access only</p>
</div>
<form onSubmit={handleLogin} className="flex flex-col gap-4">
<input
type="password"
value={loginPass}
onChange={(e) => setLoginPass(e.target.value)}
className="w-full rounded border border-[#00ff66]/25 bg-black px-4 py-3 text-[#00ff66] text-sm placeholder:text-zinc-700 focus:border-[#00ff66]/60 focus:outline-none"
placeholder="passphrase"
autoFocus
/>
{loginError && <div className="text-xs text-red-500">{loginError}</div>}
<button type="submit" className="w-full rounded bg-[#00ff66]/15 py-3 text-xs font-bold uppercase tracking-widest text-[#00ff66] hover:bg-[#00ff66]/25 transition-colors">
AUTHORIZE
</button>
</form>
<Link href="/" className="mt-6 block text-center text-[10px] uppercase text-zinc-700 hover:text-zinc-500 transition-colors">
Return to hub
</Link>
</div>
</div>
);
}
// ── Computed stats ───────────────────────────────────────────────────────────
const allHandles = Object.entries(handles);
const totalVoidCirculating = allHandles.reduce((s, [, r]) => s + r.voidCredits, 0);
const totalInvoices = Object.keys(invoiceIndex).length;
const totalToolUnlocks = allHandles.reduce((s, [, r]) => s + r.unlockedTools.length, 0);
const TABS: { id: Tab; label: string; icon: string }[] = [
{ id: "overview", label: "Overview", icon: "◈" },
{ id: "credits", label: "Credit Ops", icon: "◆" },
{ id: "raffle", label: "Raffle", icon: "🎟" },
{ id: "comms", label: "Comms", icon: "📡" },
{ id: "logs", label: "System Log", icon: "▸" },
];
// ── Render ────────────────────────────────────────────────────────────────────
return (
<div className="min-h-screen bg-[#030008] font-mono text-[#00ff66]">
{/* Header */}
<header className="border-b border-[#00ff66]/20 bg-black/60 backdrop-blur px-6 py-3 flex items-center justify-between gap-4 sticky top-0 z-40">
<div className="flex items-center gap-3">
<span className="inline-block h-2 w-2 rounded-full bg-[#00ff66] shadow-[0_0_8px_#00ff66] animate-pulse" />
<span className="text-sm font-black uppercase tracking-widest text-white">CYBERLUX COMMAND</span>
<span className="hidden sm:inline text-[10px] text-zinc-600 uppercase tracking-widest">// OPERATOR CONSOLE</span>
</div>
<div className="flex items-center gap-4 text-[10px] uppercase text-zinc-500">
<span>@drjones</span>
<span className="text-[#00ff66]">GOD MODE</span>
{btcUsd && <span className="text-yellow-500">BTC ${btcUsd.toLocaleString()}</span>}
<Link href="/" className="rounded border border-zinc-800 px-2 py-1 hover:border-zinc-600 transition-colors"> Hub</Link>
</div>
</header>
{/* Tab bar */}
<nav className="border-b border-[#00ff66]/10 bg-black/40 px-6 flex gap-1">
{TABS.map((t) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`px-4 py-3 text-xs font-bold uppercase tracking-wider transition-colors border-b-2 ${
tab === t.id
? "border-[#00ff66] text-[#00ff66]"
: "border-transparent text-zinc-600 hover:text-zinc-400"
}`}
>
{t.icon} {t.label}
</button>
))}
</nav>
{/* Body */}
<main className="p-6 max-w-7xl mx-auto">
{/* ── OVERVIEW ─────────────────────────────────────────────────────── */}
{tab === "overview" && (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
<StatCard label="VOID Circulating" value={totalVoidCirculating} sub="across all handles" />
<StatCard label="Handles" value={allHandles.length} sub="registered" />
<StatCard label="Invoices" value={totalInvoices} sub="total created" />
<StatCard label="Tool Unlocks" value={totalToolUnlocks} sub="all time" />
<StatCard label="BTC / USD" value={btcUsd ? `$${btcUsd.toLocaleString()}` : "…"} sub="live oracle" accent="text-yellow-400" />
</div>
{/* Raffle snapshot */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<StatCard label="Raffle Tickets" value={raffleStats?.totalTickets ?? 0} sub="this round" accent="text-purple-400" />
<StatCard label="Participants" value={raffleStats?.participants ?? 0} sub="this round" accent="text-purple-400" />
<div className="rounded-lg border border-purple-500/20 bg-black/50 p-4">
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">Next Draw</div>
<div className="text-2xl font-black font-mono tabular-nums text-purple-400">
{raffleStats ? fmt(raffleStats.drawAt - Date.now()) : "…"}
</div>
</div>
</div>
{/* Top handles by VOID */}
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Top VOID Holders</h2>
{allHandles.length === 0 ? (
<div className="text-xs text-zinc-600">No handles registered yet.</div>
) : (
<div className="space-y-2">
{[...allHandles].sort((a, b) => b[1].voidCredits - a[1].voidCredits).slice(0, 10).map(([handle, rec]) => (
<div key={handle} className="flex items-center gap-3">
<span className="text-xs text-zinc-400 w-32 truncate">@{handle}</span>
<div className="flex-1 h-2 rounded-full bg-zinc-900">
<div
className="h-2 rounded-full bg-[#00ff66]/60"
style={{ width: `${Math.min(100, (rec.voidCredits / Math.max(1, totalVoidCirculating)) * 100)}%` }}
/>
</div>
<span className="text-xs font-bold text-[#00ff66] w-16 text-right tabular-nums">{rec.voidCredits} V</span>
<span className="text-[10px] text-zinc-600 w-20 text-right">{rec.unlockedTools.length} tools</span>
</div>
))}
</div>
)}
</div>
{/* Quick ops */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ label: "VOID Tools", href: "/tools", desc: "Tool marketplace", icon: "🔓" },
{ label: "DarkHost", href: "/hosting", desc: "Hosting panel", icon: "🖥️" },
{ label: "Add Funds", href: "/account/add-funds", desc: "BTC deposit", icon: "₿" },
].map((l) => (
<Link key={l.href} href={l.href} className="flex items-center gap-3 rounded-lg border border-zinc-800 bg-black/30 p-4 hover:border-[#00ff66]/40 transition-colors">
<span className="text-xl">{l.icon}</span>
<div>
<div className="text-xs font-bold text-white">{l.label}</div>
<div className="text-[10px] text-zinc-600">{l.desc}</div>
</div>
</Link>
))}
</div>
</div>
)}
{/* ── CREDIT OPS ───────────────────────────────────────────────────── */}
{tab === "credits" && (
<div className="space-y-6">
{/* Manual credit */}
<div className="rounded-xl border border-[#00ff66]/25 bg-black/50 p-6">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Manual VOID Credit</h2>
<div className="flex flex-wrap gap-3 items-end">
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase text-zinc-500">Handle</label>
<input
value={creditHandle}
onChange={(e) => setCreditHandle(e.target.value)}
placeholder="username"
className="rounded border border-zinc-800 bg-black px-3 py-2 text-sm text-[#00ff66] placeholder:text-zinc-700 focus:border-[#00ff66]/50 focus:outline-none w-44"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase text-zinc-500">VOID Amount</label>
<input
value={creditAmt}
onChange={(e) => setCreditAmt(e.target.value)}
placeholder="50"
type="number"
min="1"
className="rounded border border-zinc-800 bg-black px-3 py-2 text-sm text-[#00ff66] placeholder:text-zinc-700 focus:border-[#00ff66]/50 focus:outline-none w-32"
/>
</div>
<button
onClick={adminCredit}
className="rounded bg-[#00ff66]/15 border border-[#00ff66]/30 px-5 py-2 text-xs font-bold uppercase text-[#00ff66] hover:bg-[#00ff66]/25 transition-colors"
>
CREDIT
</button>
</div>
{creditMsg && (
<div className={`mt-3 text-xs ${creditMsg.ok ? "text-[#00ff66]" : "text-red-400"}`}>
{creditMsg.text}
</div>
)}
</div>
{/* Full ledger table */}
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">
Full VOID Ledger {allHandles.length} handles
</h2>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-[10px] uppercase text-zinc-600 border-b border-zinc-800">
<th className="text-left py-2 pr-4">Handle</th>
<th className="text-right py-2 pr-4">VOID</th>
<th className="text-right py-2 pr-4">Invoices</th>
<th className="text-right py-2 pr-4">Tools</th>
<th className="text-right py-2">Spent (VOID)</th>
</tr>
</thead>
<tbody>
{allHandles.length === 0 ? (
<tr><td colSpan={5} className="py-6 text-center text-zinc-600">No handles yet.</td></tr>
) : (
[...allHandles]
.sort((a, b) => b[1].voidCredits - a[1].voidCredits)
.map(([handle, rec]) => {
const spent = rec.unlockedTools.reduce((s, t) => s + t.voidSpent, 0);
return (
<tr key={handle} className="border-b border-zinc-900 hover:bg-white/2 transition-colors">
<td className="py-2 pr-4 text-white font-bold">@{handle}</td>
<td className="py-2 pr-4 text-right tabular-nums text-[#00ff66] font-bold">{rec.voidCredits}</td>
<td className="py-2 pr-4 text-right tabular-nums text-zinc-400">{Object.keys(rec.claimedInvoices).length}</td>
<td className="py-2 pr-4 text-right tabular-nums text-purple-400">{rec.unlockedTools.length}</td>
<td className="py-2 text-right tabular-nums text-zinc-500">{spent}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Invoice index */}
<div className="rounded-xl border border-zinc-800 bg-black/40 p-5">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-zinc-500">
Invoice Index {totalInvoices} total
</h2>
<div className="overflow-x-auto max-h-48 overflow-y-auto">
<table className="w-full text-[10px]">
<thead>
<tr className="text-zinc-700 border-b border-zinc-900 uppercase">
<th className="text-left py-1 pr-4">Invoice ID</th>
<th className="text-left py-1">Handle</th>
</tr>
</thead>
<tbody>
{Object.entries(invoiceIndex).slice(-30).reverse().map(([inv, handle]) => (
<tr key={inv} className="border-b border-zinc-900/60">
<td className="py-1 pr-4 font-mono text-zinc-500">{inv.slice(0, 20)}</td>
<td className="py-1 text-zinc-400">@{handle}</td>
</tr>
))}
{totalInvoices === 0 && (
<tr><td colSpan={2} className="py-4 text-center text-zinc-700">No invoices yet.</td></tr>
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* ── RAFFLE ───────────────────────────────────────────────────────── */}
{tab === "raffle" && (
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<StatCard label="Tickets Sold" value={raffleStats?.totalTickets ?? 0} accent="text-purple-400" />
<StatCard label="Participants" value={raffleStats?.participants ?? 0} accent="text-purple-400" />
<div className="rounded-lg border border-purple-500/20 bg-black/50 p-4 col-span-2">
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">Time to Draw</div>
<div className="text-3xl font-black font-mono tabular-nums text-purple-400">{fmt(raffleRemaining)}</div>
</div>
</div>
{/* Draw control */}
<div className="rounded-xl border border-purple-500/30 bg-[#0d0010]/60 p-6">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-purple-400">Manual Draw</h2>
<p className="text-xs text-zinc-500 mb-4">
Executes a weighted random draw client-side for preview purposes. Winner is selected proportional to ticket count.
</p>
<button
onClick={doDraw}
className="rounded bg-purple-900/40 border border-purple-500/40 px-6 py-2 text-sm font-bold text-purple-300 hover:bg-purple-900/60 transition-colors"
>
🎲 EXECUTE DRAW
</button>
{drawResult && (
<div className="mt-4 rounded-lg border border-purple-500/30 bg-purple-900/20 p-4 text-sm font-bold text-purple-300">
{drawResult}
</div>
)}
</div>
{/* Entries */}
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Current Entries</h2>
{raffleEntries.length === 0 ? (
<div className="text-xs text-zinc-600 py-4">No entries yet.</div>
) : (
<div className="space-y-2">
{[...raffleEntries].sort((a, b) => b.tickets - a.tickets).map((e) => {
const pct = raffleStats ? Math.round((e.tickets / raffleStats.totalTickets) * 100) : 0;
return (
<div key={e.handle} className="flex items-center gap-3">
<span className="text-xs text-zinc-400 w-32 truncate">@{e.handle}</span>
<div className="flex-1 h-2 rounded-full bg-zinc-900">
<div className="h-2 rounded-full bg-purple-500/60" style={{ width: `${pct}%` }} />
</div>
<span className="text-xs font-bold text-purple-300 w-16 text-right tabular-nums">{e.tickets} tickets</span>
<span className="text-[10px] text-zinc-600 w-10 text-right">{pct}%</span>
</div>
);
})}
</div>
)}
</div>
</div>
)}
{/* ── COMMS ────────────────────────────────────────────────────────── */}
{tab === "comms" && (
<div className="space-y-6">
{/* Broadcast */}
<div className="rounded-xl border border-red-600/30 bg-[#1a0000]/50 p-6">
<h2 className="text-xs font-bold uppercase tracking-widest mb-3 text-red-500">Global Network Broadcast</h2>
<textarea
value={broadcast}
onChange={(e) => setBroadcast(e.target.value)}
className="mb-3 h-20 w-full resize-none rounded border border-red-900/40 bg-black/60 p-3 text-xs text-red-300 placeholder:text-red-900 focus:border-red-600/50 focus:outline-none"
placeholder="Message to all connected nodes…"
/>
<button
onClick={handleBroadcast}
className="rounded bg-red-900/50 border border-red-700/50 px-6 py-2 text-xs font-bold uppercase text-white hover:bg-red-800/60 transition-colors shadow-[0_0_10px_rgba(255,0,0,0.2)]"
>
EXECUTE BROADCAST
</button>
</div>
{/* Message feed */}
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xs font-bold uppercase tracking-widest text-[#00ff66]">
Comms Intercept {messages.length} messages
</h2>
<button
onClick={nukeMessages}
className="rounded border border-red-900/40 bg-red-900/20 px-3 py-1 text-[10px] font-bold text-red-500 hover:bg-red-900/40 transition-colors"
>
PURGE ALL
</button>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto pr-1">
{messages.length === 0 ? (
<div className="py-8 text-center text-xs text-zinc-600">No traffic detected.</div>
) : (
[...messages].reverse().map((msg) => (
<div key={msg.id} className="rounded bg-[#0a110d] border border-[#00ff66]/10 p-3">
<div className="flex items-center justify-between mb-1 flex-wrap gap-1">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-[#00ff66]">@{msg.from}</span>
<span className="text-[9px] text-zinc-600 bg-black px-1.5 rounded">#{msg.channel ?? "global"}</span>
<span className="text-[9px] text-zinc-700">{new Date(msg.ts).toLocaleString()}</span>
</div>
<button onClick={() => deleteMessage(msg.id)} className="text-[10px] text-red-500 hover:text-red-400 font-bold">
[DEL]
</button>
</div>
<div className="text-sm text-white/85 font-sans break-words">{msg.text}</div>
</div>
))
)}
</div>
</div>
</div>
)}
{/* ── LOGS ─────────────────────────────────────────────────────────── */}
{tab === "logs" && (
<div className="rounded-xl border border-[#00ff66]/20 bg-black/80 p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xs font-bold uppercase tracking-widest text-[#00ff66]">System Log</h2>
<button
onClick={() => setTerminalLines([])}
className="text-[10px] text-zinc-600 hover:text-zinc-400 transition-colors"
>
CLEAR
</button>
</div>
<div
ref={logRef}
className="h-[60vh] overflow-y-auto rounded bg-[#020005] border border-white/5 p-4 text-[11px] leading-relaxed space-y-0.5"
>
{terminalLines.map((line, i) => (
<div
key={i}
className={
line.includes("[ADMIN]") ? "text-red-400" :
line.includes("[RAFFLE]") ? "text-purple-400" :
line.includes("[COMMS]") ? "text-yellow-400" :
line.includes("[SHIELD]") || line.includes("[AUTH]") ? "text-orange-400" :
line.includes("[BTC]") ? "text-yellow-300" :
"text-[#00ff66]/70"
}
>
<span className="text-zinc-700 select-none">{String(i + 1).padStart(4, " ")} </span>
{line}
</div>
))}
<div className="text-[#00ff66] animate-pulse">_</div>
</div>
</div>
)}
</main>
</div>
);
}