refactor : simplify codebase and fix critical issues

Backend:
- Add MCP Serializer to centralize entity-to-array conversion (~300 lines deduped)
- Fix race condition in task/ticket number generation (SELECT FOR UPDATE + transaction)
- Add unique constraint on task (project_id, number) with migration
- Fix MIME type validation: use server-detected finfo instead of client-supplied type
- Add allowlist of permitted MIME types for uploads
- Fix TaskDocumentDownloadController: allow ROLE_CLIENT access, add priority:1
- Fix notification sent even when ticket status unchanged
- Remove redundant exception constructors
- Simplify services (BookStackApi double fetch, TokenEncryptor, GiteaApi)
- Consolidate duplicate checks in processors

Frontend:
- Fix useApi isHandlingUnauthorized scope (module-level to prevent double 401 redirect)
- Fix client-tickets toast key copy-paste bug
- Merge duplicated tasks service methods (getByProject + getByProjectArchived)
- Extract shared uploadWithRelation helper in task-documents service
- Extract formatFileSize utility from duplicated component code
- Extract status transition logic into useClientTicketHelpers composable
- Remove dead code (unused router, handleLogout, empty script blocks)
- Merge duplicate watchers and onMounted calls
- Normalize arrow functions to function declarations per convention

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 22:09:16 +01:00
parent a5144443a4
commit e4fc34b90f
52 changed files with 662 additions and 569 deletions
+22 -22
View File
@@ -59,7 +59,7 @@ final class BookStackApiService
* Search for pages and books within a specific shelf.
*
* Algorithm:
* 1. Fetch the shelf's book IDs
* 1. Fetch the shelf data (book IDs + slugs)
* 2. Run two search queries (one for pages, one for books)
* 3. Filter results: pages must belong to a book on the shelf, books must be on the shelf
*
@@ -67,17 +67,27 @@ final class BookStackApiService
*/
public function searchInShelf(int $shelfId, string $query): array
{
$bookIds = $this->getShelfBookIds($shelfId);
$shelfData = $this->request('GET', sprintf('/api/shelves/%d', $shelfId));
$books = $shelfData['books'] ?? [];
if (empty($bookIds)) {
if (empty($books)) {
return [];
}
$bookIds = array_map(static fn (array $book): int => $book['id'], $books);
$bookSlugs = [];
foreach ($books as $book) {
$bookSlugs[$book['id']] = $book['slug'] ?? '';
}
// Update cache for getShelfBookIds
$this->shelfBookCache[$shelfId] = $bookIds;
$config = $this->getConfiguration();
$baseUrl = rtrim($config->getUrl() ?? '', '/');
$trimmed = trim($query);
// BookStack search API accepts {type:X} for one type at a time run two queries
// BookStack search API accepts {type:X} for one type at a time -- run two queries
$pageResults = $this->request('GET', '/api/search', [
'query' => ['query' => $trimmed.' {type:page}', 'count' => 50],
]);
@@ -87,13 +97,6 @@ final class BookStackApiService
$allResults = array_merge($pageResults['data'] ?? [], $bookResults['data'] ?? []);
// Build a map of bookId → bookSlug for URL construction
$shelfData = $this->request('GET', sprintf('/api/shelves/%d', $shelfId));
$bookSlugs = [];
foreach ($shelfData['books'] ?? [] as $book) {
$bookSlugs[$book['id']] = $book['slug'] ?? '';
}
$filtered = [];
foreach ($allResults as $item) {
$type = $item['type'] ?? '';
@@ -101,23 +104,20 @@ final class BookStackApiService
if ('page' === $type) {
$bookId = $item['book_id'] ?? 0;
if (in_array($bookId, $bookIds, true)) {
$bookSlug = $bookSlugs[$bookId] ?? '';
$filtered[] = [
'id' => $item['id'],
'type' => 'page',
'name' => $item['name'] ?? '',
'url' => $baseUrl.'/books/'.$bookSlug.'/page/'.$item['slug'],
];
}
} elseif ('book' === $type) {
if (in_array($item['id'], $bookIds, true)) {
$filtered[] = [
'id' => $item['id'],
'type' => 'book',
'name' => $item['name'] ?? '',
'url' => $baseUrl.'/books/'.$item['slug'],
'url' => $baseUrl.'/books/'.($bookSlugs[$bookId] ?? '').'/page/'.$item['slug'],
];
}
} elseif ('book' === $type && in_array($item['id'], $bookIds, true)) {
$filtered[] = [
'id' => $item['id'],
'type' => 'book',
'name' => $item['name'] ?? '',
'url' => $baseUrl.'/books/'.$item['slug'],
];
}
}