chore: clean up dead barrels, duplicate config, and orphaned frontend components
Gateway: - Remove dead barrels (ai-moderation/index, attachment-upload/index, message-capture/index) — all consumers import files directly - Remove orphaned schema/ split dir (analytics/cache/messages/meta) — schema.ts is monolithic - Merge duplicate config singleton: delete shared/config/config.ts, point all 44 imports at shared/config/index Backend: - Remove dead commandHelper.ts (voice-era fallback), ws/index.ts barrel, health.schema.ts, moderationMetrics.ts, analysis.schema.ts (0 importers; metrics/handlers route directly) Frontend: - Remove orphaned CategoryDrilldown/CoverageTiles/TopicTrends, primitives/slot, use-mobile, use-mounted - Remove unused charts donut/sparkline (TopicTrends was only consumer) Kept (verified active): shared/database/index.ts facade (11 importers), hooks/index + lib/api/index barrels (10 importers), orpc/ws.ts, charts/index.ts barrel. Verified: tsc + biome + vitest per service (backend e2e 3 failures pre-existing on main); frontend next build 8 routes.
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { Badge, GlassPanel } from "@/components/primitives";
|
||||
import { SectionHeader } from "@/components/shared";
|
||||
import { formatNumber, formatRelativeTime } from "@/lib/format";
|
||||
import type { CategoryAction, ModerationTrends } from "@/lib/types";
|
||||
|
||||
const SEVERITY_TONE: Record<
|
||||
string,
|
||||
"signal" | "amber" | "vermilion" | "neutral"
|
||||
> = {
|
||||
critical: "vermilion",
|
||||
high: "vermilion",
|
||||
medium: "amber",
|
||||
low: "signal",
|
||||
none: "neutral",
|
||||
};
|
||||
|
||||
interface CategoryDrilldownProps {
|
||||
trends: ModerationTrends;
|
||||
selected?: string | null;
|
||||
actions?: CategoryAction[];
|
||||
loading?: boolean;
|
||||
onSelect: (category: string | null) => void;
|
||||
}
|
||||
|
||||
export function CategoryDrilldown({
|
||||
trends,
|
||||
selected,
|
||||
actions,
|
||||
loading,
|
||||
onSelect,
|
||||
}: CategoryDrilldownProps) {
|
||||
const maxCat = trends.categories.reduce((m, c) => Math.max(m, c.count), 0);
|
||||
|
||||
return (
|
||||
<GlassPanel className="lg:col-span-3">
|
||||
<SectionHeader eyebrow="drill-down" title="Flag Category" />
|
||||
{selected ? (
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect(null)}
|
||||
className="text-xs text-ink-soft hover:text-ink"
|
||||
>
|
||||
← Back to all categories
|
||||
</button>
|
||||
<span className="text-xs text-ink-faint">
|
||||
/ {selected} (
|
||||
{loading ? "loading…" : formatNumber(actions?.length ?? 0)} actions)
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mb-2 text-xs text-ink-faint">
|
||||
Click a category to list the underlying moderation actions.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!selected ? (
|
||||
<div className="space-y-2">
|
||||
{trends.categories.map((c) => {
|
||||
const pct = maxCat > 0 ? Math.max(2, (c.count / maxCat) * 100) : 0;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={c.name}
|
||||
onClick={() => onSelect(c.name)}
|
||||
className="flex w-full items-center gap-3 text-left text-sm"
|
||||
>
|
||||
<span className="w-36 shrink-0 truncate text-ink-soft">
|
||||
{c.name}
|
||||
</span>
|
||||
<div className="h-2 flex-1 overflow-hidden rounded-full bg-hairline">
|
||||
<div
|
||||
className="h-full rounded-full bg-signal"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="mono w-10 text-right text-ink">
|
||||
{formatNumber(c.count)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{loading && <p className="text-xs text-ink-faint">Loading…</p>}
|
||||
{!loading && actions && actions.length === 0 && (
|
||||
<p className="text-xs text-ink-faint">
|
||||
No actions in this category.
|
||||
</p>
|
||||
)}
|
||||
{actions?.slice(0, 12).map((a) => (
|
||||
<div key={a.id} className="flex items-start gap-2 text-sm">
|
||||
<Badge tone={SEVERITY_TONE[a.severity ?? "none"]}>
|
||||
{a.severity ?? "none"}
|
||||
</Badge>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2">
|
||||
<span className="font-medium text-ink">{a.action_type}</span>
|
||||
{a.username && (
|
||||
<span className="text-ink-soft">@{a.username}</span>
|
||||
)}
|
||||
<span
|
||||
className="text-ink-faint mono text-xs"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{a.created_at ? formatRelativeTime(a.created_at) : ""}
|
||||
</span>
|
||||
</div>
|
||||
{a.content && (
|
||||
<p className="mt-0.5 line-clamp-2 text-ink-faint">
|
||||
{a.content}
|
||||
</p>
|
||||
)}
|
||||
{a.reason && (
|
||||
<p className="mt-0.5 line-clamp-1 text-xs text-ink-faint">
|
||||
Reason: {a.reason}
|
||||
</p>
|
||||
)}
|
||||
<ChevronRight className="mt-1 size-3 text-ink-faint/50" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</GlassPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { AlertCircle, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { GlassPanel } from "@/components/primitives";
|
||||
import { MetricTile, SectionHeader } from "@/components/shared";
|
||||
import { formatNumber } from "@/lib/format";
|
||||
import type { ModerationCoverage } from "@/lib/types";
|
||||
|
||||
export function CoverageTiles({ coverage }: { coverage: ModerationCoverage }) {
|
||||
const pct = (n: number) => `${n.toFixed(1)}%`;
|
||||
return (
|
||||
<GlassPanel className="lg:col-span-5">
|
||||
<SectionHeader eyebrow="automation" title="Auto-mod Coverage" />
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<MetricTile
|
||||
label="Coverage"
|
||||
value={pct(coverage.coverage_rate)}
|
||||
tone={coverage.coverage_rate > 90 ? "signal" : "amber"}
|
||||
icon={<CheckCircle2 className="size-3.5" />}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Completed"
|
||||
value={formatNumber(coverage.completed)}
|
||||
tone="signal"
|
||||
icon={<CheckCircle2 className="size-3.5" />}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Failed"
|
||||
value={formatNumber(coverage.failed)}
|
||||
tone={coverage.failed > 0 ? "vermilion" : "neutral"}
|
||||
icon={<XCircle className="size-3.5" />}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Pending"
|
||||
value={formatNumber(coverage.pending)}
|
||||
tone={coverage.pending > 0 ? "amber" : "neutral"}
|
||||
icon={<AlertCircle className="size-3.5" />}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-ink-faint">
|
||||
{pct(coverage.failed_rate)} of analysis runs failed. Total runs in
|
||||
window: {formatNumber(coverage.total)}.
|
||||
</p>
|
||||
</GlassPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Donut } from "@/components/charts/donut";
|
||||
import { GlassPanel } from "@/components/primitives";
|
||||
import { SectionHeader } from "@/components/shared";
|
||||
import { formatNumber } from "@/lib/format";
|
||||
import type { ModerationTrends } from "@/lib/types";
|
||||
|
||||
const SEVERITY_COLOR: Record<string, string> = {
|
||||
critical: "var(--color-vermilion)",
|
||||
high: "var(--color-vermilion)",
|
||||
medium: "var(--color-amber)",
|
||||
low: "var(--color-signal)",
|
||||
none: "var(--color-ink-faint)",
|
||||
};
|
||||
|
||||
function BarRow({
|
||||
label,
|
||||
count,
|
||||
max,
|
||||
color = "var(--color-signal)",
|
||||
}: {
|
||||
label: string;
|
||||
count: number;
|
||||
max: number;
|
||||
color?: string;
|
||||
}) {
|
||||
const pct = max > 0 ? Math.max(2, (count / max) * 100) : 0;
|
||||
return (
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="w-32 shrink-0 truncate text-ink-soft">{label}</span>
|
||||
<div className="h-2 flex-1 overflow-hidden rounded-full bg-hairline">
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{ width: `${pct}%`, background: color }}
|
||||
/>
|
||||
</div>
|
||||
<span className="mono w-10 shrink-0 text-right text-ink">
|
||||
{formatNumber(count)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopicTrends({ trends }: { trends: ModerationTrends }) {
|
||||
const maxCat = trends.categories.reduce((m, c) => Math.max(m, c.count), 0);
|
||||
const maxAct = trends.actions.reduce((m, a) => Math.max(m, a.count), 0);
|
||||
const totalSev = trends.severities.reduce((s, x) => s + x.count, 0);
|
||||
|
||||
const severitySegments = trends.severities.map((s) => ({
|
||||
value: s.count,
|
||||
color: SEVERITY_COLOR[s.level] ?? "var(--color-ink-faint)",
|
||||
label: s.level,
|
||||
}));
|
||||
|
||||
return (
|
||||
<GlassPanel className="lg:col-span-2">
|
||||
<SectionHeader eyebrow="insight" title="Toxic Topic Trends" />
|
||||
{trends.categories.length === 0 && trends.severities.length === 0 ? (
|
||||
<p className="py-6 text-center text-xs text-ink-faint">
|
||||
No categorized actions in the last 30 days.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<p className="mb-2 text-xs uppercase tracking-wide text-ink-faint">
|
||||
Top flagged categories
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{trends.categories.slice(0, 10).map((c) => (
|
||||
<BarRow
|
||||
key={c.name}
|
||||
label={c.name}
|
||||
count={c.count}
|
||||
max={maxCat}
|
||||
/>
|
||||
))}
|
||||
{trends.categories.length === 0 && (
|
||||
<p className="text-xs text-ink-faint">
|
||||
No categories recorded.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="mb-2 text-xs uppercase tracking-wide text-ink-faint">
|
||||
Severity
|
||||
</p>
|
||||
{totalSev > 0 ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Donut
|
||||
segments={severitySegments}
|
||||
centerLabel={formatNumber(totalSev)}
|
||||
centerSub="total"
|
||||
size={88}
|
||||
/>
|
||||
<div className="space-y-1 text-xs">
|
||||
{trends.severities.map((s) => (
|
||||
<div key={s.level} className="flex items-center gap-2">
|
||||
<span
|
||||
className="size-2.5 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
SEVERITY_COLOR[s.level] ??
|
||||
"var(--color-ink-faint)",
|
||||
}}
|
||||
/>
|
||||
<span className="capitalize text-ink-soft">
|
||||
{s.level}
|
||||
</span>
|
||||
<span className="mono ml-auto text-ink">
|
||||
{formatNumber(s.count)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-ink-faint">No severity data.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs uppercase tracking-wide text-ink-faint">
|
||||
Action types
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{trends.actions.slice(0, 6).map((a) => (
|
||||
<BarRow
|
||||
key={a.type}
|
||||
label={a.type.replace("_", " ")}
|
||||
count={a.count}
|
||||
max={maxAct}
|
||||
color="#8b5cf6"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</GlassPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/** Stacked donut for moderation overview / composition. */
|
||||
export function Donut({
|
||||
segments,
|
||||
size = 132,
|
||||
thickness = 14,
|
||||
centerLabel,
|
||||
centerSub,
|
||||
}: {
|
||||
segments: { value: number; color: string; label: string }[];
|
||||
size?: number;
|
||||
thickness?: number;
|
||||
centerLabel?: string;
|
||||
centerSub?: string;
|
||||
}) {
|
||||
const total = segments.reduce((s, x) => s + x.value, 0) || 1;
|
||||
const r = size / 2 - thickness / 2;
|
||||
const c = 2 * Math.PI * r;
|
||||
let offset = 0;
|
||||
return (
|
||||
<div
|
||||
className="relative inline-flex items-center justify-center"
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className="-rotate-90"
|
||||
role="img"
|
||||
aria-label="Composition donut"
|
||||
>
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke="var(--color-hairline)"
|
||||
strokeWidth={thickness}
|
||||
/>
|
||||
{segments.map((s) => {
|
||||
const len = (s.value / total) * c;
|
||||
const el = (
|
||||
<circle
|
||||
key={`seg-${s.label}`}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke={s.color}
|
||||
strokeWidth={thickness}
|
||||
strokeDasharray={`${len} ${c - len}`}
|
||||
strokeDashoffset={-offset}
|
||||
style={{ transition: "stroke-dashoffset 0.6s ease" }}
|
||||
/>
|
||||
);
|
||||
offset += len;
|
||||
return el;
|
||||
})}
|
||||
</svg>
|
||||
{(centerLabel || centerSub) && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
{centerLabel && (
|
||||
<span className="display text-lg">{centerLabel}</span>
|
||||
)}
|
||||
{centerSub && (
|
||||
<span className="mono text-[0.6rem] text-ink-faint">
|
||||
{centerSub}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
export { AreaActivity } from "./area-activity";
|
||||
export { Donut } from "./donut";
|
||||
export { RadialGauge } from "./radial-gauge";
|
||||
export { Sparkline } from "./sparkline";
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** Minimal sparkline. Pure SVG, scales to container width. */
|
||||
export function Sparkline({
|
||||
values,
|
||||
className,
|
||||
stroke = "var(--color-signal)",
|
||||
fill = true,
|
||||
height = 40,
|
||||
}: {
|
||||
values: number[];
|
||||
className?: string;
|
||||
stroke?: string;
|
||||
fill?: boolean;
|
||||
height?: number;
|
||||
}) {
|
||||
if (values.length === 0) return null;
|
||||
const w = 100;
|
||||
const max = Math.max(...values, 1);
|
||||
const min = Math.min(...values, 0);
|
||||
const span = max - min || 1;
|
||||
const pts = values.map((v, i) => {
|
||||
const x = (i / (values.length - 1)) * w;
|
||||
const y = height - ((v - min) / span) * (height - 4) - 2;
|
||||
return [x, y] as const;
|
||||
});
|
||||
const line = pts
|
||||
.map(
|
||||
(p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(2)},${p[1].toFixed(2)}`,
|
||||
)
|
||||
.join(" ");
|
||||
const area = `${line} L${w},${height} L0,${height} Z`;
|
||||
const id = `spark-${stroke.replace(/[^a-z0-9]/gi, "")}`;
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${w} ${height}`}
|
||||
preserveAspectRatio="none"
|
||||
className={cn("w-full", className)}
|
||||
style={{ height }}
|
||||
role="img"
|
||||
aria-label="Trend sparkline"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={stroke} stopOpacity="0.35" />
|
||||
<stop offset="100%" stopColor={stroke} stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{fill && <path d={area} fill={`url(#${id})`} />}
|
||||
<path
|
||||
d={line}
|
||||
fill="none"
|
||||
stroke={stroke}
|
||||
strokeWidth={1.5}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* Minimal Slot — merges its props onto its single child element (Radix-style
|
||||
* `asChild`). Enough for wrapping <Link>/<a> in a Button.
|
||||
*/
|
||||
export const Slot = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }
|
||||
>(({ children, ...slotProps }, ref) => {
|
||||
if (!React.isValidElement(children)) return null;
|
||||
const childProps = children.props as Record<string, unknown>;
|
||||
const merged: Record<string, unknown> = { ...childProps, ...slotProps, ref };
|
||||
// Merge className
|
||||
if (slotProps.className || childProps.className) {
|
||||
merged.className = [childProps.className, slotProps.className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
// Merge style
|
||||
if (slotProps.style || childProps.style) {
|
||||
merged.style = {
|
||||
...(childProps.style as object),
|
||||
...(slotProps.style as object),
|
||||
};
|
||||
}
|
||||
return React.cloneElement(children, merged);
|
||||
});
|
||||
Slot.displayName = "Slot";
|
||||
@@ -1,21 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Returns true once the component has mounted on the client. Charts wrapped
|
||||
* in ResponsiveContainer measure their parent during the first layout — if
|
||||
* the container has no size yet (hydration/flex/grid), Recharts logs
|
||||
* "width(-1) and height(-1)" warnings. Deferring the chart render until after
|
||||
* mount guarantees the container has real dimensions.
|
||||
*/
|
||||
export function useMounted(): boolean {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
return mounted;
|
||||
}
|
||||
Reference in New Issue
Block a user