@flowselections/google-maps-api-installatie-techniek 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist-lib/components/MapsBeheerPage.d.ts.map +1 -1
- package/dist-lib/components/MapsBeheerPage.js +27 -2
- package/dist-lib/components/maps/MapsDiagnosticsPanel.js +1 -1
- package/dist-lib/components/maps/MapsErrorsPanel.d.ts +2 -0
- package/dist-lib/components/maps/MapsErrorsPanel.d.ts.map +1 -0
- package/dist-lib/components/maps/MapsErrorsPanel.js +26 -0
- package/dist-lib/components/maps/MapsStatusPanel.d.ts +1 -1
- package/dist-lib/components/maps/MapsStatusPanel.js +1 -1
- package/dist-lib/components/settings/MapsSettingsCard.js +1 -1
- package/dist-lib/integrations/supabase/client.js +1 -1
- package/dist-lib/integrations/supabase/client.server.js +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +11 -5
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.js +59 -24
- package/dist-lib/integrations/supabase/types.d.ts +23505 -6704
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +2 -0
- package/dist-lib/lib/maps-admin.server.d.ts +9 -1
- package/dist-lib/lib/maps-admin.server.d.ts.map +1 -1
- package/dist-lib/lib/maps-admin.server.js +60 -6
- package/dist-lib/lib/maps-errors.d.ts +24 -0
- package/dist-lib/lib/maps-errors.d.ts.map +1 -0
- package/dist-lib/lib/maps-errors.js +57 -0
- package/dist-lib/lib/maps-errors.test.d.ts +2 -0
- package/dist-lib/lib/maps-errors.test.d.ts.map +1 -0
- package/dist-lib/lib/maps-errors.test.js +40 -0
- package/dist-lib/lib/maps-processor.server.d.ts +2 -0
- package/dist-lib/lib/maps-processor.server.d.ts.map +1 -1
- package/dist-lib/lib/maps-processor.server.js +117 -22
- package/dist-lib/lib/maps-processor.test.d.ts +2 -0
- package/dist-lib/lib/maps-processor.test.d.ts.map +1 -0
- package/dist-lib/lib/maps-processor.test.js +182 -0
- package/dist-lib/lib/maps.functions.d.ts +52 -8
- package/dist-lib/lib/maps.functions.d.ts.map +1 -1
- package/dist-lib/lib/maps.functions.js +24 -1
- package/package.json +8 -6
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Kaarten-module (`maps_installatie`)
|
|
2
|
+
|
|
3
|
+
Deze module verwerkt `maps_request_queue`:
|
|
4
|
+
- adressen omzetten naar coördinaten (geocoding);
|
|
5
|
+
- reistijden ophalen en opslaan in `maps_addresses` en `maps_travel_times`.
|
|
6
|
+
|
|
7
|
+
## Starten van de verwerking
|
|
8
|
+
|
|
9
|
+
- `POST /api/public/maps-cron`: andere modules roepen dit aan via `MAPS_CRON_URL`. Het endpoint gebruikt een lease.
|
|
10
|
+
- De knop "Wachtrij nu verwerken" op de beheerpagina.
|
|
11
|
+
|
|
12
|
+
## Instellingen (`maps_settings`)
|
|
13
|
+
|
|
14
|
+
| Kolom | Standaard | Betekenis |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `daily_call_limit` | 2000 | Maximum aantal externe aanroepen |
|
|
17
|
+
| `cache_ttl_days` | 30 | Hoe lang reistijden geldig blijven |
|
|
18
|
+
| `claim_via_rpc_enabled` | false | Items atomair claimen (`SKIP LOCKED`), zodat elk item één keer wordt verwerkt |
|
|
19
|
+
| `per_call_limit_enabled` | false | Daglimiet per aanroep controleren en ophogen in `maps_call_counter` |
|
|
20
|
+
| `max_attempts_enabled` / `max_attempts` | false / 3 | Maximum aantal pogingen uit de instellingen; uit = vast 3 |
|
|
21
|
+
| `stop_bij_sleutelfout_enabled` | false | Batch stoppen bij een ongeldige sleutel of een tokenfout van Central |
|
|
22
|
+
| `timezone` | Europe/Amsterdam | Daggrens van de dagteller |
|
|
23
|
+
|
|
24
|
+
Met alle schakelaars uit werkt de verwerking exact zoals in versie 1.0.x.
|
|
25
|
+
|
|
26
|
+
## Omgevingswaarden
|
|
27
|
+
|
|
28
|
+
- `SUPABASE_URL` en `SERVICE_ROLE_KEY`
|
|
29
|
+
- `CENTRAL_AUTH_URL` en `CENTRAL_AUTH_SECRET`: de Google-sleutel komt via Central.
|
|
30
|
+
|
|
31
|
+
## Tests
|
|
32
|
+
|
|
33
|
+
`npm test`. De tests gebruiken mocks en doen geen echte Google- of database-aanroepen.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MapsBeheerPage.d.ts","sourceRoot":"","sources":["../../src/components/MapsBeheerPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MapsBeheerPage.d.ts","sourceRoot":"","sources":["../../src/components/MapsBeheerPage.tsx"],"names":[],"mappings":"AA0CA,wBAAgB,cAAc,gCA8Q7B"}
|
|
@@ -4,9 +4,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
4
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
5
|
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, Switch, Tabs, TabsContent, TabsList, TabsTrigger, toast, } from "@flowselections/core";
|
|
6
6
|
import { MapPin, RefreshCw, Trash2, Timer } from "lucide-react";
|
|
7
|
-
import { clearMapsCache, getMapsOverview, getMapsStatus, processMapsQueue, purgeExpiredTravelTimes, updateMapsSettings, } from "../lib/maps.functions";
|
|
7
|
+
import { clearMapsCache, getMapsOverview, getMapsStatus, processMapsQueue, purgeExpiredTravelTimes, updateMapsSettings, } from "../lib/maps.functions.js";
|
|
8
8
|
import { MapsStatusPanel } from "./maps/MapsStatusPanel";
|
|
9
9
|
import { MapsDiagnosticsPanel } from "./maps/MapsDiagnosticsPanel";
|
|
10
|
+
import { MapsErrorsPanel } from "./maps/MapsErrorsPanel";
|
|
10
11
|
function Stat({ label, value }) {
|
|
11
12
|
return (_jsxs("div", { className: "rounded-lg border border-border bg-card p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: label }), _jsx("p", { className: "mt-1 text-2xl font-semibold text-foreground", children: value })] }));
|
|
12
13
|
}
|
|
@@ -30,14 +31,27 @@ export function MapsBeheerPage() {
|
|
|
30
31
|
const [limit, setLimit] = useState("");
|
|
31
32
|
const [fallback, setFallback] = useState(null);
|
|
32
33
|
const [provider, setProvider] = useState(null);
|
|
34
|
+
const [toggles, setToggles] = useState({});
|
|
35
|
+
const [maxAttempts, setMaxAttempts] = useState("");
|
|
36
|
+
const [timezone, setTimezone] = useState("");
|
|
33
37
|
const settings = data?.settings ?? null;
|
|
34
38
|
const ttlValue = ttl !== "" ? ttl : String(settings?.cacheTtlDays ?? 30);
|
|
35
39
|
const limitValue = limit !== "" ? limit : String(settings?.dailyCallLimit ?? 2000);
|
|
36
40
|
const fallbackValue = fallback ?? settings?.allowEstimateFallback ?? true;
|
|
37
41
|
const providerValue = provider ?? settings?.provider ?? "free";
|
|
42
|
+
const toggle = (key) => toggles[key] ?? settings?.[key] ?? false;
|
|
43
|
+
const maxAttemptsValue = maxAttempts !== "" ? maxAttempts : String(settings?.maxAttempts ?? 3);
|
|
44
|
+
const timezoneValue = timezone !== "" ? timezone : settings?.timezone ?? "Europe/Amsterdam";
|
|
45
|
+
const TOGGLES = [
|
|
46
|
+
["claimViaRpcEnabled", "Wachtrij-items veilig claimen (elk item precies één keer)"],
|
|
47
|
+
["perCallLimitEnabled", "Daglimiet per aanroep controleren"],
|
|
48
|
+
["maxAttemptsEnabled", "Maximum aantal pogingen uit instellingen gebruiken"],
|
|
49
|
+
["stopBijSleutelfoutEnabled", "Batch stoppen bij ongeldige sleutel of tokenfout"],
|
|
50
|
+
];
|
|
38
51
|
const invalidate = () => {
|
|
39
52
|
queryClient.invalidateQueries({ queryKey: ["maps-overview"] });
|
|
40
53
|
queryClient.invalidateQueries({ queryKey: ["maps-status"] });
|
|
54
|
+
queryClient.invalidateQueries({ queryKey: ["maps-errors"] });
|
|
41
55
|
};
|
|
42
56
|
const mutation = useMutation({
|
|
43
57
|
mutationFn: async (action) => {
|
|
@@ -53,16 +67,27 @@ export function MapsBeheerPage() {
|
|
|
53
67
|
dailyCallLimit: Number(limitValue),
|
|
54
68
|
allowEstimateFallback: fallbackValue,
|
|
55
69
|
provider: providerValue,
|
|
70
|
+
claimViaRpcEnabled: toggle("claimViaRpcEnabled"),
|
|
71
|
+
perCallLimitEnabled: toggle("perCallLimitEnabled"),
|
|
72
|
+
maxAttemptsEnabled: toggle("maxAttemptsEnabled"),
|
|
73
|
+
stopBijSleutelfoutEnabled: toggle("stopBijSleutelfoutEnabled"),
|
|
74
|
+
maxAttempts: Number(maxAttemptsValue),
|
|
75
|
+
timezone: timezoneValue,
|
|
76
|
+
expectedUpdatedAt: settings?.updatedAt ?? null,
|
|
56
77
|
},
|
|
57
78
|
});
|
|
58
79
|
},
|
|
59
80
|
onSuccess: () => {
|
|
60
81
|
toast.success("Actie uitgevoerd");
|
|
82
|
+
setToggles({});
|
|
83
|
+
setMaxAttempts("");
|
|
84
|
+
setTimezone("");
|
|
61
85
|
invalidate();
|
|
62
86
|
},
|
|
63
87
|
onError: (error) => {
|
|
88
|
+
console.error("[maps] actie mislukt", error);
|
|
64
89
|
toast.error(error instanceof Error ? error.message : "Er ging iets mis");
|
|
65
90
|
},
|
|
66
91
|
});
|
|
67
|
-
return (_jsx("main", { className: "flex-1 p-6", children: _jsxs(Tabs, { defaultValue: "status", className: "w-full", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "status", children: "Status & diagnose" }), _jsx(TabsTrigger, { value: "beheer", children: "Beheer" })] }), _jsxs(TabsContent, { value: "status", className: "space-y-4", children: [_jsx(MapsStatusPanel, { status: status ?? null }), _jsx(MapsDiagnosticsPanel, {})] }), _jsxs(TabsContent, { value: "beheer", className: "space-y-6", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(MapPin, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Google-koppeling" })] }), data ? (_jsx(Badge, { variant: data.connected ? "default" : "destructive", children: data.connected ? "Verbonden" : "Niet beschikbaar" })) : null] }), _jsx(CardDescription, { children: "Route- en adresgegevens worden opgehaald via de centrale auth-module." })] }), _jsxs(CardContent, { className: "space-y-2 text-sm text-muted-foreground", children: [isLoading ? _jsx("p", { children: "Laden..." }) : null, data && !data.connected ? _jsx("p", { children: data.connectionReason }) : null, settings?.lastSuccessAt ? (_jsxs("p", { children: ["Laatste geslaagde aanroep: ", new Date(settings.lastSuccessAt).toLocaleString("nl-NL")] })) : null, settings?.lastError ? (_jsxs("p", { className: "text-destructive", children: ["Laatste fout: ", settings.lastError] })) : null] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: [_jsx(Stat, { label: "Aanroepen vandaag", value: data?.elementsToday ?? 0 }), _jsx(Stat, { label: "Aanroepen deze maand", value: data?.elementsMonth ?? 0 }), data?.estimatedCostMonth != null ? (_jsx(Stat, { label: "Geschatte kosten (USD)", value: data.estimatedCostMonth })) : null, _jsx(Stat, { label: "Mislukte aanroepen", value: data?.errorsMonth ?? 0 }), _jsx(Stat, { label: "Terugvallen deze maand", value: data?.fallbacksMonth ?? 0 }), _jsx(Stat, { label: "Adressen in cache", value: data?.addressCount ?? 0 }), _jsx(Stat, { label: "Reistijden in cache", value: data?.travelTimeCount ?? 0 }), _jsx(Stat, { label: "Verlopen reistijden", value: data?.expiredCount ?? 0 }), _jsx(Stat, { label: "Wachtrij (open / mislukt)", value: `${data?.queuePending ?? 0} / ${data?.queueFailed ?? 0}` })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Instellingen" }), _jsx(CardDescription, { children: "Aanbieder, houdbaarheid, daglimiet en terugval op schatting." })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "ttl", children: "Houdbaarheid cache (dagen)" }), _jsx(Input, { id: "ttl", type: "number", min: 1, value: ttlValue, onChange: (event) => setTtl(event.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "limit", children: "Daglimiet (elementen)" }), _jsx(Input, { id: "limit", type: "number", min: 0, value: limitValue, onChange: (event) => setLimit(event.target.value) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "provider", children: "Aanbieder" }), _jsxs("select", { id: "provider", className: "h-10 w-full rounded-md border border-input bg-background px-3 text-sm sm:w-64", value: providerValue, onChange: (event) => setProvider(event.target.value === "google" ? "google" : "free"), children: [_jsx("option", { value: "free", children: "Gratis (geen kosten)" }), _jsx("option", { value: "google", children: "Google (met billing)" })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Kostenschatting wordt alleen getoond bij Google." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: "fallback", checked: fallbackValue, onCheckedChange: (checked) => setFallback(checked) }), _jsx(Label, { htmlFor: "fallback", children: "Terugvallen op postcodeschatting toestaan" })] }), _jsx(Button, { onClick: () => mutation.mutate("save"), disabled: mutation.isPending, children: "Instellingen opslaan" })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Acties" }), _jsx(CardDescription, { children: "Onderhoud van de wachtrij en de cache." })] }), _jsxs(CardContent, { className: "flex flex-wrap gap-3", children: [_jsxs(Button, { onClick: () => mutation.mutate("queue"), disabled: mutation.isPending, children: [_jsx(RefreshCw, { className: "mr-2 h-4 w-4" }), " Wachtrij nu verwerken"] }), _jsxs(Button, { variant: "outline", onClick: () => mutation.mutate("purge"), disabled: mutation.isPending, children: [_jsx(Timer, { className: "mr-2 h-4 w-4" }), " Verlopen rijen opruimen"] }), _jsxs(Button, { variant: "destructive", onClick: () => mutation.mutate("clear"), disabled: mutation.isPending, children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), " Cache legen"] })] })] })] })] }) }));
|
|
92
|
+
return (_jsx("main", { className: "flex-1 p-6", children: _jsxs(Tabs, { defaultValue: "status", className: "w-full", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "status", children: "Status & diagnose" }), _jsx(TabsTrigger, { value: "beheer", children: "Beheer" })] }), _jsxs(TabsContent, { value: "status", className: "space-y-4", children: [_jsx(MapsStatusPanel, { status: status ?? null }), _jsx(MapsDiagnosticsPanel, {})] }), _jsxs(TabsContent, { value: "beheer", className: "space-y-6", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(MapPin, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Google-koppeling" })] }), data ? (_jsx(Badge, { variant: data.connected ? "default" : "destructive", children: data.connected ? "Verbonden" : "Niet beschikbaar" })) : null] }), _jsx(CardDescription, { children: "Route- en adresgegevens worden opgehaald via de centrale auth-module." })] }), _jsxs(CardContent, { className: "space-y-2 text-sm text-muted-foreground", children: [isLoading ? _jsx("p", { children: "Laden..." }) : null, data && !data.connected ? _jsx("p", { children: data.connectionReason }) : null, settings?.lastSuccessAt ? (_jsxs("p", { children: ["Laatste geslaagde aanroep: ", new Date(settings.lastSuccessAt).toLocaleString("nl-NL")] })) : null, settings?.lastError ? (_jsxs("p", { className: "text-destructive", children: ["Laatste fout: ", settings.lastError] })) : null] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: [_jsx(Stat, { label: "Aanroepen vandaag", value: data?.elementsToday ?? 0 }), _jsx(Stat, { label: "Aanroepen deze maand", value: data?.elementsMonth ?? 0 }), data?.estimatedCostMonth != null ? (_jsx(Stat, { label: "Geschatte kosten (USD)", value: data.estimatedCostMonth })) : null, _jsx(Stat, { label: "Mislukte aanroepen", value: data?.errorsMonth ?? 0 }), _jsx(Stat, { label: "Terugvallen deze maand", value: data?.fallbacksMonth ?? 0 }), _jsx(Stat, { label: "Adressen in cache", value: data?.addressCount ?? 0 }), _jsx(Stat, { label: "Reistijden in cache", value: data?.travelTimeCount ?? 0 }), _jsx(Stat, { label: "Verlopen reistijden", value: data?.expiredCount ?? 0 }), _jsx(Stat, { label: "Wachtrij (open / mislukt)", value: `${data?.queuePending ?? 0} / ${data?.queueFailed ?? 0}` })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Instellingen" }), _jsx(CardDescription, { children: "Aanbieder, houdbaarheid, daglimiet en terugval op schatting." })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "ttl", children: "Houdbaarheid cache (dagen)" }), _jsx(Input, { id: "ttl", type: "number", min: 1, value: ttlValue, onChange: (event) => setTtl(event.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "limit", children: "Daglimiet (elementen)" }), _jsx(Input, { id: "limit", type: "number", min: 0, value: limitValue, onChange: (event) => setLimit(event.target.value) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "provider", children: "Aanbieder" }), _jsxs("select", { id: "provider", className: "h-10 w-full rounded-md border border-input bg-background px-3 text-sm sm:w-64", value: providerValue, onChange: (event) => setProvider(event.target.value === "google" ? "google" : "free"), children: [_jsx("option", { value: "free", children: "Gratis (geen kosten)" }), _jsx("option", { value: "google", children: "Google (met billing)" })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Kostenschatting wordt alleen getoond bij Google." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: "fallback", checked: fallbackValue, onCheckedChange: (checked) => setFallback(checked) }), _jsx(Label, { htmlFor: "fallback", children: "Terugvallen op postcodeschatting toestaan" })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "max-attempts", children: "Maximum aantal pogingen" }), _jsx(Input, { id: "max-attempts", type: "number", min: 1, value: maxAttemptsValue, onChange: (event) => setMaxAttempts(event.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "timezone", children: "Tijdzone daglimiet" }), _jsx(Input, { id: "timezone", value: timezoneValue, onChange: (event) => setTimezone(event.target.value) })] })] }), _jsxs("div", { className: "space-y-3", children: [TOGGLES.map(([key, label]) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: key, checked: toggle(key), onCheckedChange: (checked) => setToggles((prev) => ({ ...prev, [key]: checked })) }), _jsx(Label, { htmlFor: key, children: label })] }, key))), _jsx("p", { className: "text-xs text-muted-foreground", children: "Alle schakelaars staan standaard uit; dan werkt de verwerking zoals voorheen." })] }), _jsx(Button, { onClick: () => mutation.mutate("save"), disabled: mutation.isPending, children: "Instellingen opslaan" })] })] }), _jsx(MapsErrorsPanel, {}), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Acties" }), _jsx(CardDescription, { children: "Onderhoud van de wachtrij en de cache." })] }), _jsxs(CardContent, { className: "flex flex-wrap gap-3", children: [_jsxs(Button, { onClick: () => mutation.mutate("queue"), disabled: mutation.isPending, children: [_jsx(RefreshCw, { className: "mr-2 h-4 w-4" }), " Wachtrij nu verwerken"] }), _jsxs(Button, { variant: "outline", onClick: () => mutation.mutate("purge"), disabled: mutation.isPending, children: [_jsx(Timer, { className: "mr-2 h-4 w-4" }), " Verlopen rijen opruimen"] }), _jsxs(Button, { variant: "destructive", onClick: () => mutation.mutate("clear"), disabled: mutation.isPending, children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), " Cache legen"] })] })] })] })] }) }));
|
|
68
93
|
}
|
|
@@ -5,7 +5,7 @@ import { useState } from "react";
|
|
|
5
5
|
import { useServerFn } from "@tanstack/react-start";
|
|
6
6
|
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@flowselections/core";
|
|
7
7
|
import { AlertTriangle, CheckCircle2, ClipboardCopy, Loader2, XCircle, } from "lucide-react";
|
|
8
|
-
import { getMapsDiagnostics } from "../../lib/maps.functions";
|
|
8
|
+
import { getMapsDiagnostics } from "../../lib/maps.functions.js";
|
|
9
9
|
function formatDateTime(value) {
|
|
10
10
|
const parsed = Date.parse(value);
|
|
11
11
|
if (Number.isNaN(parsed))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MapsErrorsPanel.d.ts","sourceRoot":"","sources":["../../../src/components/maps/MapsErrorsPanel.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,eAAe,gCA6C9B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
+
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "@flowselections/core";
|
|
6
|
+
import { getMapsErrors } from "../../lib/maps.functions";
|
|
7
|
+
const KIND_LABEL = {
|
|
8
|
+
quota: "Quota / limiet",
|
|
9
|
+
invalid_key: "Ongeldige sleutel",
|
|
10
|
+
not_found: "Adres niet gevonden",
|
|
11
|
+
token: "Token Central",
|
|
12
|
+
other: "Overig",
|
|
13
|
+
};
|
|
14
|
+
const PAGE_SIZE = 10;
|
|
15
|
+
export function MapsErrorsPanel() {
|
|
16
|
+
const fetchErrors = useServerFn(getMapsErrors);
|
|
17
|
+
const [page, setPage] = useState(0);
|
|
18
|
+
const { data, isFetching, error } = useQuery({
|
|
19
|
+
queryKey: ["maps-errors", page],
|
|
20
|
+
queryFn: () => fetchErrors({ data: { page, pageSize: PAGE_SIZE } }),
|
|
21
|
+
});
|
|
22
|
+
const items = Array.isArray(data?.items) ? data.items : [];
|
|
23
|
+
const total = data?.total ?? 0;
|
|
24
|
+
const lastPage = Math.max(0, Math.ceil(total / PAGE_SIZE) - 1);
|
|
25
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Recente fouten" }), _jsxs(CardDescription, { children: ["Fouten van Google en Central, met tijdstip. Totaal: ", total] })] }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [error ? (_jsx("p", { className: "text-destructive", children: error instanceof Error ? error.message : "Fouten laden mislukt" })) : null, items.length === 0 && !isFetching ? _jsx("p", { className: "text-muted-foreground", children: "Geen fouten." }) : null, _jsx("ul", { className: "divide-y divide-border", children: items.map((item) => (_jsxs("li", { className: "flex flex-col gap-1 py-2 sm:flex-row sm:items-start sm:gap-3", children: [_jsx("span", { className: "w-40 shrink-0 text-muted-foreground", children: new Date(item.at).toLocaleString("nl-NL") }), _jsx(Badge, { variant: "outline", className: "w-fit", children: KIND_LABEL[item.kind] ?? item.kind }), _jsx("span", { className: "break-all text-foreground", children: item.message })] }, item.id))) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: page === 0 || isFetching, onClick: () => setPage((p) => p - 1), children: "Vorige" }), _jsxs("span", { className: "text-muted-foreground", children: ["Pagina ", page + 1, " van ", lastPage + 1] }), _jsx(Button, { variant: "outline", size: "sm", disabled: page >= lastPage || isFetching, onClick: () => setPage((p) => p + 1), children: "Volgende" })] })] })] }));
|
|
26
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MapsConnectionStatus } from "../../lib/central-auth.server";
|
|
1
|
+
import type { MapsConnectionStatus } from "../../lib/central-auth.server.js";
|
|
2
2
|
export declare function MapsStatusPanel({ status, }: {
|
|
3
3
|
status: MapsConnectionStatus | null;
|
|
4
4
|
}): import("react").JSX.Element;
|
|
@@ -6,7 +6,7 @@ import { useServerFn } from "@tanstack/react-start";
|
|
|
6
6
|
import { useQuery } from "@tanstack/react-query";
|
|
7
7
|
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, toast, } from "@flowselections/core";
|
|
8
8
|
import { AlertTriangle, CheckCircle2, RefreshCw, ShieldCheck, XCircle, } from "lucide-react";
|
|
9
|
-
import { getMapsOverview, testMapsConnection } from "../../lib/maps.functions";
|
|
9
|
+
import { getMapsOverview, testMapsConnection } from "../../lib/maps.functions.js";
|
|
10
10
|
function formatDateTime(value) {
|
|
11
11
|
if (!value)
|
|
12
12
|
return "onbekend";
|
|
@@ -4,7 +4,7 @@ import { useServerFn } from "@tanstack/react-start";
|
|
|
4
4
|
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@flowselections/core";
|
|
5
5
|
import { Link } from "@tanstack/react-router";
|
|
6
6
|
import { MapPin } from "lucide-react";
|
|
7
|
-
import { getMapsOverview } from "../../lib/maps.functions";
|
|
7
|
+
import { getMapsOverview } from "../../lib/maps.functions.js";
|
|
8
8
|
export function MapsSettingsCard() {
|
|
9
9
|
const fetchOverview = useServerFn(getMapsOverview);
|
|
10
10
|
const { data } = useQuery({
|
|
@@ -19,7 +19,7 @@ function createSupabaseClient() {
|
|
|
19
19
|
}
|
|
20
20
|
let _supabase;
|
|
21
21
|
// Import the supabase client like this:
|
|
22
|
-
// import { supabase } from "
|
|
22
|
+
// import { supabase } from "./client.js";
|
|
23
23
|
export const supabase = new Proxy({}, {
|
|
24
24
|
get(_, prop, receiver) {
|
|
25
25
|
if (!_supabase)
|
|
@@ -20,7 +20,7 @@ function createSupabaseAdminClient() {
|
|
|
20
20
|
let _supabaseAdmin;
|
|
21
21
|
// Server-side Supabase client with service role - bypasses RLS
|
|
22
22
|
// SECURITY: Only use this for trusted server-side operations, never expose to client code
|
|
23
|
-
// Import like: import { supabaseAdmin } from "
|
|
23
|
+
// Import like: import { supabaseAdmin } from "./client.server.js";
|
|
24
24
|
export const supabaseAdmin = new Proxy({}, {
|
|
25
25
|
get(_, prop, receiver) {
|
|
26
26
|
if (!_supabaseAdmin)
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/** Storage accepted by Supabase's `auth.storage` option; `localStorage` and the editor broker both satisfy it. */
|
|
2
|
+
export interface PreviewAuthStorage {
|
|
3
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
4
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
5
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* On a Lovable preview surface, broker the auth session to the editor over
|
|
9
|
+
* postMessage so the project's preview surfaces share one login; else localStorage.
|
|
10
|
+
*/
|
|
11
|
+
export declare function brokeredPreviewStorage(): PreviewAuthStorage | undefined;
|
|
6
12
|
//# sourceMappingURL=previewAuthStorage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"previewAuthStorage.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/previewAuthStorage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"previewAuthStorage.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/previewAuthStorage.ts"],"names":[],"mappings":"AAEA,kHAAkH;AAClH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACjE,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AA6BD;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,kBAAkB,GAAG,SAAS,CAsGvE"}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
// This file is automatically generated. Do not edit it directly.
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const RESULT = 'lovable-preview-auth:result';
|
|
3
|
+
function readBrokerReply(data, requestId) {
|
|
4
|
+
if (typeof data !== 'object' || data === null)
|
|
5
|
+
return null;
|
|
6
|
+
if (!('type' in data) || data.type !== RESULT)
|
|
7
|
+
return null;
|
|
8
|
+
if (!('requestId' in data) || data.requestId !== requestId)
|
|
9
|
+
return null;
|
|
10
|
+
return {
|
|
11
|
+
ok: 'ok' in data && Boolean(data.ok),
|
|
12
|
+
value: 'value' in data && typeof data.value === 'string' ? data.value : null,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* On a Lovable preview surface, broker the auth session to the editor over
|
|
17
|
+
* postMessage so the project's preview surfaces share one login; else localStorage.
|
|
18
|
+
*/
|
|
4
19
|
export function brokeredPreviewStorage() {
|
|
5
20
|
if (typeof window === 'undefined')
|
|
6
21
|
return undefined;
|
|
@@ -11,10 +26,13 @@ export function brokeredPreviewStorage() {
|
|
|
11
26
|
// preview--<name> host can't smuggle another project's id.
|
|
12
27
|
const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
|
|
13
28
|
const projectId = onPreviewZone
|
|
14
|
-
? (
|
|
15
|
-
??
|
|
29
|
+
? (new RegExp('^(?:id-preview(?:-[a-z0-9]+)?|project)--(' + UUID + ')(?:-dev)?(?=\\.|$)', 'i').exec(host)?.[1]
|
|
30
|
+
?? new RegExp('^(' + UUID + ')(?=[.-])', 'i').exec(host)?.[1])
|
|
16
31
|
: undefined;
|
|
17
|
-
|
|
32
|
+
// lib.dom types window.parent and location.ancestorOrigins as always set, but a
|
|
33
|
+
// detached frame has no parent and not every browser implements ancestorOrigins.
|
|
34
|
+
const parentWindow = window.parent;
|
|
35
|
+
const framed = parentWindow !== null && parentWindow !== window;
|
|
18
36
|
if (!projectId || !framed)
|
|
19
37
|
return localStorage;
|
|
20
38
|
// Post only to the real editor ancestor, validated as a Lovable origin, so the
|
|
@@ -23,59 +41,75 @@ export function brokeredPreviewStorage() {
|
|
|
23
41
|
const EDITOR = dev
|
|
24
42
|
? /^https:\/\/([a-z0-9-]+\.)*(lovable\.dev|gptengineer\.app)$|^http:\/\/localhost:3000$/
|
|
25
43
|
: /^https:\/\/([a-z0-9-]+\.)*(lovable\.dev|gptengineer\.app)$/;
|
|
26
|
-
const
|
|
44
|
+
const ancestorOrigins = location.ancestorOrigins;
|
|
45
|
+
const ancestor = ancestorOrigins?.[0] ?? (document.referrer ? new URL(document.referrer).origin : '');
|
|
27
46
|
const editorOrigins = ancestor && EDITOR.test(ancestor)
|
|
28
47
|
? [ancestor]
|
|
29
48
|
: (dev ? ['https://lovable.dev', 'http://localhost:3000'] : ['https://lovable.dev']);
|
|
30
|
-
const RESULT = 'lovable-preview-auth:result';
|
|
31
49
|
const TIMEOUT = 2000;
|
|
32
50
|
const newId = () => Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
33
51
|
const request = (type, key, value) => new Promise((resolve) => {
|
|
34
52
|
const requestId = newId();
|
|
35
53
|
let done = false;
|
|
36
|
-
|
|
37
|
-
const finish = (r) => {
|
|
54
|
+
const finish = (reply) => {
|
|
38
55
|
if (done)
|
|
39
56
|
return;
|
|
40
57
|
done = true;
|
|
41
58
|
clearTimeout(timer);
|
|
42
59
|
window.removeEventListener('message', onMessage);
|
|
43
|
-
resolve(
|
|
60
|
+
resolve(reply);
|
|
44
61
|
};
|
|
45
62
|
const onMessage = (e) => {
|
|
46
|
-
if (editorOrigins.
|
|
63
|
+
if (!editorOrigins.includes(e.origin))
|
|
47
64
|
return;
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
finish(
|
|
65
|
+
const reply = readBrokerReply(e.data, requestId);
|
|
66
|
+
if (reply)
|
|
67
|
+
finish(reply);
|
|
51
68
|
};
|
|
52
69
|
window.addEventListener('message', onMessage);
|
|
70
|
+
const timer = setTimeout(() => { finish(null); }, TIMEOUT);
|
|
53
71
|
const msg = { type, requestId, projectId, key };
|
|
54
72
|
if (value !== undefined)
|
|
55
|
-
msg
|
|
73
|
+
msg.value = value;
|
|
56
74
|
// targetOrigin per trusted editor origin, so a session token never reaches an arbitrary embedder.
|
|
57
75
|
for (const origin of editorOrigins)
|
|
58
76
|
window.parent.postMessage(msg, origin);
|
|
59
|
-
timer = setTimeout(() => finish(null), TIMEOUT);
|
|
60
77
|
});
|
|
61
78
|
// The editor may not be listening yet at the first getItem, so retry once.
|
|
62
79
|
let firstGet = true;
|
|
63
80
|
const RETRY_DELAY = 250;
|
|
81
|
+
// Blocked third-party storage throws here; seeding must not break broker reads.
|
|
82
|
+
const readLocal = (key) => { try {
|
|
83
|
+
return localStorage.getItem(key);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
} };
|
|
64
88
|
return {
|
|
65
89
|
getItem: async (key) => {
|
|
66
|
-
|
|
90
|
+
// Sent with GET and REMOVE so an editor that never cached this session can check or revoke it.
|
|
91
|
+
const local = readLocal(key);
|
|
92
|
+
let res = await request('lovable-preview-auth:get', key, local ?? undefined);
|
|
67
93
|
if (!res && firstGet) {
|
|
68
|
-
await new Promise((
|
|
69
|
-
res = await request('lovable-preview-auth:get', key);
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
|
95
|
+
res = await request('lovable-preview-auth:get', key, local ?? undefined);
|
|
70
96
|
}
|
|
71
97
|
firstGet = false;
|
|
72
98
|
// '' is the logout tombstone: clear the local copy too so it can't resurrect if
|
|
73
|
-
// the broker later goes silent. A null
|
|
74
|
-
if (res
|
|
99
|
+
// the broker later goes silent. A null value means never-synced -> keep local.
|
|
100
|
+
if (res?.ok && res.value !== null) {
|
|
75
101
|
if (res.value === '') {
|
|
76
|
-
|
|
102
|
+
if (local !== undefined && readLocal(key) === local)
|
|
103
|
+
localStorage.removeItem(key);
|
|
77
104
|
return null;
|
|
78
105
|
}
|
|
106
|
+
// Seed this origin so a new tab, whose editor holds no session, stays signed in.
|
|
107
|
+
if (local === null && readLocal(key) === null) {
|
|
108
|
+
try {
|
|
109
|
+
localStorage.setItem(key, res.value);
|
|
110
|
+
}
|
|
111
|
+
catch { /* storage full: still return the editor's session */ }
|
|
112
|
+
}
|
|
79
113
|
return res.value;
|
|
80
114
|
}
|
|
81
115
|
return localStorage.getItem(key);
|
|
@@ -83,7 +117,7 @@ export function brokeredPreviewStorage() {
|
|
|
83
117
|
setItem: (key, value) => {
|
|
84
118
|
localStorage.setItem(key, value);
|
|
85
119
|
return request('lovable-preview-auth:set', key, value).then((res) => {
|
|
86
|
-
if (res
|
|
120
|
+
if (res?.ok && res.value !== null && localStorage.getItem(key) === value) {
|
|
87
121
|
if (res.value === '')
|
|
88
122
|
localStorage.removeItem(key);
|
|
89
123
|
else
|
|
@@ -92,8 +126,9 @@ export function brokeredPreviewStorage() {
|
|
|
92
126
|
});
|
|
93
127
|
},
|
|
94
128
|
removeItem: (key) => {
|
|
129
|
+
const local = readLocal(key);
|
|
95
130
|
localStorage.removeItem(key);
|
|
96
|
-
return request('lovable-preview-auth:remove', key).then(() =>
|
|
131
|
+
return request('lovable-preview-auth:remove', key, local ?? undefined).then(() => { });
|
|
97
132
|
},
|
|
98
133
|
};
|
|
99
134
|
}
|