Feat: Vulkan/RADV-Engine + vocab-gepruefte Spec-Drafts + Provisioning/Sync
Engine-Cutover ROCm/HIP -> Vulkan/RADV (gfx1151): +12-22% tg auf MoE (llama-bench
verifiziert, fast 53->65 t/s). ROCm-Build bleibt als Rollback unter /opt/llamacpp.
- Backend: vocab-aware Speculative Decoding. services/gguf_meta.py liest den
Tokenizer-Fingerprint (model/pre/n_vocab) direkt aus dem GGUF-Header (ohne Modell-Load);
register_model + migrate_config haengen nur VOCAB-KOMPATIBLE Drafts an (inkl. --spec-type,
das in dieser llama.cpp-Generation noetig ist). Neue Endpoints /api/models/drafts + /{id}/draft.
- Frontend: idiotensichere Spec-Draft-UI (SpecDraftModal) - nur kompatible Drafts waehlbar,
inkompatible gesperrt mit Begruendung; SPEC/SPEC?-Badge nach echtem Aktiv-Status; Rolle in AddModel.
- maintenance.py: Engine-Update-Quelle -> ggml-org/llama.cpp (Build-Nummer-Vergleich),
ENGINE_PATH=/opt/llamacpp-vulkan.
- Startup-Warmup der brains (deploy/warmup.sh, self-detaching ExecStartPost) + deploy/provision-engine.sh.
- Cleanup: tote LiteLLM gateway/config.yaml + alle Referenzen (config.py/backup.py/backup.sh) entfernt;
README + docs/memory aktualisiert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { useState } from "react"
|
||||
import { X, Check, Zap, AlertTriangle } from "lucide-react"
|
||||
import { api, type ModelInfo, type DraftInfo } from "@/lib/api"
|
||||
import { useDrafts } from "@/lib/queries"
|
||||
import { fmtSize } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/**
|
||||
* Idiotensichere Speculative-Decoding-Konfiguration für EIN Modell.
|
||||
* Zeigt nur VOCAB-KOMPATIBLE Drafts als wählbar; inkompatible werden gesperrt
|
||||
* und mit Begründung angezeigt. So kann nie ein kaputter Draft gesetzt werden
|
||||
* (der das Modell beim Laden scheitern ließe).
|
||||
*/
|
||||
export function SpecDraftModal({
|
||||
model, onClose, onChanged,
|
||||
}: { model: ModelInfo; onClose: () => void; onChanged: () => void }) {
|
||||
const { data, isLoading } = useDrafts(model.gguf_path)
|
||||
const [busy, setBusy] = useState<string | null>(null)
|
||||
const [err, setErr] = useState("")
|
||||
|
||||
const tv = data?.target_vocab
|
||||
const drafts = data?.drafts ?? []
|
||||
const compatibles = drafts.filter((d) => d.compatible === true)
|
||||
const currentFile = model.spec_draft_model
|
||||
|
||||
async function apply(draftPath: string | null) {
|
||||
setBusy(draftPath ?? "__clear__")
|
||||
setErr("")
|
||||
try {
|
||||
await api(`/api/models/${encodeURIComponent(model.name)}/draft`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ draft_path: draftPath }),
|
||||
})
|
||||
onChanged()
|
||||
onClose()
|
||||
} catch (e: any) {
|
||||
setErr(String(e?.message || e))
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
const vocabLabel = (v?: { pre: string | null; n_vocab: number | null } | null) =>
|
||||
v ? `${v.pre ?? "?"} · ${v.n_vocab?.toLocaleString() ?? "?"} Tokens` : "—"
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-lg rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-2">
|
||||
<Zap className="h-4 w-4" /> Speculative Draft
|
||||
</h3>
|
||||
<button onClick={onClose} className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground leading-relaxed">
|
||||
Beschleunigt die Token-Generierung mit einem kleinen "Draft"-Modell. Voraussetzung:
|
||||
der Draft muss den <strong className="text-foreground">exakt gleichen Tokenizer (Vocab)</strong> haben
|
||||
wie das Modell — sonst lehnt llama.cpp es ab.
|
||||
</div>
|
||||
|
||||
{/* Ziel-Vocab */}
|
||||
<div className="rounded-lg border border-border/40 bg-background/30 px-3 py-2 text-[11px] font-mono flex items-center justify-between">
|
||||
<span className="text-muted-foreground">{model.name.split("/").pop()?.replace(/\.gguf$/i, "")}</span>
|
||||
<span className="text-foreground">Vocab: {vocabLabel(tv)}</span>
|
||||
</div>
|
||||
|
||||
{/* Aktueller Zustand */}
|
||||
{model.spec_active && currentFile && (
|
||||
<div className="rounded-lg border border-emerald-500/30 bg-emerald-500/5 px-3 py-2 flex items-center justify-between gap-2">
|
||||
<span className="text-[11px] text-emerald-400 font-mono flex items-center gap-1.5 truncate">
|
||||
<Check className="h-3.5 w-3.5 shrink-0" /> Aktiv: {currentFile}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => apply(null)}
|
||||
disabled={busy !== null}
|
||||
className="h-6 px-2.5 rounded border border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/10 text-amber-400 text-[9px] font-bold uppercase shrink-0 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
Deaktivieren
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!data?.target_exists && (
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-[11px] text-amber-400 flex items-center gap-2">
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
|
||||
Modell-GGUF noch nicht vorhanden — Spec-Draft ist nach dem Download konfigurierbar.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Draft-Liste */}
|
||||
<div className="space-y-1.5 max-h-64 overflow-y-auto pr-1">
|
||||
{isLoading ? (
|
||||
<div className="text-xs text-muted-foreground py-6 text-center">Prüfe Vocab-Kompatibilität…</div>
|
||||
) : drafts.length === 0 ? (
|
||||
<div className="text-[11px] text-muted-foreground border border-dashed border-border/50 rounded-lg p-4 text-center">
|
||||
Keine Draft-Modelle in <code className="font-mono">/srv/models/drafts</code>.
|
||||
Lade ein kleines same-family-Modell (z.B. Qwen3-0.6B) und lege es dort ab.
|
||||
</div>
|
||||
) : (
|
||||
drafts.map((d: DraftInfo) => {
|
||||
const isCurrent = d.filename === currentFile
|
||||
const ok = d.compatible === true
|
||||
return (
|
||||
<div
|
||||
key={d.path}
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-2 flex items-center justify-between gap-2 text-left",
|
||||
ok ? "border-border/40 bg-background/20" : "border-border/20 bg-background/10 opacity-60",
|
||||
isCurrent && "border-primary/40 bg-primary/10",
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] font-mono font-semibold text-foreground truncate">{d.filename}</div>
|
||||
<div className="text-[9px] text-muted-foreground font-mono">
|
||||
{fmtSize(d.size_bytes)} · Vocab: {vocabLabel(d.vocab)}
|
||||
</div>
|
||||
</div>
|
||||
{ok ? (
|
||||
isCurrent ? (
|
||||
<span className="text-[9px] font-bold uppercase text-primary flex items-center gap-1 shrink-0"><Check className="h-3.5 w-3.5" /> Aktiv</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => apply(d.path)}
|
||||
disabled={busy !== null}
|
||||
className="h-7 px-3 rounded-lg border border-emerald-500/30 bg-emerald-500/5 hover:bg-emerald-500/15 text-emerald-400 text-[9px] font-bold uppercase shrink-0 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
Aktivieren
|
||||
</button>
|
||||
)
|
||||
) : (
|
||||
<span
|
||||
className="text-[9px] font-bold uppercase text-amber-400/80 flex items-center gap-1 shrink-0"
|
||||
title={d.compatible === false
|
||||
? `Inkompatibel: Tokenizer/Vocab weicht ab (Draft ${d.vocab?.pre}/${d.vocab?.n_vocab} ≠ Modell ${tv?.pre}/${tv?.n_vocab}).`
|
||||
: "Kompatibilität nicht prüfbar (Datei fehlt)."}
|
||||
>
|
||||
<AlertTriangle className="h-3.5 w-3.5" /> {d.compatible === false ? "Vocab ≠" : "n/a"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hinweis, wenn Drafts da sind aber keiner kompatibel */}
|
||||
{!isLoading && data?.target_exists && drafts.length > 0 && compatibles.length === 0 && (
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-[11px] text-amber-400 leading-relaxed">
|
||||
Kein vocab-kompatibler Draft verfügbar — Speculative Decoding ist für dieses Modell nicht möglich.
|
||||
Es braucht einen Draft mit identischem Tokenizer (pre=<span className="font-mono">{tv?.pre}</span>,
|
||||
n_vocab=<span className="font-mono">{tv?.n_vocab?.toLocaleString()}</span>).
|
||||
</div>
|
||||
)}
|
||||
|
||||
{err && <div className="text-[10px] text-red-400 font-mono">{err}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -88,10 +88,34 @@ export interface ModelInfo {
|
||||
incomplete: boolean
|
||||
prompt_cache: boolean
|
||||
spec_draft_model: string | null
|
||||
spec_type: string | null
|
||||
spec_active: boolean
|
||||
parallel_slots: number
|
||||
capabilities: Capabilities
|
||||
}
|
||||
|
||||
export interface VocabFingerprint {
|
||||
model: string | null
|
||||
pre: string | null
|
||||
n_vocab: number | null
|
||||
arch: string | null
|
||||
}
|
||||
|
||||
export interface DraftInfo {
|
||||
path: string
|
||||
filename: string
|
||||
size_bytes: number | null
|
||||
vocab: VocabFingerprint | null
|
||||
compatible: boolean | null // null = nicht prüfbar (Ziel-GGUF fehlt)
|
||||
}
|
||||
|
||||
export interface DraftsResp {
|
||||
target_path: string
|
||||
target_exists: boolean
|
||||
target_vocab: VocabFingerprint | null
|
||||
drafts: DraftInfo[]
|
||||
}
|
||||
|
||||
export interface DiscoverModel {
|
||||
name: string
|
||||
author: string
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type AgentStatus,
|
||||
type ConnectResp,
|
||||
type DiscoverResp,
|
||||
type DraftsResp,
|
||||
type Health,
|
||||
type Job,
|
||||
type Memory,
|
||||
@@ -31,6 +32,7 @@ export const qk = {
|
||||
agentStatus: ["agent-status"] as const,
|
||||
updates: ["updates"] as const,
|
||||
discover: ["discover"] as const,
|
||||
drafts: (target?: string) => ["drafts", target ?? ""] as const,
|
||||
connect: (params?: string) => ["connect", params ?? ""] as const,
|
||||
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
|
||||
}
|
||||
@@ -70,6 +72,14 @@ export const useUpdates = (refetchInterval?: number) =>
|
||||
export const useDiscover = () =>
|
||||
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
|
||||
|
||||
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
|
||||
export const useDrafts = (target?: string) =>
|
||||
useQuery({
|
||||
queryKey: qk.drafts(target),
|
||||
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
|
||||
enabled: !!target,
|
||||
})
|
||||
|
||||
export const useConnect = (params?: string) =>
|
||||
useQuery({
|
||||
queryKey: qk.connect(params),
|
||||
|
||||
@@ -7,8 +7,10 @@ export function AddModel() {
|
||||
const [quants, setQuants] = useState<string[]>([])
|
||||
const [quant, setQuant] = useState("Q4_K_M")
|
||||
const [msg, setMsg] = useState("")
|
||||
const [role, setRole] = useState("")
|
||||
const [q, setQ] = useState("")
|
||||
const [results, setResults] = useState<{ repo: string; downloads: number }[]>([])
|
||||
const ROLE_OPTS = ["fast", "heavy", "coder", "vision", "scout"]
|
||||
|
||||
async function loadQuants(r?: string) {
|
||||
const rr = r ?? repo
|
||||
@@ -42,10 +44,16 @@ export function AddModel() {
|
||||
setMsg("Download-Job wird initiiert...")
|
||||
try {
|
||||
await api("/api/models/install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo, quant, jinja: true }),
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo, quant, role: role || undefined, jinja: true }),
|
||||
})
|
||||
setMsg(`Download gestartet: ${repo} (${quant}). Fortschritt wird oben angezeigt.`)
|
||||
setMsg(
|
||||
`Download gestartet: ${repo} (${quant})${role ? `, Rolle: ${role}` : ""}. ` +
|
||||
`Fortschritt oben.` +
|
||||
(role === "fast" || role === "coder"
|
||||
? " Speculative Draft (Vocab-geprüft) kannst du nach dem Download an der Modellkarte (»Spec«) setzen."
|
||||
: "")
|
||||
)
|
||||
} catch (e) {
|
||||
setMsg(`Download-Fehler: ${e}`)
|
||||
}
|
||||
@@ -71,13 +79,22 @@ export function AddModel() {
|
||||
</button>
|
||||
{quants.length > 0 && (
|
||||
<>
|
||||
<select
|
||||
value={quant}
|
||||
onChange={(e) => setQuant(e.target.value)}
|
||||
<select
|
||||
value={quant}
|
||||
onChange={(e) => setQuant(e.target.value)}
|
||||
className="h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none text-foreground font-semibold"
|
||||
>
|
||||
{quants.map((qq) => <option key={qq} value={qq} className="bg-popover text-foreground">{qq}</option>)}
|
||||
</select>
|
||||
<select
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
title="Rolle (optional) — bestimmt Auto-Konfiguration wie parallele Slots"
|
||||
className="h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none text-foreground font-semibold"
|
||||
>
|
||||
<option value="" className="bg-popover text-foreground">Rolle…</option>
|
||||
{ROLE_OPTS.map((r) => <option key={r} value={r} className="bg-popover text-foreground">{r}</option>)}
|
||||
</select>
|
||||
<button
|
||||
onClick={install}
|
||||
className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all cursor-pointer flex items-center gap-1.5"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState, useRef, useCallback } from "react"
|
||||
import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Copy } from "lucide-react"
|
||||
import { api } from "@/lib/api"
|
||||
import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Copy, Zap } from "lucide-react"
|
||||
import { api, type ModelInfo } from "@/lib/api"
|
||||
import { useModels, useRouting, useConnect, useUpdates, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { CapsChips } from "@/components/CapsChips"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { fmtSize, fmtCtx } from "@/lib/format"
|
||||
import { getBrandInfo, ROLES } from "@/components/models/ModelBadges"
|
||||
import { SpecDraftModal } from "@/components/models/SpecDraftModal"
|
||||
|
||||
export function Cockpit() {
|
||||
const qc = useQueryClient()
|
||||
@@ -26,6 +27,7 @@ export function Cockpit() {
|
||||
// UI state
|
||||
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
|
||||
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
|
||||
const [specModel, setSpecModel] = useState<ModelInfo | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid")
|
||||
@@ -742,11 +744,15 @@ export function Cockpit() {
|
||||
PC
|
||||
</span>
|
||||
)}
|
||||
{m.spec_draft_model && (
|
||||
{m.spec_active ? (
|
||||
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-[9px] font-mono text-emerald-400 font-bold uppercase" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
||||
SPEC
|
||||
</span>
|
||||
)}
|
||||
) : m.spec_draft_model ? (
|
||||
<span className="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[9px] font-mono text-amber-400 font-bold uppercase" title={`Draft gesetzt (${m.spec_draft_model}), aber INAKTIV — --spec-type fehlt oder Vocab inkompatibel. Über 'Spec' neu konfigurieren.`}>
|
||||
SPEC?
|
||||
</span>
|
||||
) : null}
|
||||
{m.parallel_slots > 1 && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-[9px] font-mono text-violet-400 font-bold uppercase" title={`${m.parallel_slots} parallele Slots aktiv`}>
|
||||
SLOTS: {m.parallel_slots}
|
||||
@@ -825,6 +831,18 @@ export function Cockpit() {
|
||||
>
|
||||
Ctx
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSpecModel(m)}
|
||||
className={cn(
|
||||
"h-7 px-2.5 rounded-lg border text-[9px] font-bold uppercase transition-colors cursor-pointer flex items-center gap-1",
|
||||
m.spec_active ? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
: m.spec_draft_model ? "border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
|
||||
: "border-border/40 text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
title="Speculative Draft konfigurieren (Vocab-geprüft)"
|
||||
>
|
||||
<Zap className="h-3 w-3" /> Spec
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(m.name)}
|
||||
className="h-7 w-7 rounded-lg border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 flex items-center justify-center transition-colors cursor-pointer"
|
||||
@@ -885,11 +903,15 @@ export function Cockpit() {
|
||||
PC
|
||||
</span>
|
||||
)}
|
||||
{m.spec_draft_model && (
|
||||
{m.spec_active ? (
|
||||
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-[8px] font-mono text-emerald-400 font-bold uppercase shrink-0" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
||||
SPEC
|
||||
</span>
|
||||
)}
|
||||
) : m.spec_draft_model ? (
|
||||
<span className="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[8px] font-mono text-amber-400 font-bold uppercase shrink-0" title={`Draft gesetzt (${m.spec_draft_model}), aber INAKTIV — --spec-type fehlt oder Vocab inkompatibel.`}>
|
||||
SPEC?
|
||||
</span>
|
||||
) : null}
|
||||
{m.parallel_slots > 1 && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-[8px] font-mono text-violet-400 font-bold uppercase shrink-0" title={`${m.parallel_slots} parallele Slots aktiv`}>
|
||||
SLOTS: {m.parallel_slots}
|
||||
@@ -937,15 +959,27 @@ export function Cockpit() {
|
||||
>
|
||||
{isRunning ? "Entladen" : "Laden"}
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={() => handleSetCtx(m.name, m.ctx)}
|
||||
className="h-7 px-2.5 rounded-lg border border-border/40 text-[9px] font-bold uppercase hover:bg-accent text-muted-foreground transition-all cursor-pointer"
|
||||
title="Kontextlänge anpassen"
|
||||
>
|
||||
Ctx
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(m.name)}
|
||||
<button
|
||||
onClick={() => setSpecModel(m)}
|
||||
className={cn(
|
||||
"h-7 px-2.5 rounded-lg border text-[9px] font-bold uppercase transition-all cursor-pointer flex items-center gap-1",
|
||||
m.spec_active ? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
: m.spec_draft_model ? "border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
|
||||
: "border-border/40 text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
title="Speculative Draft konfigurieren (Vocab-geprüft)"
|
||||
>
|
||||
<Zap className="h-3 w-3" /> Spec
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(m.name)}
|
||||
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all cursor-pointer"
|
||||
title="Modell löschen"
|
||||
>
|
||||
@@ -1016,6 +1050,14 @@ export function Cockpit() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{specModel && (
|
||||
<SpecDraftModal
|
||||
model={specModel}
|
||||
onClose={() => setSpecModel(null)}
|
||||
onChanged={reload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dialogElement}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user