Backup #282
| ID | 282 |
| Dateipfad | /var/www/dev.campus.systemische-tools.de/src/Controller/ChatController.php |
| Version | 64 |
| Typ |
modified |
| Größe | 23.3 KB |
| Hash | 6c206265dd9aa98601d17a82724619860f75cfab11b81ff34f6ded982a48aba9 |
| Datum | 2025-12-22 02:19:25 |
| Geändert von | claude-code-hook |
| Grund | Claude Code Pre-Hook Backup vor Edit-Operation |
| Datei existiert |
Ja
|
Dateiinhalt
<?php
namespace Controller;
use Framework\Controller;
use Infrastructure\AI\AIConfig;
use Infrastructure\AI\ChatService;
use Infrastructure\AI\ModelConfig;
use Infrastructure\Persistence\CollectionRepository;
use Infrastructure\Persistence\ContentConfigRepository;
use Infrastructure\Validation\CollectionValidator;
use UseCases\Chat\SendChatMessageUseCase;
class ChatController extends Controller
{
private ChatService $chatService;
private CollectionRepository $collectionRepository;
private CollectionValidator $collectionValidator;
private SendChatMessageUseCase $sendMessageUseCase;
private \PDO $db;
/** @var array<array<string,mixed>>|null Cached collections list */
private ?array $collectionsCache = null;
public function __construct()
{
$config = AIConfig::fromCredentialsFile();
$this->chatService = $config->createChatService();
$this->collectionRepository = new CollectionRepository();
$this->collectionValidator = new CollectionValidator($this->collectionRepository);
$this->sendMessageUseCase = new SendChatMessageUseCase($this->chatService);
$this->db = $this->initializeDatabase();
}
/**
* GET /chat
* Show chat interface with session list, create new session if none
*/
public function index(): void
{
$sessions = $this->getSessions();
// Create new session and redirect
$uuid = $this->createSession();
header('Location: /chat/' . $uuid);
exit;
}
/**
* GET /chat/{uuid}
* Show specific chat session
*/
public function show(string $uuid): void
{
$session = $this->getSession($uuid);
if ($session === null) {
header('Location: /chat');
exit;
}
$messages = $this->getMessages($session['id']);
$sessions = $this->getSessions();
$authorProfiles = $this->getAuthorProfiles();
$systemPrompts = $this->getSystemPrompts();
$collections = $this->getAvailableCollections();
$this->view('chat.index', [
'title' => $session['title'] ?? 'KI-Chat',
'session' => $session,
'messages' => $messages,
'sessions' => $sessions,
'authorProfiles' => $authorProfiles,
'systemPrompts' => $systemPrompts,
'collections' => $collections,
'models' => ModelConfig::getAll(),
'defaultModel' => ModelConfig::DEFAULT_MODEL,
]);
}
/**
* GET /chat/sessions (HTMX partial)
* Return session list HTML
*/
public function sessionList(): void
{
$sessions = $this->getSessions();
$this->view('chat.partials.session-list', [
'sessions' => $sessions,
'currentUuid' => $_GET['current'] ?? null,
]);
}
/**
* POST /chat/{uuid}/message (HTMX)
* Process message and return HTML response
*/
public function message(string $uuid): void
{
$session = $this->getSession($uuid);
if ($session === null) {
$this->view('chat.partials.error', ['error' => 'Session nicht gefunden.']);
return;
}
$question = trim($_POST['message'] ?? '');
$model = $this->validateModel($_POST['model'] ?? $session['model']);
$sessionCollections = json_decode($session['collections'] ?? '["documents"]', true) ?: ['documents'];
$collections = $this->validateCollections($_POST['collections'] ?? $sessionCollections);
$contextLimit = $this->validateContextLimit((int) ($_POST['context_limit'] ?? $session['context_limit'] ?? 5));
$authorProfileId = $this->validateAuthorProfileId((int) ($_POST['author_profile_id'] ?? $session['author_profile_id'] ?? 0));
$systemPromptId = (int) ($_POST['system_prompt_id'] ?? $session['system_prompt_id'] ?? 1);
$temperature = $this->validateTemperature((float) ($_POST['temperature'] ?? $session['temperature'] ?? 0.7));
$maxTokens = $this->validateMaxTokens((int) ($_POST['max_tokens'] ?? $session['max_tokens'] ?? 4096));
// Update session if settings changed
$currentLimit = (int) ($session['context_limit'] ?? 5);
$currentProfileId = (int) ($session['author_profile_id'] ?? 0);
$currentTemperature = (float) ($session['temperature'] ?? 0.7);
$currentMaxTokens = (int) ($session['max_tokens'] ?? 4096);
$collectionsJson = json_encode($collections);
if ($model !== $session['model'] || $collectionsJson !== ($session['collections'] ?? '["documents"]') || $contextLimit !== $currentLimit || $authorProfileId !== $currentProfileId || $temperature !== $currentTemperature || $maxTokens !== $currentMaxTokens) {
$this->updateSessionSettings($session['id'], $model, $collections, $contextLimit, $authorProfileId, $temperature, $maxTokens);
}
if ($question === '') {
$this->view('chat.partials.error', ['error' => 'Bitte gib eine Frage ein.']);
return;
}
// Validate collection compatibility (vector dimensions)
if (!empty($collections)) {
$compatibility = $this->validateCollectionCompatibility($collections);
if (!$compatibility['valid']) {
$this->view('chat.partials.error', [
'error' => 'Collection-Fehler: ' . $compatibility['error'],
'details' => 'Bitte wähle nur Collections mit gleichem Embedding-Modell.',
]);
return;
}
}
// Execute UseCase
$response = $this->sendMessageUseCase->execute(
sessionUuid: $uuid,
message: $question,
model: $model,
collections: $collections,
contextLimit: $contextLimit,
authorProfileId: $authorProfileId,
systemPromptId: $systemPromptId,
temperature: $temperature,
maxTokens: $maxTokens
);
if ($response->hasError()) {
$this->view('chat.partials.error', ['error' => $response->getError()]);
return;
}
$this->renderResponse($question, $response->toArray(), $model);
}
/**
* POST /chat/{uuid}/title (HTMX)
* Update session title
*/
public function updateTitle(string $uuid): void
{
$session = $this->getSession($uuid);
if ($session === null) {
$this->notFound('Session nicht gefunden');
}
$title = trim($_POST['title'] ?? '');
// Validate title
if ($title === '') {
$title = 'Neuer Chat';
}
// Max 100 characters
$title = mb_substr($title, 0, 100);
// Save to database
$this->updateSessionTitle($session['id'], $title);
// Return updated title HTML
echo htmlspecialchars($title);
}
/**
* POST /chat/{uuid}/system-prompt (HTMX)
* Update session system prompt
*/
public function systemPrompt(string $uuid): void
{
$session = $this->getSession($uuid);
if ($session === null) {
$this->notFound('Session nicht gefunden');
}
$systemPrompt = trim($_POST['system_prompt'] ?? '');
// Validate and sanitize system prompt
$systemPrompt = $this->validateSystemPrompt($systemPrompt);
// Save to database
$this->updateSystemPrompt($session['id'], $systemPrompt);
// Return success message
$this->view('chat.partials.success', ['message' => 'System-Prompt gespeichert.']);
}
/**
* GET /chat/{uuid}/system-prompt (HTMX)
* Get system prompt form
*/
public function getSystemPrompt(string $uuid): void
{
$session = $this->getSession($uuid);
if ($session === null) {
$this->notFound('Session nicht gefunden');
}
$defaultPrompt = $this->getDefaultSystemPrompt();
$currentPrompt = $session['system_prompt'] ?? '';
$this->view('chat.partials.system-prompt-modal', [
'session' => $session,
'currentPrompt' => $currentPrompt,
'defaultPrompt' => $defaultPrompt,
]);
}
/**
* DELETE /chat/{uuid}
* Delete a session
*/
public function delete(string $uuid): void
{
$session = $this->getSession($uuid);
if ($session !== null) {
$stmt = $this->db->prepare('DELETE FROM chat_sessions WHERE id = ?');
$stmt->execute([$session['id']]);
}
// Return success for HTMX
header('HX-Redirect: /chat');
echo 'OK';
}
// ========== Private Helper Methods ==========
/**
* Initialize database connection
*/
private function initializeDatabase(): \PDO
{
return \Infrastructure\Config\DatabaseFactory::content();
}
/**
* Create a new chat session
*/
private function createSession(): string
{
$uuid = $this->generateUuid();
$stmt = $this->db->prepare(
'INSERT INTO chat_sessions (uuid, model, collections, context_limit) VALUES (?, ?, ?, ?)'
);
$stmt->execute([$uuid, 'claude-opus-4-5-20251101', '["documents"]', 5]);
return $uuid;
}
/**
* Get session by UUID
*/
private function getSession(string $uuid): ?array
{
$stmt = $this->db->prepare('SELECT * FROM chat_sessions WHERE uuid = ?');
$stmt->execute([$uuid]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
return $result !== false ? $result : null;
}
/**
* Get all sessions ordered by most recent
*/
private function getSessions(): array
{
$stmt = $this->db->query(
'SELECT s.*,
(SELECT COUNT(*) FROM chat_messages WHERE session_id = s.id) as message_count,
(SELECT COALESCE(SUM(tokens_input), 0) FROM chat_messages WHERE session_id = s.id) as total_input_tokens,
(SELECT COALESCE(SUM(tokens_output), 0) FROM chat_messages WHERE session_id = s.id) as total_output_tokens,
(SELECT COALESCE(SUM(end_microtime - start_microtime), 0) FROM chat_messages WHERE session_id = s.id AND start_microtime IS NOT NULL) as total_duration,
(SELECT model FROM chat_messages WHERE session_id = s.id AND role = "assistant" ORDER BY id DESC LIMIT 1) as last_model
FROM chat_sessions s
ORDER BY s.last_activity DESC
LIMIT 50'
);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Get author profiles from content_config table
*/
private function getAuthorProfiles(): array
{
$stmt = $this->db->query(
"SELECT id, name, slug, content as config
FROM content_config
WHERE type = 'author_profile' AND status = 'active'
ORDER BY name"
);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Get author profile by ID
*/
private function getAuthorProfile(int $profileId): ?array
{
$stmt = $this->db->prepare(
"SELECT id, name, slug, content as config
FROM content_config
WHERE id = ? AND type = 'author_profile' AND status = 'active'"
);
$stmt->execute([$profileId]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
return $result !== false ? $result : null;
}
/**
* Get system prompts from content_config table
*/
private function getSystemPrompts(): array
{
$stmt = $this->db->query(
"SELECT id, name, slug, content
FROM content_config
WHERE type = 'system_prompt' AND status = 'active'
ORDER BY name"
);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Get system prompt by ID from content_config
*/
private function getSystemPromptById(int $promptId): ?array
{
$stmt = $this->db->prepare(
"SELECT id, name, slug, content
FROM content_config
WHERE id = ? AND type = 'system_prompt' AND status = 'active'"
);
$stmt->execute([$promptId]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
return $result !== false ? $result : null;
}
/**
* Get messages for a session
*/
private function getMessages(int $sessionId): array
{
$stmt = $this->db->prepare(
'SELECT * FROM chat_messages WHERE session_id = ? ORDER BY created_at ASC'
);
$stmt->execute([$sessionId]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Update session title
*/
private function updateSessionTitle(int $sessionId, string $title): void
{
$stmt = $this->db->prepare('UPDATE chat_sessions SET title = ? WHERE id = ?');
$stmt->execute([$title, $sessionId]);
}
/**
* Generate UUID v4
*/
private function generateUuid(): string
{
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
/**
* Validate model parameter
*/
private function validateModel(string $model): string
{
return ModelConfig::validate($model);
}
/**
* Get available collections from database (cached per request)
*
* @return array<int, array<string,mixed>> List of collection data
*/
private function getAvailableCollections(): array
{
if ($this->collectionsCache === null) {
$this->collectionsCache = $this->collectionRepository->getSearchable();
// Fallback if database is empty
if ($this->collectionsCache === []) {
$this->collectionsCache = [
['collection_id' => 'documents', 'display_name' => 'Dokumente', 'points_count' => 0, 'vector_size' => 1024],
];
}
}
return $this->collectionsCache;
}
/**
* Get collection IDs only (for backwards compatibility)
*
* @return array<string>
*/
private function getAvailableCollectionIds(): array
{
return array_column($this->getAvailableCollections(), 'collection_id');
}
/**
* Validate collections array against available collections
*
* @param array<string>|string $collections Collections to validate (array or single string)
* @return array<string> Validated collections array (may be empty for no RAG)
*/
private function validateCollections(array|string $collections): array
{
$availableIds = $this->getAvailableCollectionIds();
// Handle string input (backwards compatibility)
if (is_string($collections)) {
$collections = [$collections];
}
// Filter to only valid collections
$valid = array_filter($collections, fn ($c) => in_array($c, $availableIds, true));
return array_values($valid);
}
/**
* Validate collection compatibility (vector dimensions)
*
* @param array<string> $collectionIds
* @return array{valid: bool, error: string|null}
*/
private function validateCollectionCompatibility(array $collectionIds): array
{
if (empty($collectionIds)) {
return ['valid' => true, 'error' => null];
}
$result = $this->collectionValidator->validateSelection($collectionIds);
return [
'valid' => $result->isValid(),
'error' => $result->getError(),
];
}
/**
* Validate context limit parameter against allowed values
*/
private function validateContextLimit(int $limit): int
{
$allowedLimits = [3, 5, 10, 15];
... (229 weitere Zeilen)
Vollständig herunterladen
Aktionen
Andere Versionen dieser Datei
| ID |
Version |
Typ |
Größe |
Datum |
| 2128 |
131 |
modified |
17.2 KB |
2025-12-29 15:04 |
| 2126 |
130 |
modified |
15.7 KB |
2025-12-29 15:00 |
| 2125 |
129 |
modified |
15.6 KB |
2025-12-29 15:00 |
| 2124 |
128 |
modified |
15.6 KB |
2025-12-29 15:00 |
| 2120 |
127 |
modified |
15.5 KB |
2025-12-29 09:18 |
| 2119 |
126 |
modified |
15.3 KB |
2025-12-29 09:17 |
| 2111 |
125 |
modified |
15.3 KB |
2025-12-29 08:49 |
| 2091 |
124 |
modified |
13.5 KB |
2025-12-29 00:15 |
| 2090 |
123 |
modified |
14.3 KB |
2025-12-29 00:14 |
| 2089 |
122 |
modified |
16.9 KB |
2025-12-29 00:14 |
| 2088 |
121 |
modified |
17.8 KB |
2025-12-29 00:13 |
| 2087 |
120 |
modified |
14.2 KB |
2025-12-29 00:13 |
| 1583 |
119 |
modified |
14.1 KB |
2025-12-26 21:07 |
| 1579 |
118 |
modified |
13.9 KB |
2025-12-26 20:56 |
| 1578 |
117 |
modified |
13.8 KB |
2025-12-26 20:55 |
| 1574 |
116 |
modified |
13.6 KB |
2025-12-26 20:40 |
| 1573 |
115 |
modified |
13.5 KB |
2025-12-26 20:39 |
| 1572 |
114 |
modified |
13.5 KB |
2025-12-26 20:39 |
| 1571 |
113 |
modified |
13.2 KB |
2025-12-26 20:39 |
| 1559 |
112 |
modified |
8.6 KB |
2025-12-26 20:28 |
| 1558 |
111 |
modified |
8.4 KB |
2025-12-26 20:28 |
| 1544 |
110 |
modified |
8.3 KB |
2025-12-26 20:05 |
| 1539 |
109 |
modified |
9.6 KB |
2025-12-26 20:04 |
| 1538 |
108 |
modified |
10.1 KB |
2025-12-26 20:04 |
| 1536 |
107 |
modified |
8.8 KB |
2025-12-26 19:45 |
| 1535 |
106 |
modified |
8.3 KB |
2025-12-26 19:45 |
| 1494 |
105 |
modified |
8.3 KB |
2025-12-25 17:31 |
| 1488 |
104 |
modified |
8.3 KB |
2025-12-25 17:04 |
| 1487 |
103 |
modified |
8.2 KB |
2025-12-25 17:04 |
| 1486 |
102 |
modified |
8.2 KB |
2025-12-25 17:04 |
| 1485 |
101 |
modified |
8.2 KB |
2025-12-25 17:04 |
| 1484 |
100 |
modified |
8.2 KB |
2025-12-25 17:04 |
| 1483 |
99 |
modified |
8.2 KB |
2025-12-25 17:04 |
| 1482 |
98 |
modified |
8.2 KB |
2025-12-25 17:03 |
| 1466 |
97 |
modified |
8.2 KB |
2025-12-25 17:02 |
| 1465 |
96 |
modified |
8.2 KB |
2025-12-25 17:01 |
| 1464 |
95 |
modified |
8.1 KB |
2025-12-25 17:01 |
| 1453 |
94 |
modified |
8.1 KB |
2025-12-25 17:00 |
| 1452 |
93 |
modified |
8.1 KB |
2025-12-25 17:00 |
| 1451 |
92 |
modified |
8.1 KB |
2025-12-25 17:00 |
| 1445 |
91 |
modified |
7.7 KB |
2025-12-25 17:00 |
| 1293 |
90 |
modified |
7.6 KB |
2025-12-25 13:27 |
| 1292 |
89 |
modified |
7.6 KB |
2025-12-25 13:27 |
| 1291 |
88 |
modified |
7.9 KB |
2025-12-25 13:27 |
| 1145 |
87 |
modified |
7.9 KB |
2025-12-25 09:45 |
| 1144 |
86 |
modified |
7.8 KB |
2025-12-25 09:45 |
| 1142 |
85 |
modified |
7.7 KB |
2025-12-25 09:45 |
| 1137 |
84 |
modified |
7.7 KB |
2025-12-25 09:42 |
| 1136 |
83 |
modified |
7.7 KB |
2025-12-25 09:42 |
| 1135 |
82 |
modified |
7.6 KB |
2025-12-25 09:42 |
| 685 |
81 |
modified |
7.5 KB |
2025-12-23 07:51 |
| 681 |
80 |
modified |
7.5 KB |
2025-12-23 07:44 |
| 632 |
79 |
modified |
7.8 KB |
2025-12-23 04:44 |
| 631 |
78 |
modified |
7.8 KB |
2025-12-23 04:44 |
| 630 |
77 |
modified |
7.8 KB |
2025-12-23 04:44 |
| 629 |
76 |
modified |
7.6 KB |
2025-12-23 04:44 |
| 619 |
75 |
modified |
7.7 KB |
2025-12-23 04:42 |
| 564 |
74 |
modified |
7.6 KB |
2025-12-23 03:35 |
| 551 |
73 |
modified |
7.6 KB |
2025-12-23 02:39 |
| 550 |
72 |
modified |
7.5 KB |
2025-12-23 02:39 |
| 546 |
71 |
modified |
7.4 KB |
2025-12-23 02:38 |
| 540 |
70 |
modified |
6.4 KB |
2025-12-22 22:30 |
| 539 |
69 |
modified |
6.4 KB |
2025-12-22 22:29 |
| 470 |
68 |
modified |
22.7 KB |
2025-12-22 10:36 |
| 321 |
67 |
modified |
22.7 KB |
2025-12-22 08:06 |
| 293 |
66 |
modified |
22.7 KB |
2025-12-22 08:03 |
| 283 |
65 |
modified |
23.2 KB |
2025-12-22 02:19 |
| 282 |
64 |
modified |
23.3 KB |
2025-12-22 02:19 |
| 281 |
63 |
modified |
23.4 KB |
2025-12-22 02:19 |
| 274 |
62 |
modified |
24.1 KB |
2025-12-22 02:14 |
| 273 |
61 |
modified |
25.6 KB |
2025-12-22 02:14 |
| 272 |
60 |
modified |
27.0 KB |
2025-12-22 02:14 |
| 271 |
59 |
modified |
27.4 KB |
2025-12-22 02:13 |
| 270 |
58 |
modified |
29.0 KB |
2025-12-22 02:13 |
| 269 |
57 |
modified |
28.8 KB |
2025-12-22 02:13 |
| 268 |
56 |
modified |
28.7 KB |
2025-12-22 02:12 |
| 257 |
55 |
modified |
29.0 KB |
2025-12-22 02:00 |
| 256 |
54 |
modified |
32.1 KB |
2025-12-22 01:59 |
| 255 |
53 |
modified |
32.1 KB |
2025-12-22 01:59 |
| 254 |
52 |
modified |
32.1 KB |
2025-12-22 01:59 |
| 253 |
51 |
modified |
32.1 KB |
2025-12-22 01:59 |
| 252 |
50 |
modified |
32.2 KB |
2025-12-22 01:59 |
| 251 |
49 |
modified |
32.2 KB |
2025-12-22 01:59 |
| 250 |
48 |
modified |
32.2 KB |
2025-12-22 01:59 |
| 217 |
47 |
modified |
32.2 KB |
2025-12-22 01:43 |
| 216 |
46 |
modified |
32.3 KB |
2025-12-22 01:43 |
| 215 |
45 |
modified |
32.3 KB |
2025-12-22 01:43 |
| 197 |
44 |
modified |
32.4 KB |
2025-12-21 14:39 |
| 196 |
43 |
modified |
32.4 KB |
2025-12-21 14:39 |
| 195 |
42 |
modified |
32.5 KB |
2025-12-21 14:39 |
| 183 |
41 |
modified |
31.9 KB |
2025-12-21 14:15 |
| 182 |
40 |
modified |
31.4 KB |
2025-12-21 14:15 |
| 181 |
39 |
modified |
31.0 KB |
2025-12-21 14:15 |
| 180 |
38 |
modified |
30.6 KB |
2025-12-21 14:15 |
| 169 |
37 |
modified |
30.5 KB |
2025-12-21 04:12 |
| 168 |
36 |
modified |
30.6 KB |
2025-12-21 04:12 |
| 167 |
35 |
modified |
30.6 KB |
2025-12-21 04:12 |
| 148 |
34 |
modified |
30.6 KB |
2025-12-21 02:30 |
| 147 |
33 |
modified |
30.6 KB |
2025-12-21 02:30 |
| 146 |
32 |
modified |
29.4 KB |
2025-12-21 02:30 |
| 145 |
31 |
modified |
29.4 KB |
2025-12-21 02:29 |
| 144 |
30 |
modified |
29.4 KB |
2025-12-21 02:29 |
| 143 |
29 |
modified |
29.5 KB |
2025-12-21 02:29 |
| 142 |
28 |
modified |
29.3 KB |
2025-12-21 02:16 |
| 132 |
27 |
modified |
27.8 KB |
2025-12-20 19:46 |
| 131 |
26 |
modified |
27.7 KB |
2025-12-20 19:46 |
| 130 |
25 |
modified |
26.5 KB |
2025-12-20 19:42 |
| 82 |
24 |
modified |
26.4 KB |
2025-12-20 19:13 |
| 81 |
23 |
modified |
26.3 KB |
2025-12-20 19:13 |
| 80 |
22 |
modified |
26.3 KB |
2025-12-20 19:13 |
| 79 |
21 |
modified |
25.9 KB |
2025-12-20 19:12 |
| 78 |
20 |
modified |
25.9 KB |
2025-12-20 19:12 |
| 77 |
19 |
modified |
25.9 KB |
2025-12-20 19:12 |
| 76 |
18 |
modified |
25.9 KB |
2025-12-20 19:10 |
| 75 |
17 |
modified |
25.8 KB |
2025-12-20 19:10 |
| 74 |
16 |
modified |
25.7 KB |
2025-12-20 19:10 |
| 68 |
15 |
modified |
25.9 KB |
2025-12-20 18:49 |
| 67 |
14 |
modified |
25.4 KB |
2025-12-20 18:49 |
| 66 |
13 |
modified |
25.3 KB |
2025-12-20 18:49 |
| 65 |
12 |
modified |
25.0 KB |
2025-12-20 18:49 |
| 60 |
11 |
modified |
24.5 KB |
2025-12-20 18:32 |
| 59 |
10 |
modified |
24.4 KB |
2025-12-20 18:31 |
| 58 |
9 |
modified |
24.3 KB |
2025-12-20 18:31 |
| 57 |
8 |
modified |
24.3 KB |
2025-12-20 18:31 |
| 56 |
7 |
modified |
24.0 KB |
2025-12-20 18:31 |
| 55 |
6 |
modified |
23.8 KB |
2025-12-20 18:31 |
| 40 |
5 |
modified |
24.1 KB |
2025-12-20 17:24 |
| 39 |
4 |
modified |
24.9 KB |
2025-12-20 17:24 |
| 9 |
3 |
modified |
24.9 KB |
2025-12-20 16:32 |
| 5 |
2 |
modified |
24.8 KB |
2025-12-20 16:08 |
| 4 |
1 |
modified |
24.8 KB |
2025-12-20 16:08 |
← Zurück zur Übersicht