Files
sitemente/app/amun/page.tsx
T

441 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useCallback } from "react";
import Card from "@/components/ui/Card";
const today = () => new Date().toISOString().split("T")[0];
type Severity = "low" | "medium" | "high" | "critical";
type BugStatus = "open" | "closed";
interface Bug {
description: string;
severity: Severity;
status: BugStatus;
}
interface FormState {
date: string;
testsRun: string[];
bugsFound: Bug[];
codeQualityScore: number;
recommendations: string[];
}
const defaultForm: FormState = {
date: today(),
testsRun: [""],
bugsFound: [],
codeQualityScore: 0,
recommendations: [""],
};
const severityColors: Record<Severity, string> = {
low: "text-emerald-400 bg-emerald-500/10 border-emerald-500/30",
medium: "text-amber-400 bg-amber-500/10 border-amber-500/30",
high: "text-orange-400 bg-orange-500/10 border-orange-500/30",
critical: "text-red-400 bg-red-500/10 border-red-500/30",
};
export default function AmunPage() {
const [form, setForm] = useState<FormState>(defaultForm);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [loadingDate, setLoadingDate] = useState(false);
const [history, setHistory] = useState<FormState[]>([]);
const loadSessionForDate = useCallback(async (date: string) => {
setLoadingDate(true);
try {
const res = await fetch(`/api/amun?date=${date}`);
const data = await res.json();
if (data) {
setForm({
date: data.date,
testsRun: typeof data.testsRun === "string" ? JSON.parse(data.testsRun) : (data.testsRun || [""]),
bugsFound: typeof data.bugsFound === "string" ? JSON.parse(data.bugsFound) : (data.bugsFound || []),
codeQualityScore: data.codeQualityScore || 0,
recommendations: typeof data.recommendations === "string" ? JSON.parse(data.recommendations) : (data.recommendations || [""]),
});
} else {
setForm({ ...defaultForm, date });
}
} catch {
setForm({ ...defaultForm, date });
} finally {
setLoadingDate(false);
}
}, []);
const loadHistory = useCallback(async () => {
try {
const res = await fetch("/api/amun?limit=10");
const data = await res.json();
if (Array.isArray(data)) {
setHistory(
data.map((s) => ({
date: s.date,
testsRun: typeof s.testsRun === "string" ? JSON.parse(s.testsRun) : (s.testsRun || []),
bugsFound: typeof s.bugsFound === "string" ? JSON.parse(s.bugsFound) : (s.bugsFound || []),
codeQualityScore: s.codeQualityScore || 0,
recommendations: typeof s.recommendations === "string" ? JSON.parse(s.recommendations) : (s.recommendations || []),
}))
);
}
} catch {
// ignore
}
}, []);
useEffect(() => {
loadSessionForDate(today());
loadHistory();
}, [loadSessionForDate, loadHistory]);
const validate = (): boolean => {
const errs: Record<string, string> = {};
if (form.codeQualityScore < 0 || form.codeQualityScore > 100) {
errs.codeQualityScore = "Score must be between 0 and 100";
}
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSave = async () => {
if (!validate()) return;
setSaving(true);
try {
const res = await fetch("/api/amun", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) {
const err = await res.json();
setErrors({ submit: err.error || "Failed to save" });
} else {
setSaved(true);
setTimeout(() => setSaved(false), 3000);
loadHistory();
}
} catch {
setErrors({ submit: "Network error. Please try again." });
} finally {
setSaving(false);
}
};
const updateArrayItem = (field: "testsRun" | "recommendations", index: number, value: string) => {
setForm((prev) => {
const arr = [...prev[field]];
arr[index] = value;
return { ...prev, [field]: arr };
});
};
const addArrayItem = (field: "testsRun" | "recommendations") => {
setForm((prev) => ({ ...prev, [field]: [...prev[field], ""] }));
};
const removeArrayItem = (field: "testsRun" | "recommendations", index: number) => {
setForm((prev) => ({ ...prev, [field]: prev[field].filter((_, i) => i !== index) }));
};
const addBug = () => {
setForm((prev) => ({
...prev,
bugsFound: [...prev.bugsFound, { description: "", severity: "medium", status: "open" }],
}));
};
const updateBug = (index: number, field: keyof Bug, value: string) => {
setForm((prev) => {
const bugs = [...prev.bugsFound];
bugs[index] = { ...bugs[index], [field]: value as never };
return { ...prev, bugsFound: bugs };
});
};
const removeBug = (index: number) => {
setForm((prev) => ({ ...prev, bugsFound: prev.bugsFound.filter((_, i) => i !== index) }));
};
const scoreColor = (score: number) => {
if (score >= 80) return "text-emerald-400";
if (score >= 60) return "text-amber-400";
if (score >= 40) return "text-orange-400";
return "text-red-400";
};
const scoreBarColor = (score: number) => {
if (score >= 80) return "bg-emerald-500";
if (score >= 60) return "bg-amber-500";
if (score >= 40) return "bg-orange-500";
return "bg-red-500";
};
const openBugs = form.bugsFound.filter((b) => b.status === "open");
const closedBugs = form.bugsFound.filter((b) => b.status === "closed");
return (
<div className="p-6 max-w-4xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<span className="text-purple-400"></span> Amun Dev Agent
</h1>
<p className="text-sm text-slate-500 mt-0.5">Development testing & code quality dashboard</p>
</div>
<div className="flex items-center gap-3">
<input
type="date"
value={form.date}
onChange={(e) => {
setForm((prev) => ({ ...prev, date: e.target.value }));
loadSessionForDate(e.target.value);
}}
className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-purple-500/50"
/>
<button
onClick={handleSave}
disabled={saving || loadingDate}
className="px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Saved" : "Save Session"}
</button>
</div>
</div>
{errors.submit && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
{errors.submit}
</div>
)}
{/* Stats Row */}
<div className="grid grid-cols-3 gap-4 mb-5">
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
<p className="text-xs text-slate-500 mb-1">Tests Run</p>
<p className="text-2xl font-bold text-white">{form.testsRun.filter((t) => t.trim()).length}</p>
</div>
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
<p className="text-xs text-slate-500 mb-1">Open Bugs</p>
<p className={`text-2xl font-bold ${openBugs.length > 0 ? "text-red-400" : "text-emerald-400"}`}>
{openBugs.length}
</p>
</div>
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
<p className="text-xs text-slate-500 mb-1">Code Quality</p>
<p className={`text-2xl font-bold ${scoreColor(form.codeQualityScore)}`}>
{form.codeQualityScore}%
</p>
</div>
</div>
<div className="space-y-5">
{/* Code Quality Score */}
<Card title="📊 Code Quality Score" accent="purple">
{errors.codeQualityScore && (
<p className="text-xs text-red-400 mb-2">{errors.codeQualityScore}</p>
)}
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="100"
value={form.codeQualityScore}
onChange={(e) => setForm((p) => ({ ...p, codeQualityScore: parseInt(e.target.value) }))}
className="flex-1 accent-purple-500"
/>
<div className="flex items-center gap-2">
<input
type="number"
min="0"
max="100"
value={form.codeQualityScore}
onChange={(e) =>
setForm((p) => ({
...p,
codeQualityScore: Math.min(100, Math.max(0, parseInt(e.target.value) || 0)),
}))
}
className="w-16 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-2 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50 text-center"
/>
<span className="text-sm text-slate-500">%</span>
</div>
</div>
<div className="mt-3 h-2 bg-[#0f1117] rounded-full overflow-hidden border border-[#2a2d3a]">
<div
className={`h-full rounded-full transition-all duration-300 ${scoreBarColor(form.codeQualityScore)}`}
style={{ width: `${form.codeQualityScore}%` }}
/>
</div>
</Card>
{/* Tests Run */}
<Card title="🧪 Tests Run" accent="blue">
<div className="space-y-2">
{form.testsRun.map((t, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-blue-500/70 font-mono mt-2.5 w-4"></span>
<input
type="text"
value={t}
onChange={(e) => updateArrayItem("testsRun", i, e.target.value)}
placeholder={`Test ${i + 1} (e.g. unit:auth, e2e:checkout)`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 font-mono"
/>
{form.testsRun.length > 1 && (
<button
onClick={() => removeArrayItem("testsRun", i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
<button
onClick={() => addArrayItem("testsRun")}
className="text-xs text-blue-400 hover:text-blue-300 transition-colors mt-1"
>
+ Add test
</button>
</div>
</Card>
{/* Bugs Found */}
<Card title="🐛 Bugs Found" subtitle={`${openBugs.length} open · ${closedBugs.length} closed`} accent="red">
<div className="space-y-3">
{form.bugsFound.length === 0 && (
<p className="text-sm text-slate-600 italic">No bugs logged yet.</p>
)}
{form.bugsFound.map((bug, i) => (
<div key={i} className="p-3 bg-[#0f1117] rounded-lg border border-[#2a2d3a]">
<div className="flex gap-2 mb-2">
<input
type="text"
value={bug.description}
onChange={(e) => updateBug(i, "description", e.target.value)}
placeholder="Bug description"
className="flex-1 bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
/>
<button
onClick={() => removeBug(i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none px-1"
>
×
</button>
</div>
<div className="flex gap-2">
<select
value={bug.severity}
onChange={(e) => updateBug(i, "severity", e.target.value)}
className={`flex-1 bg-[#13151f] border rounded-lg px-3 py-1.5 text-xs focus:outline-none ${severityColors[bug.severity]}`}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
<button
onClick={() => updateBug(i, "status", bug.status === "open" ? "closed" : "open")}
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
bug.status === "open"
? "bg-red-500/10 border-red-500/30 text-red-400 hover:bg-red-500/20"
: "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20"
}`}
>
{bug.status === "open" ? "● Open" : "✓ Closed"}
</button>
</div>
</div>
))}
<button
onClick={addBug}
className="text-xs text-red-400 hover:text-red-300 transition-colors mt-1"
>
+ Log bug
</button>
</div>
</Card>
{/* Recommendations */}
<Card title="💡 Recommendations" accent="amber">
<div className="space-y-2">
{form.recommendations.map((r, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-amber-500/70 font-mono mt-2.5 w-4"></span>
<input
type="text"
value={r}
onChange={(e) => updateArrayItem("recommendations", i, e.target.value)}
placeholder={`Recommendation ${i + 1}`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50"
/>
{form.recommendations.length > 1 && (
<button
onClick={() => removeArrayItem("recommendations", i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
<button
onClick={() => addArrayItem("recommendations")}
className="text-xs text-amber-400 hover:text-amber-300 transition-colors mt-1"
>
+ Add recommendation
</button>
</div>
</Card>
{/* Session History */}
{history.length > 0 && (
<Card title="📅 Recent Sessions" subtitle="Last 10 Amun sessions">
<div className="space-y-2">
{history.map((s) => (
<button
key={s.date}
onClick={() => loadSessionForDate(s.date)}
className="w-full flex items-center justify-between p-3 bg-[#0f1117] rounded-lg border border-[#2a2d3a] hover:border-purple-500/30 transition-colors text-left"
>
<div className="flex items-center gap-3">
<span className="text-xs text-slate-500 font-mono">{s.date}</span>
<span className="text-xs text-slate-400">
{s.testsRun.filter((t) => t.trim()).length} tests
</span>
{s.bugsFound.filter((b) => b.status === "open").length > 0 && (
<span className="text-xs text-red-400">
{s.bugsFound.filter((b) => b.status === "open").length} open bugs
</span>
)}
</div>
<span className={`text-sm font-bold ${scoreColor(s.codeQualityScore)}`}>
{s.codeQualityScore}%
</span>
</button>
))}
</div>
</Card>
)}
{/* Save Button */}
<div className="flex justify-end pb-6">
<button
onClick={handleSave}
disabled={saving}
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Session Saved!" : "Save Amun Session"}
</button>
</div>
</div>
</div>
);
}