feat: Labels-Feature vollständig entfernen (PROJ-9)

Backend:
- internal/labelstore/ gelöscht (Store, Schema, CRUD)
- internal/api/label_handlers.go gelöscht (alle Label-Routen)
- internal/api/server.go: labels-Feld + SetLabels() entfernt
- internal/api/search_handlers.go: label_id-Filter + Enrichment entfernt
- internal/index/index.go: LabelID aus SearchRequest entfernt
- internal/imapserver/server.go: labels-Feld + labelbasierte Mailboxen entfernt
- cmd/archivmail/main.go: labelstore-Init + SetLabels() entfernt
- cmd/archivmail/version.go: labelstore-Modul entfernt, index-Kommentar korrigiert

Frontend:
- LabelList.tsx, LabelPicker.tsx, LabelsTab.tsx gelöscht
- src/lib/api/system.ts: MailLabel/LabelRule-Typen + alle Label-Funktionen entfernt
- src/lib/api/index.ts: Label-Exports entfernt
- src/app/search/page.tsx: LabelList + selectedLabelId State entfernt
- src/app/mail/[id]/page.tsx: LabelPicker + Labels-State entfernt
- src/app/admin/page.tsx: LabelsTab + alle Label-Handler/State entfernt

Docs:
- features/PROJ-9: Status auf Removed gesetzt
- features/INDEX.md: PROJ-9 auf Removed gesetzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-04-04 21:32:13 +02:00
co-authored by Claude Sonnet 4.6
parent 4d1bdb6e8b
commit fdb25cb16a
19 changed files with 6 additions and 1954 deletions
-132
View File
@@ -28,12 +28,6 @@ import {
getTenantDomains,
addTenantDomain,
removeTenantDomain,
getAdminLabels,
createAdminLabel,
deleteAdminLabel,
getLabelRules,
createLabelRule,
deleteLabelRule,
getTenantLogoUrl,
uploadTenantLogo,
deleteTenantLogo,
@@ -50,8 +44,6 @@ import {
type Tenant,
type TenantDefaultUser,
type TenantDomain,
type MailLabel,
type LabelRule,
getCertInfo,
uploadCert,
generateSelfSignedCert,
@@ -72,7 +64,6 @@ import { SecurityTab } from "@/components/admin/tabs/SecurityTab";
import { LDAPTab } from "@/components/admin/tabs/LDAPTab";
import { TenantLDAPTab } from "@/components/admin/tabs/TenantLDAPTab";
import { TenantsTab } from "@/components/admin/tabs/TenantsTab";
import { LabelsTab } from "@/components/admin/tabs/LabelsTab";
import { CertTab } from "@/components/admin/tabs/CertTab";
import { ModulesTab } from "@/components/admin/ModulesTab";
import { IMAPSettingsTab } from "@/components/admin/tabs/IMAPSettingsTab";
@@ -235,19 +226,6 @@ export default function AdminPage() {
handleSyncLDAPUsers,
} = useTenantUsers();
// Labels state
const [adminLabels, setAdminLabels] = useState<MailLabel[]>([]);
const [adminLabelsLoading, setAdminLabelsLoading] = useState(false);
const [adminLabelsError, setAdminLabelsError] = useState("");
const [newLabelName, setNewLabelName] = useState("");
const [newLabelColor, setNewLabelColor] = useState("#ef4444");
const [labelCreating, setLabelCreating] = useState(false);
const [labelRules, setLabelRules] = useState<LabelRule[]>([]);
const [labelRulesLoading, setLabelRulesLoading] = useState(false);
const [newRuleField, setNewRuleField] = useState("from_domain");
const [newRuleValue, setNewRuleValue] = useState("");
const [newRuleLabelId, setNewRuleLabelId] = useState<number | null>(null);
const [ruleCreating, setRuleCreating] = useState(false);
// Certificate state
const [certInfo, setCertInfo] = useState<CertInfo | null>(null);
@@ -535,87 +513,6 @@ export default function AdminPage() {
}
}, []);
const loadAdminLabels = useCallback(async () => {
setAdminLabelsLoading(true);
setAdminLabelsError("");
try {
const data = await getAdminLabels();
setAdminLabels(data || []);
} catch {
setAdminLabelsError("Labels konnten nicht geladen werden.");
} finally {
setAdminLabelsLoading(false);
}
}, []);
const loadLabelRules = useCallback(async () => {
setLabelRulesLoading(true);
try {
const data = await getLabelRules();
setLabelRules(data || []);
} catch {
// ignore
} finally {
setLabelRulesLoading(false);
}
}, []);
function loadLabelsTab() {
loadAdminLabels();
loadLabelRules();
}
async function handleCreateAdminLabel(e: React.FormEvent) {
e.preventDefault();
if (!newLabelName.trim()) return;
setLabelCreating(true);
try {
await createAdminLabel(newLabelName.trim(), newLabelColor);
setNewLabelName("");
setNewLabelColor("#ef4444");
await loadAdminLabels();
} catch (err) {
setAdminLabelsError(err instanceof Error ? err.message : "Fehler");
} finally {
setLabelCreating(false);
}
}
async function handleDeleteAdminLabel(id: number, name: string) {
if (!window.confirm(`Globales Label "${name}" wirklich loeschen?`)) return;
try {
await deleteAdminLabel(id);
await loadAdminLabels();
await loadLabelRules();
} catch (err) {
setAdminLabelsError(err instanceof Error ? err.message : "Fehler");
}
}
async function handleCreateRule(e: React.FormEvent) {
e.preventDefault();
if (!newRuleValue.trim() || !newRuleLabelId) return;
setRuleCreating(true);
try {
await createLabelRule(newRuleField, newRuleValue.trim(), newRuleLabelId);
setNewRuleValue("");
await loadLabelRules();
} catch (err) {
setAdminLabelsError(err instanceof Error ? err.message : "Fehler");
} finally {
setRuleCreating(false);
}
}
async function handleDeleteRule(id: number) {
if (!window.confirm("Regel wirklich loeschen?")) return;
try {
await deleteLabelRule(id);
await loadLabelRules();
} catch {
// ignore
}
}
async function handleCreateTenant(e: React.FormEvent) {
e.preventDefault();
@@ -806,7 +703,6 @@ export default function AdminPage() {
{!isSuperAdmin && user?.role === "domain_admin" && (
<TabsTrigger value="imap-settings">IMAP</TabsTrigger>
)}
{isSuperAdmin && <TabsTrigger value="labels" onClick={loadLabelsTab}>Labels</TabsTrigger>}
{isSuperAdmin && <TabsTrigger value="security">Security</TabsTrigger>}
{isSuperAdmin && <TabsTrigger value="cert" onClick={loadCert}>Zertifikat</TabsTrigger>}
{isSuperAdmin && <TabsTrigger value="tenants" onClick={loadTenants}>Mandanten</TabsTrigger>}
@@ -957,34 +853,6 @@ export default function AdminPage() {
</TabsContent>
)}
{isSuperAdmin && (
<TabsContent value="labels">
<LabelsTab
adminLabels={adminLabels}
adminLabelsLoading={adminLabelsLoading}
adminLabelsError={adminLabelsError}
newLabelName={newLabelName}
setNewLabelName={setNewLabelName}
newLabelColor={newLabelColor}
setNewLabelColor={setNewLabelColor}
labelCreating={labelCreating}
labelRules={labelRules}
labelRulesLoading={labelRulesLoading}
newRuleField={newRuleField}
setNewRuleField={setNewRuleField}
newRuleValue={newRuleValue}
setNewRuleValue={setNewRuleValue}
newRuleLabelId={newRuleLabelId}
setNewRuleLabelId={setNewRuleLabelId}
ruleCreating={ruleCreating}
onCreateLabel={handleCreateAdminLabel}
onDeleteLabel={handleDeleteAdminLabel}
onCreateRule={handleCreateRule}
onDeleteRule={handleDeleteRule}
/>
</TabsContent>
)}
{isSuperAdmin && (
<TabsContent value="cert">
<CertTab
-29
View File
@@ -7,15 +7,11 @@ import {
downloadMailAttachment,
downloadMailRaw,
exportMailPDF,
getLabels,
getMailLabelIds,
type MailDetail,
type MailAttachment,
type MailLabel,
} from "@/lib/api";
import { useAuth } from "@/hooks/useAuth";
import { Navbar } from "@/components/navbar";
import { LabelPicker } from "@/components/LabelPicker";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
@@ -260,10 +256,6 @@ export default function MailViewPage({
const [downloading, setDownloading] = useState(false);
const [pdfLoading, setPdfLoading] = useState(false);
// Labels state
const [allLabels, setAllLabels] = useState<MailLabel[]>([]);
const [assignedLabelIds, setAssignedLabelIds] = useState<number[]>([]);
useEffect(() => {
if (!user) return;
getMail(id)
@@ -272,16 +264,9 @@ export default function MailViewPage({
setError(e instanceof Error ? e.message : "Unbekannter Fehler")
)
.finally(() => setLoading(false));
// Load labels
getLabels().then(setAllLabels).catch(() => {});
loadMailLabels();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, user]);
function loadMailLabels() {
getMailLabelIds(id).then(setAssignedLabelIds).catch(() => setAssignedLabelIds([]));
}
async function handleEmlDownload() {
setDownloading(true);
try {
@@ -380,20 +365,6 @@ export default function MailViewPage({
</CardHeader>
</Card>
{/* Labels */}
{allLabels.length > 0 && (
<Card>
<CardContent className="pt-4 pb-4">
<LabelPicker
emailId={id}
assignedLabelIds={assignedLabelIds}
allLabels={allLabels}
onUpdate={loadMailLabels}
/>
</CardContent>
</Card>
)}
{/* Body */}
<Card>
<CardContent className="pt-6">
+1 -21
View File
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
import { searchEmails, exportMailsZIP, uploadMailFilesUser, getUploadProgressUser, type SearchHit, type UploadJob } from "@/lib/api";
import { Navbar } from "@/components/navbar";
import { LabelList } from "@/components/LabelList";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
@@ -57,7 +56,6 @@ export default function SearchPage() {
const [dateTo, setDateTo] = useState("");
const [sort, setSort] = useState("date_desc");
const [hasAttachment, setHasAttachment] = useState<boolean | undefined>(undefined);
const [selectedLabelId, setSelectedLabelId] = useState<number | null>(null);
const [results, setResults] = useState<SearchHit[]>([]);
const [total, setTotal] = useState(0);
@@ -96,7 +94,6 @@ export default function SearchPage() {
date_to: dateTo || undefined,
sort: sort !== "date_desc" ? sort : undefined,
has_attachment: hasAttachment,
label_id: selectedLabelId ?? undefined,
page: p,
page_size: PAGE_SIZE,
});
@@ -111,7 +108,7 @@ export default function SearchPage() {
setSearching(false);
}
},
[query, fromFilter, toFilter, dateFrom, dateTo, sort, hasAttachment, selectedLabelId]
[query, fromFilter, toFilter, dateFrom, dateTo, sort, hasAttachment]
);
// Superadmin has no mail access — redirect to admin dashboard
@@ -139,13 +136,6 @@ export default function SearchPage() {
.finally(() => setSearching(false));
}, [user]);
// Re-search when label selection changes
useEffect(() => {
if (!user || !searched) return;
doSearch(1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedLabelId]);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
doSearch(1);
@@ -234,16 +224,6 @@ export default function SearchPage() {
)}
{!authLoading && user && (<>
<div className="flex gap-6">
{/* Label sidebar — nur für Rollen mit eigenen Mails */}
{user.role !== "auditor" && user.role !== "domain_auditor" && (
<div className="hidden md:block w-48 shrink-0">
<LabelList
selectedLabelId={selectedLabelId}
onLabelSelect={setSelectedLabelId}
/>
</div>
)}
{/* Main content */}
<div className="flex-1 min-w-0">
<form onSubmit={handleSubmit} className="space-y-4">