Skip to content

Commit c4dba12

Browse files
rorarclaude
andcommitted
feat(ui): shared LocationBadge component + fix SSE logs hanging
LocationBadge (src/components/ui/location-badge.tsx): Shared component for rendering location codes with country flags. Resolves NUTS codes via getLocationLabel, shows flag SVGs. Used by: AutomationList, AutomationDetail, AutomationWizard review. SSE Logs fix (api/automations/[id]/logs/route.ts): Stream now closes immediately when no active run exists instead of polling for 10 minutes. Prevents Firefox "Blocked" connection state. Replaced inline location rendering in 3 files with <LocationBadge />. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2a254d0 commit c4dba12

5 files changed

Lines changed: 70 additions & 43 deletions

File tree

src/app/api/automations/[id]/logs/route.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,24 @@ export async function GET(
6767
completedAt: store.completedAt,
6868
});
6969
controller.enqueue(encoder.encode(`data: ${initialData}\n\n`));
70+
71+
// If run already completed, close the stream immediately
72+
if (!store.isRunning && store.completedAt) {
73+
cleanup();
74+
return;
75+
}
7076
} else {
7177
controller.enqueue(
7278
encoder.encode(
7379
`data: ${JSON.stringify({ logs: [], isRunning: false })}\n\n`,
7480
),
7581
);
82+
// No active run and no stored logs — close immediately to prevent hanging
83+
cleanup();
84+
return;
7685
}
7786

78-
// Poll for new logs every second
87+
// Poll for new logs every second (only reached when a run is active)
7988
const interval = setInterval(() => {
8089
if (isClosed) return;
8190
const currentStore = automationLogger.getStore(automationId);
@@ -92,6 +101,9 @@ export async function GET(
92101
if (!currentStore.isRunning && currentStore.completedAt) {
93102
cleanup();
94103
}
104+
} else {
105+
// Store was cleared while streaming — close gracefully
106+
cleanup();
95107
}
96108
}, 1000);
97109

src/app/dashboard/automations/[id]/page.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { RunHistoryList } from "@/components/automations/RunHistoryList";
4444
import { LogsTab } from "@/components/automations/LogsTab";
4545
import Loading from "@/components/Loading";
4646
import { parseKeywords, parseLocations } from "@/utils/automation.utils";
47+
import { LocationBadge } from "@/components/ui/location-badge";
4748

4849
export default function AutomationDetailPage() {
4950
const params = useParams();
@@ -222,12 +223,13 @@ export default function AutomationDetailPage() {
222223
</div>
223224
<div className="flex items-center gap-1.5 flex-wrap">
224225
<span className="font-medium text-foreground">Location:</span>
225-
{parseLocations(automation.location)
226-
.map((loc: string) => (
227-
<Badge key={loc} variant="secondary" className="text-xs">
228-
{loc}
229-
</Badge>
230-
))}
226+
{parseLocations(automation.location).map((code) => (
227+
<LocationBadge
228+
key={code}
229+
code={code}
230+
resolve={automation.jobBoard === "eures" || automation.jobBoard === "arbeitsagentur"}
231+
/>
232+
))}
231233
</div>
232234
</div>
233235
</div>

src/components/automations/AutomationList.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
resumeAutomation,
5050
} from "@/actions/automation.actions";
5151
import Link from "next/link";
52-
import { getLocationLabel } from "@/lib/connector/job-discovery/modules/eures/countries";
52+
import { LocationBadge } from "@/components/ui/location-badge";
5353
import { parseKeywords, parseLocations } from "@/utils/automation.utils";
5454

5555
interface AutomationListProps {
@@ -66,15 +66,10 @@ const PAUSE_REASON_KEYS: Record<AutomationPauseReason, string> = {
6666
cb_escalation: "automations.pauseReasonCbEscalation",
6767
};
6868

69-
/** Resolve a stored location string to readable labels */
70-
function resolveLocations(location: string, jobBoard: string): string[] {
69+
/** Parse location string into raw codes for LocationBadge rendering */
70+
function getLocationCodes(location: string): string[] {
7171
if (!location) return [];
72-
const codes = parseLocations(location);
73-
// Only resolve EURES / arbeitsagentur codes; for other boards, return as-is
74-
if (jobBoard === "eures" || jobBoard === "arbeitsagentur") {
75-
return codes.map((code) => getLocationLabel(code));
76-
}
77-
return codes;
72+
return parseLocations(location);
7873
}
7974

8075
export function AutomationList({
@@ -162,7 +157,8 @@ export function AutomationList({
162157
const isLoading = loadingAction === automation.id;
163158
const resumeMissing = !automation.resume;
164159
const keywordChips = parseKeywords(automation.keywords);
165-
const locationLabels = resolveLocations(automation.location, automation.jobBoard);
160+
const locationCodes = getLocationCodes(automation.location);
161+
const isEures = automation.jobBoard === "eures" || automation.jobBoard === "arbeitsagentur";
166162

167163
return (
168164
<Link
@@ -222,10 +218,8 @@ export function AutomationList({
222218
</div>
223219
<div className="flex items-center gap-1.5 flex-wrap">
224220
<span className="font-medium text-foreground">{t("automations.locationLabel")}:</span>
225-
{locationLabels.map((label, idx) => (
226-
<Badge key={idx} variant="secondary" className="text-xs">
227-
{label}
228-
</Badge>
221+
{locationCodes.map((code, idx) => (
222+
<LocationBadge key={idx} code={code} resolve={isEures} />
229223
))}
230224
</div>
231225
{automation.resume && (

src/components/automations/AutomationWizard.tsx

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { ConnectorType } from "@/lib/connector/manifest";
4747
import { toast } from "@/components/ui/use-toast";
4848
import { useTranslations } from "@/i18n";
4949
import type { AutomationWithResume } from "@/models/automation.model";
50-
import { AlertTriangle, Check, CheckCircle2, ChevronLeft, ChevronRight, ChevronsUpDown, HelpCircle, Loader2, Play } from "lucide-react";
50+
import { AlertTriangle, Check, CheckCircle2, ChevronLeft, ChevronRight, ChevronsUpDown, Loader2, Play } from "lucide-react";
5151
import {
5252
Popover,
5353
PopoverContent,
@@ -64,9 +64,8 @@ import {
6464
import { cn } from "@/lib/utils";
6565
import { EuresOccupationCombobox } from "@/components/automations/EuresOccupationCombobox";
6666
import { EuresLocationCombobox } from "@/components/automations/EuresLocationCombobox";
67-
import { getLocationLabel, getCountryCode } from "@/lib/connector/job-discovery/modules/eures/countries";
6867
import { Badge } from "@/components/ui/badge";
69-
import Image from "next/image";
68+
import { LocationBadge } from "@/components/ui/location-badge";
7069
import { parseKeywords, parseLocations } from "@/utils/automation.utils";
7170

7271
interface Resume {
@@ -754,25 +753,9 @@ export function AutomationWizard({
754753
<span className="text-muted-foreground">{t("automations.reviewLocation")}</span>
755754
{jobBoard === "eures" && form.getValues("location") ? (
756755
<div className="flex flex-wrap gap-1 justify-end max-w-[60%]">
757-
{parseLocations(form.getValues("location")).map((trimmed) => {
758-
const isNS = trimmed.toLowerCase() === "ns" || trimmed.toLowerCase().endsWith("-ns");
759-
return (
760-
<Badge key={trimmed} variant="secondary" className="gap-1">
761-
{isNS ? (
762-
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground" />
763-
) : (
764-
<Image
765-
src={`/flags/${getCountryCode(trimmed)}.svg`}
766-
alt={trimmed}
767-
className="h-3.5 w-3.5"
768-
width={14}
769-
height={14}
770-
/>
771-
)}
772-
{getLocationLabel(trimmed)}
773-
</Badge>
774-
);
775-
})}
756+
{parseLocations(form.getValues("location")).map((code) => (
757+
<LocationBadge key={code} code={code} />
758+
))}
776759
</div>
777760
) : (
778761
<span className="font-medium">{form.getValues("location") || "-"}</span>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
import { Badge } from "@/components/ui/badge";
5+
import { getLocationLabel, getCountryCode } from "@/lib/connector/job-discovery/modules/eures/countries";
6+
7+
interface LocationBadgeProps {
8+
code: string;
9+
/** If true, resolve NUTS code to label via getLocationLabel. Default: true */
10+
resolve?: boolean;
11+
className?: string;
12+
}
13+
14+
/**
15+
* Renders a location code as a Badge with country flag.
16+
* Resolves NUTS codes (de1 -> "DE1: Baden-Württemberg") and shows flag SVGs.
17+
*/
18+
export function LocationBadge({ code, resolve = true, className }: LocationBadgeProps) {
19+
const label = resolve ? getLocationLabel(code) : code;
20+
const countryCode = getCountryCode(code);
21+
22+
return (
23+
<Badge variant="secondary" className={`text-xs gap-1 ${className ?? ""}`}>
24+
{countryCode && (
25+
<Image
26+
src={`/flags/${countryCode.toLowerCase()}.svg`}
27+
alt={countryCode}
28+
width={14}
29+
height={14}
30+
className="h-3.5 w-3.5"
31+
/>
32+
)}
33+
{label}
34+
</Badge>
35+
);
36+
}

0 commit comments

Comments
 (0)