feat(case-files): פאנל "קבצי-התיק" בטאב טיוטות — גישה מה-UI לכל תתי-התיקיות
היו"ר לא יכל לגשת לטיוטה שהסוכן שמר בדיסק (interim-decision-draft.md) אלא ב-SSH.
מימוש מוקאפ X17 מאושר (18m): פאנל דפדפן-קבצים בטאב "טיוטות והערות".
Backend (web/app.py) — הכללה של ה-endpoints הקיימים ל**כל** תת-תיקייה:
- `_case_file_folders` — מונה כל תת-תיקיית-תוכן תחת התיק (מקור/מחולץ/מחקר/הגהה/
טיוטות/גיבויים), רקורסיה שכבה אחת לתוך documents/, מדלג על מוסתרות (.git/.claude)
ו-thumbnails. `_resolve_case_file` מאמת folder-key מול הרשימה + חוסם path-traversal.
- `/local-files` מחזיר את כל התיקיות (כולל ריקות, עם files=[]); `/local-files/{folder}/{filename}`
מגיש מכל תיקייה עם media-type לפי סיומת.
Frontend:
- `lib/api/case-files.ts` — useCaseFiles / useCaseFileText / caseFileUrl.
- `CaseFilesBrowser` — עץ-תיקיות (אקורדיון, ריקות מסומנות "ריק") + viewer מובנה
ל-md/txt (Markdown הקיים) + הורדה לכל קובץ. נטען בטאב "טיוטות והערות".
אין response_model → אין תלות ב-api:types (מודול טיפוסים בכתב-יד).
Invariants: G1, G2 (אין מסלול-קבצים מקביל — מכליל את הקיים), אבטחת-נתיב.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
web-ui/src/lib/api/case-files.ts
Normal file
66
web-ui/src/lib/api/case-files.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Case files API — browse & read every content subfolder of a case on disk.
|
||||
*
|
||||
* Surfaces the agent-produced work files (drafts, OCR text, research, originals…)
|
||||
* that previously lived only on the server filesystem. Backed by the pre-existing
|
||||
* `/local-files` endpoints, generalized to all subfolders (path-safe on the
|
||||
* server via `_resolve_case_file`).
|
||||
*/
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiRequest } from "./client";
|
||||
|
||||
export type CaseFile = {
|
||||
filename: string;
|
||||
size: number;
|
||||
/** Unix epoch seconds (float) of last modification. */
|
||||
modified_at: number;
|
||||
};
|
||||
|
||||
export type CaseFileFolder = {
|
||||
/** Opaque folder key for the read endpoint (e.g. "documents__research"). */
|
||||
key: string;
|
||||
/** Leaf folder name (e.g. "research"). */
|
||||
name: string;
|
||||
/** Case-relative path (e.g. "documents/research"). */
|
||||
path: string;
|
||||
files: CaseFile[];
|
||||
};
|
||||
|
||||
export type CaseFilesResponse = { folders: CaseFileFolder[] };
|
||||
|
||||
export function useCaseFiles(caseNumber: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ["case-files", caseNumber ?? ""],
|
||||
queryFn: () =>
|
||||
apiRequest<CaseFilesResponse>(`/api/cases/${caseNumber}/local-files`),
|
||||
enabled: !!caseNumber,
|
||||
});
|
||||
}
|
||||
|
||||
/** Direct URL to a single file — use as an <a href> for download/open. */
|
||||
export function caseFileUrl(
|
||||
caseNumber: string,
|
||||
folderKey: string,
|
||||
filename: string,
|
||||
): string {
|
||||
return `/api/cases/${encodeURIComponent(caseNumber)}/local-files/${encodeURIComponent(folderKey)}/${encodeURIComponent(filename)}`;
|
||||
}
|
||||
|
||||
/** Fetch a text file's contents (md/txt/json) for the inline viewer. */
|
||||
export function useCaseFileText(
|
||||
caseNumber: string | undefined,
|
||||
folderKey: string | undefined,
|
||||
filename: string | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ["case-file-text", caseNumber ?? "", folderKey ?? "", filename ?? ""],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(caseFileUrl(caseNumber!, folderKey!, filename!));
|
||||
if (!res.ok) throw new Error(`שגיאה בטעינת הקובץ (${res.status})`);
|
||||
return res.text();
|
||||
},
|
||||
enabled: !!caseNumber && !!folderKey && !!filename,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user