Fix case repo sync + auto-create Gitea repos + add sync indicator
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m30s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m30s
- auto-sync-cases.sh: fix broken directory scan (was looking for
status subdirs that don't exist), fix env var word-splitting bug,
add safe.directory handling and error logging
- cases.py: auto-create Gitea repo on case_create, fix
documents/original → documents/originals naming mismatch
- app.py: add GET /api/cases/{case_number}/git-status endpoint
- web-ui: add SyncIndicator component in case header showing
sync status (synced/pending/no remote) with last commit time
- pyproject.toml: add httpx dependency
- CLAUDE.md: update Paperclip wakeup API docs
- settings page: switch tag input from Select to free-text with datalist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ import {
|
||||
import { APPEAL_SUBTYPES } from "@/lib/practice-area";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const TAG_SUGGESTIONS = APPEAL_SUBTYPES.filter((s) => s.value !== "unknown");
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { data: mappings, isPending: loadingMappings } = useTagMappings();
|
||||
const { data: companies, isPending: loadingCompanies } = usePaperclipCompanies();
|
||||
@@ -35,9 +37,9 @@ export default function SettingsPage() {
|
||||
const [tagLabel, setTagLabel] = useState("");
|
||||
const [companyId, setCompanyId] = useState("");
|
||||
|
||||
function handleTagChange(value: string) {
|
||||
function handleTagInput(value: string) {
|
||||
setTag(value);
|
||||
const match = APPEAL_SUBTYPES.find((s) => s.value === value);
|
||||
const match = TAG_SUGGESTIONS.find((s) => s.value === value);
|
||||
if (match) setTagLabel(match.label);
|
||||
}
|
||||
|
||||
@@ -137,22 +139,22 @@ export default function SettingsPage() {
|
||||
<div className="flex flex-wrap items-end gap-3 mb-5 p-4 rounded-md bg-rule-soft/40 border border-rule">
|
||||
<div className="flex flex-col gap-1.5 min-w-[180px]">
|
||||
<label className="text-[0.72rem] text-ink-muted">
|
||||
תגית (appeal subtype)
|
||||
תגית
|
||||
</label>
|
||||
<Select value={tag} onValueChange={handleTagChange}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="בחר סוג ערר" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{APPEAL_SUBTYPES.filter((s) => s.value !== "unknown").map(
|
||||
(s) => (
|
||||
<SelectItem key={s.value} value={s.value}>
|
||||
{s.label}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
list="tag-suggestions"
|
||||
value={tag}
|
||||
onChange={(e) => handleTagInput(e.target.value)}
|
||||
placeholder="סוג ערר או תגית חופשית"
|
||||
className="w-[220px]"
|
||||
/>
|
||||
<datalist id="tag-suggestions">
|
||||
{TAG_SUGGESTIONS.map((s) => (
|
||||
<option key={s.value} value={s.value}>
|
||||
{s.label}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5 min-w-[140px]">
|
||||
|
||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { StatusBadge } from "@/components/cases/status-badge";
|
||||
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
||||
import {
|
||||
PRACTICE_AREA_LABELS,
|
||||
APPEAL_SUBTYPE_LABELS,
|
||||
@@ -71,6 +72,10 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
|
||||
עודכן
|
||||
</dt>
|
||||
<dd className="text-ink-soft tabular-nums">{formatDate(data?.updated_at)}</dd>
|
||||
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
||||
סנכרון
|
||||
</dt>
|
||||
<dd><SyncIndicator caseNumber={data?.case_number} /></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
62
web-ui/src/components/cases/sync-indicator.tsx
Normal file
62
web-ui/src/components/cases/sync-indicator.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useGitStatus } from "@/lib/api/cases";
|
||||
import { CheckCircle2, AlertCircle, Clock, CloudOff } from "lucide-react";
|
||||
|
||||
function formatRelative(iso: string): string {
|
||||
const diff = Date.now() - new Date(iso).getTime();
|
||||
const mins = Math.floor(diff / 60_000);
|
||||
if (mins < 1) return "עכשיו";
|
||||
if (mins < 60) return `לפני ${mins} דק׳`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return `לפני ${hours} שע׳`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `לפני ${days} ימים`;
|
||||
}
|
||||
|
||||
export function SyncIndicator({ caseNumber }: { caseNumber?: string }) {
|
||||
const { data, isLoading } = useGitStatus(caseNumber);
|
||||
|
||||
if (isLoading || !data) return null;
|
||||
|
||||
if (data.error === "no_repo") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-[0.7rem] text-ink-muted/60" title="אין ריפו מקומי">
|
||||
<CloudOff className="w-3 h-3" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const synced = data.synced;
|
||||
const pending = data.dirty_files + data.commits_ahead;
|
||||
|
||||
let Icon = synced ? CheckCircle2 : pending > 0 ? Clock : AlertCircle;
|
||||
let color = synced
|
||||
? "text-success"
|
||||
: pending > 0
|
||||
? "text-warn"
|
||||
: "text-ink-muted";
|
||||
let label = synced
|
||||
? "מסונכרן"
|
||||
: pending > 0
|
||||
? `${pending} שינויים ממתינים`
|
||||
: "לא מחובר";
|
||||
|
||||
if (!data.has_remote) {
|
||||
Icon = CloudOff;
|
||||
color = "text-ink-muted/60";
|
||||
label = "אין remote";
|
||||
}
|
||||
|
||||
const time = data.last_commit_time ? formatRelative(data.last_commit_time) : null;
|
||||
const tooltip = [label, time ? `commit אחרון: ${time}` : null, data.last_commit_msg]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 text-[0.7rem] ${color}`} title={tooltip}>
|
||||
<Icon className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">{time ?? label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -145,6 +145,28 @@ export function useUpdateCase(caseNumber: string | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
export type GitSyncStatus = {
|
||||
synced: boolean;
|
||||
has_remote: boolean;
|
||||
remote_url?: string | null;
|
||||
dirty_files: number;
|
||||
commits_ahead: number;
|
||||
last_commit_time?: string | null;
|
||||
last_commit_msg?: string | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export function useGitStatus(caseNumber: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...casesKeys.all, "git-status", caseNumber ?? ""] as const,
|
||||
queryFn: ({ signal }) =>
|
||||
apiRequest<GitSyncStatus>(`/api/cases/${caseNumber}/git-status`, { signal }),
|
||||
enabled: Boolean(caseNumber),
|
||||
staleTime: 30_000,
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorkflowStatus(caseNumber: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...casesKeys.all, "workflow", caseNumber ?? ""] as const,
|
||||
|
||||
Reference in New Issue
Block a user