add permission scanner, info panels, image QR scan, invite tracker, ticket templates

Five new features:
- /permissions scan: audits admin/ban roles, dangerous bot permissions,
  public channels, empty/duplicate/useless roles, too-many-admins warning.
- /panel create: fixed info panels (rules/support/bewerbung/partner/
  rollen/events/faq) with buttons wired into the existing ticket/
  register/partner flows, FAQ panels get a question dropdown.
- Image moderation: QR-code detection in message attachments via jimp+jsqr
  (pure JS, no native build deps) with a small scam-pattern check; opt-in
  toggle in dashboard settings since decoding costs per-message.
- Invite tracker extension: now resolves the inviter (not just the code),
  tracks suspicious joins per invite, and the dashboard growth page gets
  an invite breakdown + recent-joins table.
- Ticket categories: /ticketconfig lets each ticket topic get its own
  ping role and a modal question template shown before the channel is
  created; topics without config behave exactly as before.

Also fixes two rough edges found along the way: the welcome embed had
no way to attach an image despite the backend already supporting it
(added URL/file-upload fields + live preview), and Reaction Roles
required typing raw role IDs into a textarea (replaced with a proper
role picker). Incidentally repairs node_modules/.bin symlinks that were
committed as literal broken shell-script text instead of real symlinks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 00:46:23 +02:00
parent 54376fdad7
commit 9943254aa4
49 changed files with 3992 additions and 339 deletions

View File

@@ -1,13 +1,29 @@
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Separator, TextField, Label } from '@heroui/react';
import { Tag, Save, Hash, List } from 'lucide-react';
import { useState } from 'react';
import { Card, CardContent, CardHeader, Input, Button, Chip, TextField, Label } from '@heroui/react';
import { Tag, Save, Plus, X } from 'lucide-react';
import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard';
import { ChannelSelect } from '../components/shared/ChannelSelect';
import { RoleSelect } from '../components/shared/RoleSelect';
import { useGuildResources } from '../hooks/useGuildResources';
export function ReactionRoles() {
const { reactionRoles, reactionDraft, setReactionDraft, saveReactionRole, currentGuildId } = useApp();
const { channels } = useGuildResources(currentGuildId);
const { channels, roles } = useGuildResources(currentGuildId);
const [entryDraft, setEntryDraft] = useState({ emoji: '', roleId: '', label: '' });
const addEntry = () => {
if (!entryDraft.emoji.trim() || !entryDraft.roleId) return;
setReactionDraft((s) => ({
...s,
entries: [...s.entries, { emoji: entryDraft.emoji.trim(), roleId: entryDraft.roleId, label: entryDraft.label.trim(), description: '' }]
}));
setEntryDraft({ emoji: '', roleId: '', label: '' });
};
const removeEntry = (index: number) => {
setReactionDraft((s) => ({ ...s, entries: s.entries.filter((_: any, i: number) => i !== index) }));
};
return (
<SectionCard title="Reaction Roles" subtitle="Sets anzeigen und neue Zuordnungen anlegen">
@@ -61,20 +77,56 @@ export function ReactionRoles() {
/>
</TextField>
<TextField>
<div>
<Label>Einträge</Label>
<TextArea
placeholder="Emoji | Role ID | Label&#10;:emoji: | 123456789 | Rolle 1&#10;:wave: | 987654321 | Rolle 2"
rows={6}
value={reactionDraft.entries}
onChange={(e) => setReactionDraft((s) => ({ ...s, entries: e.target.value }))}
/>
<p className="mt-1 text-xs text-muted">
Pro Zeile: Emoji | Role ID | Label (optional) | Beschreibung (optional)
</p>
</TextField>
<div className="mt-2 flex flex-col gap-2">
{reactionDraft.entries.map((entry, i) => {
const role = roles.find((r) => r.id === entry.roleId);
return (
<div key={i} className="bg-default-soft flex items-center gap-2 rounded-xl py-1.5 pl-3 pr-1.5 text-sm">
<span>{entry.emoji}</span>
{role?.color && <span className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: role.color }} />}
<span className="min-w-0 flex-1 truncate">{entry.label || role?.name || entry.roleId}</span>
<button
type="button"
className="flex size-5 shrink-0 items-center justify-center rounded-full text-muted hover:bg-danger-soft hover:text-danger"
onClick={() => removeEntry(i)}
>
<X size={12} />
</button>
</div>
);
})}
{!reactionDraft.entries.length && <p className="text-xs text-muted">Noch keine Einträge hinzugefügt.</p>}
</div>
<Button variant="primary" onPress={saveReactionRole}>
<div className="mt-3 flex flex-col gap-2 rounded-xl border border-border p-3">
<div className="flex gap-2">
<Input
className="w-16 shrink-0"
placeholder="😀"
value={entryDraft.emoji}
onChange={(e) => setEntryDraft((s) => ({ ...s, emoji: e.target.value }))}
/>
<RoleSelect
options={roles}
value={entryDraft.roleId}
onChange={(id) => setEntryDraft((s) => ({ ...s, roleId: id }))}
placeholder="Rolle wählen"
/>
</div>
<Input
placeholder="Label (optional, z.B. Gamer)"
value={entryDraft.label}
onChange={(e) => setEntryDraft((s) => ({ ...s, label: e.target.value }))}
/>
<Button size="sm" variant="tertiary" onPress={addEntry} isDisabled={!entryDraft.emoji.trim() || !entryDraft.roleId}>
<Plus size={14} /> Eintrag hinzufügen
</Button>
</div>
</div>
<Button variant="primary" onPress={saveReactionRole} isDisabled={!reactionDraft.entries.length || !reactionDraft.channelId}>
<Save size={16} /> Reaction Role speichern
</Button>
</CardContent>