feat(radar): recognize supporters and add Intel UI

This commit is contained in:
diegosouzapw
2026-08-09 07:07:05 -03:00
parent d468ff4153
commit 34115cbf33
9 changed files with 431 additions and 9 deletions

View File

@@ -0,0 +1,197 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { useTranslations } from "next-intl";
import type { RadarIntelFeed } from "@/lib/radar/intelFeedSchema";
import { Card } from "@/shared/components";
interface IntelMeta {
version: string;
tier: "live";
fetchedAt: string;
supporterVerified: true;
}
export default function RadarIntelPage() {
const t = useTranslations("radarIntelPage");
const [intel, setIntel] = useState<RadarIntelFeed | null>(null);
const [meta, setMeta] = useState<IntelMeta | null>(null);
const [loading, setLoading] = useState(true);
const [syncing, setSyncing] = useState(false);
const [flagOff, setFlagOff] = useState(false);
const [error, setError] = useState("");
const load = useCallback(async () => {
const response = await fetch("/api/radar/intel");
if (response.status === 404) {
setFlagOff(true);
return;
}
if (!response.ok) throw new Error("intel_load_failed");
const body = (await response.json()) as {
intel?: RadarIntelFeed | null;
meta?: IntelMeta | null;
};
setIntel(body.intel ?? null);
setMeta(body.meta ?? null);
}, []);
const sync = useCallback(async () => {
setSyncing(true);
setError("");
try {
const response = await fetch("/api/radar/intel/sync", { method: "POST" });
if (response.status === 404) {
setFlagOff(true);
return;
}
if (!response.ok) throw new Error("intel_sync_failed");
const status = (await response.json()) as { status?: string };
if (
["error", "invalid_signature", "invalid_schema", "wrong_tier", "too_large"].includes(
status.status ?? ""
)
) {
setError(t("loadFailed"));
}
await load();
} catch {
setError(t("loadFailed"));
await load().catch(() => undefined);
} finally {
setSyncing(false);
}
}, [load, t]);
useEffect(() => {
load()
.catch(() => setError(t("loadFailed")))
.finally(() => setLoading(false));
}, [load, t]);
if (flagOff) notFound();
return (
<div className="flex flex-col gap-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<Link
href="/dashboard/radar"
className="text-sm text-text-muted hover:text-text-main transition-colors"
>
{t("backToRadar")}
</Link>
<h1 className="mt-3 text-2xl font-bold">{t("title")}</h1>
<p className="mt-1 text-sm text-text-muted">{t("subtitle")}</p>
</div>
<div className="flex items-center gap-2">
{meta?.supporterVerified === true && (
<span
data-badge-id="radar-supporter"
className="rounded-full border border-violet-500 px-3 py-1 text-sm text-violet-300"
>
{t("supporterBadge")}
</span>
)}
<button
type="button"
onClick={() => void sync()}
disabled={syncing}
className="rounded-lg border border-violet-500 px-4 py-2 text-sm font-medium text-violet-400 disabled:opacity-50"
>
{syncing ? t("syncing") : t("refresh")}
</button>
</div>
</div>
{error && <div className="rounded-lg bg-red-500/10 p-3 text-sm text-red-400">{error}</div>}
{loading ? (
<div className="flex min-h-48 items-center justify-center text-text-muted">
{t("loading")}
</div>
) : !intel || !meta ? (
<Card>
<p className="py-8 text-center text-text-muted">{t("empty")}</p>
</Card>
) : (
<>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<p className="text-xs uppercase tracking-wide text-text-muted">{t("methodology")}</p>
<p className="mt-2 font-semibold">
{t("eloMethod", {
initial: intel.methodology.initialRating,
factor: intel.methodology.kFactor,
})}
</p>
</Card>
<Card>
<p className="text-xs uppercase tracking-wide text-text-muted">{t("freshness")}</p>
<p className="mt-2 font-semibold">
{t(`freshnessValues.${intel.catalog.freshness}`)}
</p>
<p className="mt-1 text-sm text-text-muted">
{t("ageDays", { days: intel.catalog.ageDays })}
</p>
</Card>
<Card>
<p className="text-xs uppercase tracking-wide text-text-muted">{t("trend")}</p>
<p className="mt-2 font-semibold">{t(`trendValues.${intel.catalog.trend}`)}</p>
<p className="mt-1 text-sm text-text-muted">
{t("modelDelta", {
current: intel.catalog.models.current,
added: intel.catalog.models.added,
removed: intel.catalog.models.removed,
})}
</p>
</Card>
</div>
<Card>
<div className="mb-4 flex items-center justify-between gap-3">
<h2 className="text-lg font-semibold">{t("ranking")}</h2>
<span className="text-xs text-text-muted">{meta.version}</span>
</div>
{intel.rankings.length === 0 ? (
<p className="py-6 text-center text-text-muted">{t("noRankings")}</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="text-text-muted">
<tr>
<th className="pb-3">#</th>
<th className="pb-3">{t("model")}</th>
<th className="pb-3">{t("category")}</th>
<th className="pb-3 text-right">{t("rating")}</th>
<th className="pb-3 text-right">{t("matches")}</th>
</tr>
</thead>
<tbody>
{intel.rankings.map((ranking) => (
<tr
key={`${ranking.category}:${ranking.provider}:${ranking.modelId}`}
className="border-t border-border"
>
<td className="py-3">{ranking.rank}</td>
<td className="py-3 font-mono">
{ranking.provider}/{ranking.modelId}
</td>
<td className="py-3">{ranking.category}</td>
<td className="py-3 text-right">{ranking.rating}</td>
<td className="py-3 text-right">{ranking.matches}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Card>
</>
)}
</div>
);
}

View File

@@ -317,6 +317,14 @@ export default function RadarPage() {
<p className="text-sm text-text-muted mt-1">{t("subtitle")}</p>
</div>
<div className="flex items-center gap-2">
{(pageState === "empty" || pageState === "populated") && (
<Link
href="/dashboard/radar/intel"
className="px-4 py-2 text-sm font-medium rounded-lg border border-border text-text-main hover:border-violet-500 hover:text-violet-400 transition-colors"
>
{t("intel")}
</Link>
)}
{(pageState === "empty" || pageState === "populated") && (
<Link
href="/dashboard/radar/offers"

View File

@@ -11600,6 +11600,11 @@
"rare": "Rare achievements"
},
"badges": {
"radar-supporter": {
"name": "Radar Supporter",
"description": "Verified a live OmniRoute Radar supporter feed",
"criteria": "Verify a signed live Radar supporter feed."
},
"first-token": {
"name": "First Token",
"description": "Made your first API request",
@@ -12307,7 +12312,8 @@
"modelEnabled": "Enabled locally",
"localStateSaveFailed": "Failed to save local Radar settings",
"guidedCombos": "Guided combos",
"offers": "Offers"
"offers": "Offers",
"intel": "Intel"
},
"radarCombosPage": {
"title": "Radar guided combos",
@@ -12351,6 +12357,31 @@
"addConnectionDescription": "Don't have a connection yet? Add one in the providers dashboard.",
"addConnectionLink": "Go to providers →"
},
"radarIntelPage": {
"title": "Radar Intel",
"subtitle": "Radar-owned ELO rankings and factual catalog movement.",
"backToRadar": "Back to Radar",
"loading": "Loading Intel...",
"refresh": "Refresh Intel",
"syncing": "Refreshing...",
"loadFailed": "We could not refresh Intel. The last verified local cache is kept.",
"empty": "No verified Intel snapshot is available yet.",
"supporterBadge": "Radar Supporter",
"methodology": "Methodology",
"eloMethod": "ELO, initial {initial}, K={factor}",
"freshness": "Catalog freshness",
"ageDays": "{days, plural, one {# day old} other {# days old}}",
"trend": "Catalog trend",
"modelDelta": "{current} models, +{added} / -{removed}",
"ranking": "Model ranking",
"noRankings": "No confirmed comparisons are available yet.",
"model": "Model",
"category": "Category",
"rating": "Rating",
"matches": "Matches",
"freshnessValues": { "fresh": "Fresh", "aging": "Aging", "stale": "Stale" },
"trendValues": { "growing": "Growing", "stable": "Stable", "shrinking": "Shrinking" }
},
"radarOffersPage": {
"title": "Supporter offers",
"subtitle": "Official discounts, credits, trials, and reviewed partner benefits from Radar.",

View File

@@ -11600,6 +11600,11 @@
"rare": "Conquistas raras"
},
"badges": {
"radar-supporter": {
"name": "Apoiador do Radar",
"description": "Verificou um feed ativo de apoiador do OmniRoute Radar",
"criteria": "Verifique um feed ativo e assinado de apoiador do Radar."
},
"first-token": {
"name": "Primeiro Token",
"description": "Fez sua primeira requisição de API",
@@ -12307,7 +12312,8 @@
"modelEnabled": "Ativado localmente",
"localStateSaveFailed": "Falha ao salvar as configurações locais do Radar",
"guidedCombos": "Combos guiados",
"offers": "Ofertas"
"offers": "Ofertas",
"intel": "Intel"
},
"radarCombosPage": {
"title": "Combos guiados pelo Radar",
@@ -12351,6 +12357,31 @@
"addConnectionDescription": "Ainda não tem uma conexão? Adicione uma no painel de provedores.",
"addConnectionLink": "Ir para provedores →"
},
"radarIntelPage": {
"title": "Intel do Radar",
"subtitle": "Ranking ELO próprio do Radar e evolução factual do catálogo.",
"backToRadar": "Voltar ao Radar",
"loading": "Carregando Intel...",
"refresh": "Atualizar Intel",
"syncing": "Atualizando...",
"loadFailed": "Não foi possível atualizar o Intel. O último cache local verificado foi preservado.",
"empty": "Ainda não há um snapshot Intel verificado.",
"supporterBadge": "Apoiador do Radar",
"methodology": "Metodologia",
"eloMethod": "ELO, inicial {initial}, K={factor}",
"freshness": "Atualidade do catálogo",
"ageDays": "{days, plural, one {# dia} other {# dias}}",
"trend": "Tendência do catálogo",
"modelDelta": "{current} modelos, +{added} / -{removed}",
"ranking": "Ranking de modelos",
"noRankings": "Ainda não há comparações confirmadas.",
"model": "Modelo",
"category": "Categoria",
"rating": "Pontuação",
"matches": "Partidas",
"freshnessValues": { "fresh": "Atual", "aging": "Envelhecendo", "stale": "Desatualizado" },
"trendValues": { "growing": "Crescendo", "stable": "Estável", "shrinking": "Diminuindo" }
},
"radarOffersPage": {
"title": "Ofertas para apoiadores",
"subtitle": "Descontos, créditos, testes oficiais e benefícios de parceiros revisados pelo Radar.",

View File

@@ -11600,6 +11600,11 @@
"rare": "Thành tích hiếm"
},
"badges": {
"radar-supporter": {
"name": "Người ủng hộ Radar",
"description": "Đã xác minh nguồn dữ liệu trực tiếp dành cho người ủng hộ OmniRoute Radar",
"criteria": "Xác minh nguồn dữ liệu Radar trực tiếp đã được ký dành cho người ủng hộ."
},
"first-token": {
"name": "Token đầu tiên",
"description": "Đã thực hiện yêu cầu API đầu tiên",
@@ -12307,7 +12312,8 @@
"modelEnabled": "Enabled locally",
"localStateSaveFailed": "Failed to save local Radar settings",
"guidedCombos": "Guided combos",
"offers": "Offers"
"offers": "Offers",
"intel": "Thông tin chuyên sâu"
},
"radarSetupPage": {
"title": "Thiết lập nhà cung cấp",
@@ -12351,6 +12357,39 @@
"loadFailed": "Failed to load Radar combo suggestions.",
"createFailed": "Failed to create the combo. Review your provider connections and try again."
},
"radarIntelPage": {
"title": "Thông tin chuyên sâu Radar",
"subtitle": "Xếp hạng ELO do Radar quản lý và biến động thực tế của danh mục.",
"backToRadar": "Quay lại Radar",
"loading": "Đang tải thông tin chuyên sâu...",
"refresh": "Làm mới thông tin chuyên sâu",
"syncing": "Đang làm mới...",
"loadFailed": "Không thể làm mới thông tin chuyên sâu. Bộ nhớ đệm cục bộ đã xác minh gần nhất được giữ lại.",
"empty": "Chưa có bản chụp thông tin chuyên sâu đã xác minh.",
"supporterBadge": "Người ủng hộ Radar",
"methodology": "Phương pháp",
"eloMethod": "ELO, khởi tạo {initial}, K={factor}",
"freshness": "Độ mới của danh mục",
"ageDays": "{days, plural, one {# ngày tuổi} other {# ngày tuổi}}",
"trend": "Xu hướng danh mục",
"modelDelta": "{current} mô hình, +{added} / -{removed}",
"ranking": "Xếp hạng mô hình",
"noRankings": "Chưa có phép so sánh nào được xác nhận.",
"model": "Mô hình",
"category": "Danh mục",
"rating": "Điểm",
"matches": "Lượt so sánh",
"freshnessValues": {
"fresh": "Mới",
"aging": "Đang cũ dần",
"stale": "Đã cũ"
},
"trendValues": {
"growing": "Đang tăng",
"stable": "Ổn định",
"shrinking": "Đang giảm"
}
},
"radarOffersPage": {
"title": "Supporter offers",
"subtitle": "Official discounts, credits, trials, and reviewed partner benefits from Radar.",

View File

@@ -161,6 +161,16 @@ export const BUILTIN_BADGES: Omit<BadgeDefinition, "createdAt">[] = [
criteria: JSON.stringify({ type: "threshold", metric: "uptime", threshold: 100, window: 7 }),
hidden: 0,
},
{
id: "radar-supporter",
name: "Radar Supporter",
description: "Verified a live OmniRoute Radar supporter feed",
icon: "radar",
category: "contribution",
rarity: "rare",
criteria: JSON.stringify({ type: "action_count", action: "radar_supporter", threshold: 1 }),
hidden: 0,
},
// ── Streak (Engagement) ──────────────────────────────────────────────────
{

View File

@@ -25,7 +25,8 @@ export async function emitGamificationEvent(params: {
| "combo_use"
| "token_share"
| "invite_redeem"
| "daily_login";
| "daily_login"
| "radar_supporter";
metadata?: Record<string, unknown>;
}): Promise<void> {
const { apiKeyId, action, metadata } = params;
@@ -33,6 +34,13 @@ export async function emitGamificationEvent(params: {
if (!apiKeyId) return; // Skip if no API key
try {
// A verified Radar supporter is a recognition event, not an XP or
// leaderboard action. The caller supplies only a one-way key identity.
if (action === "radar_supporter") {
await checkAndUnlockBadge(apiKeyId, "radar-supporter", false);
return;
}
// 1. Award XP
const xpAmount = getXpForAction(action);
if (xpAmount > 0) {
@@ -87,7 +95,7 @@ export async function emitGamificationEvent(params: {
} catch (err) {
// Never throw — gamification must not break the request pipeline
log.error("events.error", {
apiKeyId,
...(action === "radar_supporter" ? {} : { apiKeyId }),
action,
error: err instanceof Error ? err.message : String(err),
});
@@ -114,22 +122,25 @@ function getXpForAction(action: string): number {
/**
* Check and unlock a specific badge.
*/
async function checkAndUnlockBadge(apiKeyId: string, badgeId: string): Promise<void> {
async function checkAndUnlockBadge(
apiKeyId: string,
badgeId: string,
logIdentity = true
): Promise<void> {
const { unlockBadge, hasBadge } = await import("../db/gamification");
// #3472: dedup via user_badges directly. getBadges() INNER-JOINs badge_definitions, which is
// empty until seeded, so it falsely reported "not earned" and re-emitted the unlock event on
// every request.
if (!hasBadge(apiKeyId, badgeId)) {
unlockBadge(apiKeyId, badgeId);
log.info("events.badge_unlocked", { apiKeyId, badgeId });
log.info("events.badge_unlocked", logIdentity ? { apiKeyId, badgeId } : { badgeId });
// Look up badge details from badge_definitions
const { getDbInstance } = await import("../db/core");
const badgeRow = getDbInstance()
.prepare("SELECT name, description, icon, rarity FROM badge_definitions WHERE id = ?")
.get(badgeId) as
| { name: string; description: string | null; icon: string | null; rarity: string }
| undefined;
{ name: string; description: string | null; icon: string | null; rarity: string } | undefined;
// Record notification for SSE toast
const { recordBadgeUnlock } = await import("./notifications");

View File

@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const pagePath = path.resolve(process.cwd(), "src/app/(dashboard)/dashboard/radar/intel/page.tsx");
const radarPagePath = path.resolve(process.cwd(), "src/app/(dashboard)/dashboard/radar/page.tsx");
test("Radar links to a dedicated local-only Intel page", () => {
assert.ok(fs.existsSync(pagePath));
assert.match(fs.readFileSync(radarPagePath, "utf8"), /href="\/dashboard\/radar\/intel"/);
const source = fs.readFileSync(pagePath, "utf8");
assert.match(source, /fetch\("\/api\/radar\/intel"\)/);
assert.match(source, /fetch\("\/api\/radar\/intel\/sync",\s*\{\s*method:\s*"POST"/);
assert.doesNotMatch(source, /RADAR_FEED_URL|radar\.omniroute\.online|omr_|getDbInstance/);
});
test("Intel page exposes methodology, ranking, freshness, trend, and verified supporter badge only", () => {
const source = fs.readFileSync(pagePath, "utf8");
for (const marker of [
"methodology",
"rankings",
"freshness",
"trend",
"supporterVerified",
"radar-supporter",
]) {
assert.match(source, new RegExp(marker));
}
assert.doesNotMatch(source, /\bhealth\b|\buptime\b|\blatency\b|\btelemetry\b/i);
});
test("Intel UI strings exist in English and Brazilian Portuguese", () => {
for (const locale of ["en", "pt-BR"]) {
const messages = JSON.parse(
fs.readFileSync(path.resolve(process.cwd(), `src/i18n/messages/${locale}.json`), "utf8")
) as { radarIntelPage?: Record<string, unknown>; radarPage?: Record<string, unknown> };
for (const key of [
"title",
"subtitle",
"methodology",
"supporterBadge",
"ranking",
"freshness",
"trend",
"empty",
"loadFailed",
]) {
assert.equal(typeof messages.radarIntelPage?.[key], "string", `${locale}: ${key}`);
}
assert.equal(typeof messages.radarPage?.intel, "string", `${locale}: radarPage.intel`);
}
});

View File

@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-radar-supporter-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { getDbInstance, resetDbInstance } = await import("../../src/lib/db/core.ts");
const { BUILTIN_BADGES } = await import("../../src/lib/gamification/badges.ts");
const { emitGamificationEvent } = await import("../../src/lib/gamification/events.ts");
test.after(() => {
resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("Radar supporter has a dedicated badge and zero-XP idempotent action", async () => {
const identity = `radar:${"a".repeat(64)}`;
const badge = BUILTIN_BADGES.find((item) => item.id === "radar-supporter");
assert.ok(badge);
assert.equal(JSON.parse(badge.criteria).action, "radar_supporter");
await emitGamificationEvent({ apiKeyId: identity, action: "radar_supporter" });
await emitGamificationEvent({ apiKeyId: identity, action: "radar_supporter" });
const db = getDbInstance();
const userBadges = db
.prepare("SELECT badge_id AS badgeId FROM user_badges WHERE api_key_id = ?")
.all(identity) as Array<{ badgeId: string }>;
const xpRows = db
.prepare("SELECT action FROM xp_audit_log WHERE api_key_id = ?")
.all(identity) as Array<{ action: string }>;
const scoreRows = db
.prepare("SELECT score FROM leaderboard WHERE api_key_id = ?")
.all(identity) as Array<{ score: number }>;
assert.deepEqual(userBadges, [{ badgeId: "radar-supporter" }]);
assert.deepEqual(xpRows, []);
assert.deepEqual(scoreRows, []);
});