Kansho/frontend/src/pages/AdminIdentitiesPage.jsx
Lars fa1c11c33e
Some checks failed
Deploy Development / deploy (push) Successful in 57s
Test Suite / pytest-backend (push) Failing after 2m57s
Test Suite / smoke-dev (push) Successful in 0s
Test Suite / frontend-build (push) Successful in 16s
Add transitional learning detect with in-dialog mask review.
Senses grow from confirmations instead of a word list, so a later local pipeline can decide homonyms on a short passage. Default stays semantic. Local detect waits longer, and the reverse-proxy timeout is documented so Dev does not 504 first.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 08:34:43 +02:00

200 lines
6.5 KiB
JavaScript

import { useEffect, useState } from 'react'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
const emptyForm = { canonical_label: '', entity_type: 'PERSON', aliases: '' }
export default function AdminIdentitiesPage() {
const { session } = useAuth()
const [data, setData] = useState(null)
const [form, setForm] = useState(emptyForm)
const [error, setError] = useState('')
const [notice, setNotice] = useState('')
const load = () =>
api('/api/admin/identities', { token: session.token })
.then(setData)
.catch((e) => setError(e.message))
useEffect(() => { load() }, [session.token])
const aliasesOf = (value) =>
value
.split(',')
.map((item) => item.trim())
.filter(Boolean)
const create = async (e) => {
e.preventDefault()
setError('')
setNotice('')
try {
await api('/api/admin/identities', {
token: session.token,
method: 'POST',
body: {
canonical_label: form.canonical_label,
entity_type: form.entity_type,
aliases: aliasesOf(form.aliases)
}
})
setForm(emptyForm)
setNotice('Eintrag bestätigt und lokal gespeichert. Kein externer Aufruf.')
await load()
} catch (err) {
setError(err.message)
}
}
const patch = async (id, body) => {
setError('')
try {
await api(`/api/admin/identities/${id}`, {
token: session.token,
method: 'PATCH',
body
})
await load()
} catch (err) {
setError(err.message)
}
}
const remove = async (id) => {
setError('')
try {
await api(`/api/admin/identities/${id}`, { token: session.token, method: 'DELETE' })
await load()
} catch (err) {
setError(err.message)
}
}
const confirmProposal = async (id) => {
setError('')
try {
await api(`/api/admin/identities/proposals/${id}/confirm`, {
token: session.token,
method: 'POST',
body: {}
})
await load()
} catch (err) {
setError(err.message)
}
}
const dismissProposal = async (id) => {
setError('')
try {
await api(`/api/admin/identities/proposals/${id}/dismiss`, {
token: session.token,
method: 'POST',
body: {}
})
await load()
} catch (err) {
setError(err.message)
}
}
return (
<section className="card">
<h1>Identitäten</h1>
<p className="muted">
Nur lokale bestätigte Registry. Detect-Treffer werden nicht automatisch aktiv.
Unbestätigte Vorschläge gelten nicht als bekannte Identität. Kein externer Egress.
Mehrdeutigkeit entsteht aus Dialog-Bestätigungen, nicht aus einer Wortlisten-Seite.
</p>
{error && <p className="error">{error}</p>}
{notice && <p>{notice}</p>}
{data?.backup && <p className="muted">{data.backup}</p>}
<div className="table-scroll">
<table className="plain">
<thead>
<tr>
<th>Bezeichnung</th>
<th>Typ</th>
<th>Status</th>
<th>Aliase</th>
<th />
</tr>
</thead>
<tbody>
{(data?.registry || []).map((item) => {
const sense = (data?.senses || []).find(
(entry) => (entry.normalized_label || '').toLowerCase() === (item.canonical_label || '').toLowerCase()
)
return (
<tr key={item.id}>
<td>
{item.canonical_label}
{sense?.ambiguous ? <span className="muted"> · mehrdeutig</span> : null}
</td>
<td>
<select
value={item.entity_type}
onChange={(e) => patch(item.id, { entity_type: e.target.value })}
>
<option value="PERSON">PERSON</option>
<option value="PLACE">PLACE</option>
<option value="ORG">ORG</option>
<option value="PROJECT">PROJECT</option>
</select>
</td>
<td>{item.status}</td>
<td>{(item.aliases || []).join(', ')}</td>
<td>
{item.status !== 'confirmed' && (
<button type="button" className="ghost" onClick={() => patch(item.id, { status: 'confirmed' })}>
Bestätigen
</button>
)}
{item.status !== 'inactive' && (
<button type="button" className="ghost" onClick={() => patch(item.id, { status: 'inactive' })}>
Deaktivieren
</button>
)}
<button type="button" className="ghost" onClick={() => remove(item.id)}>Entfernen</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
<form className="stack" onSubmit={create}>
<h2>Identität bestätigen</h2>
<label>Kanonische Schreibweise <input value={form.canonical_label} onChange={(e) => setForm({ ...form, canonical_label: e.target.value })} required /></label>
<label>
Typ
<select value={form.entity_type} onChange={(e) => setForm({ ...form, entity_type: e.target.value })}>
<option value="PERSON">PERSON</option>
<option value="PLACE">PLACE</option>
<option value="ORG">ORG</option>
<option value="PROJECT">PROJECT</option>
</select>
</label>
<label>Bestätigte Aliase, kommagetrennt <input value={form.aliases} onChange={(e) => setForm({ ...form, aliases: e.target.value })} /></label>
<button type="submit">Lokal speichern</button>
</form>
<h2>Unbestätigte Detect-Vorschläge</h2>
<p className="muted">Wirken nicht auf spätere Requests und ersetzen keine vollständige Detection.</p>
<ul className="stack">
{(data?.proposals || []).map((item) => (
<li key={item.id}>
{item.observed_label} · {item.entity_type} · {item.status}
{item.status === 'unconfirmed' && (
<>
{' '}
<button type="button" className="ghost" onClick={() => confirmProposal(item.id)}>Bestätigen</button>
<button type="button" className="ghost" onClick={() => dismissProposal(item.id)}>Verwerfen</button>
</>
)}
</li>
))}
</ul>
</section>
)
}