@prenta/admin 1.14.0 → 1.16.0
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/CHANGELOG.md +16 -0
- package/dist/__tests__/lib/api.test.d.ts +2 -0
- package/dist/__tests__/lib/api.test.d.ts.map +1 -0
- package/dist/__tests__/lib/api.test.js +58 -0
- package/dist/__tests__/lib/api.test.js.map +1 -0
- package/dist/__tests__/lib/seo-service-autopilot.test.d.ts +2 -0
- package/dist/__tests__/lib/seo-service-autopilot.test.d.ts.map +1 -0
- package/dist/__tests__/lib/seo-service-autopilot.test.js +171 -0
- package/dist/__tests__/lib/seo-service-autopilot.test.js.map +1 -0
- package/dist/__tests__/lib/seo-service-geo.test.d.ts +2 -0
- package/dist/__tests__/lib/seo-service-geo.test.d.ts.map +1 -0
- package/dist/__tests__/lib/seo-service-geo.test.js +103 -0
- package/dist/__tests__/lib/seo-service-geo.test.js.map +1 -0
- package/dist/__tests__/lib/seo-service-proposals.test.js +28 -0
- package/dist/__tests__/lib/seo-service-proposals.test.js.map +1 -1
- package/dist/__tests__/router/seo-tab-for-path.test.js +4 -0
- package/dist/__tests__/router/seo-tab-for-path.test.js.map +1 -1
- package/dist/__tests__/views/redirects-pane.render.test.js +30 -12
- package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-ai-visibility.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-ai-visibility.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-ai-visibility.render.test.js +244 -0
- package/dist/__tests__/views/seo-ai-visibility.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-autopilot-applied.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-autopilot-applied.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-autopilot-applied.render.test.js +330 -0
- package/dist/__tests__/views/seo-autopilot-applied.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-autopilot-card.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-autopilot-card.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-autopilot-card.render.test.js +512 -0
- package/dist/__tests__/views/seo-autopilot-card.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-geo-settings-card.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-geo-settings-card.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-geo-settings-card.render.test.js +227 -0
- package/dist/__tests__/views/seo-geo-settings-card.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.js +139 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-settings.render.test.js +53 -4
- package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-technical-tab.render.test.js +13 -0
- package/dist/__tests__/views/seo-technical-tab.render.test.js.map +1 -1
- package/dist/lib/api.d.ts +13 -2
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +1 -2
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/seo-service.d.ts +300 -2
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +172 -5
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/SEO.d.ts +5 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +12 -7
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/seo/AiVisibilityTab.d.ts +4 -0
- package/dist/views/seo/AiVisibilityTab.d.ts.map +1 -0
- package/dist/views/seo/AiVisibilityTab.js +165 -0
- package/dist/views/seo/AiVisibilityTab.js.map +1 -0
- package/dist/views/seo/AutopilotAppliedSection.d.ts +10 -0
- package/dist/views/seo/AutopilotAppliedSection.d.ts.map +1 -0
- package/dist/views/seo/AutopilotAppliedSection.js +295 -0
- package/dist/views/seo/AutopilotAppliedSection.js.map +1 -0
- package/dist/views/seo/ProposalsTab.d.ts.map +1 -1
- package/dist/views/seo/ProposalsTab.js +53 -17
- package/dist/views/seo/ProposalsTab.js.map +1 -1
- package/dist/views/seo/RedirectsTab.d.ts +3 -1
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
- package/dist/views/seo/RedirectsTab.js +14 -19
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/dist/views/seo/TechnicalTab.d.ts +3 -1
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +4 -4
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/dist/views/seo/relative-age.d.ts +7 -0
- package/dist/views/seo/relative-age.d.ts.map +1 -0
- package/dist/views/seo/relative-age.js +20 -0
- package/dist/views/seo/relative-age.js.map +1 -0
- package/dist/views/settings/SeoAutopilotCard.d.ts +30 -0
- package/dist/views/settings/SeoAutopilotCard.d.ts.map +1 -0
- package/dist/views/settings/SeoAutopilotCard.js +375 -0
- package/dist/views/settings/SeoAutopilotCard.js.map +1 -0
- package/dist/views/settings/SeoGeoSettingsCard.d.ts +12 -0
- package/dist/views/settings/SeoGeoSettingsCard.d.ts.map +1 -0
- package/dist/views/settings/SeoGeoSettingsCard.js +178 -0
- package/dist/views/settings/SeoGeoSettingsCard.js.map +1 -0
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +3 -1
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/dist/views/settings/components.d.ts +3 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +2 -2
- package/dist/views/settings/components.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/lib/api.test.ts +76 -0
- package/src/__tests__/lib/seo-service-autopilot.test.ts +196 -0
- package/src/__tests__/lib/seo-service-geo.test.ts +118 -0
- package/src/__tests__/lib/seo-service-proposals.test.ts +29 -0
- package/src/__tests__/router/seo-tab-for-path.test.ts +5 -0
- package/src/__tests__/views/redirects-pane.render.test.tsx +36 -16
- package/src/__tests__/views/seo-ai-visibility.render.test.tsx +279 -0
- package/src/__tests__/views/seo-autopilot-applied.render.test.tsx +382 -0
- package/src/__tests__/views/seo-autopilot-card.render.test.tsx +610 -0
- package/src/__tests__/views/seo-geo-settings-card.render.test.tsx +253 -0
- package/src/__tests__/views/seo-proposals-tab.render.test.tsx +161 -0
- package/src/__tests__/views/seo-settings.render.test.tsx +68 -7
- package/src/__tests__/views/seo-technical-tab.render.test.tsx +15 -0
- package/src/lib/api.ts +19 -3
- package/src/lib/seo-service.ts +439 -6
- package/src/views/SEO.tsx +16 -7
- package/src/views/seo/AiVisibilityTab.tsx +616 -0
- package/src/views/seo/AutopilotAppliedSection.tsx +597 -0
- package/src/views/seo/ProposalsTab.tsx +92 -12
- package/src/views/seo/RedirectsTab.tsx +39 -60
- package/src/views/seo/TechnicalTab.tsx +27 -8
- package/src/views/seo/relative-age.ts +15 -0
- package/src/views/settings/SeoAutopilotCard.tsx +826 -0
- package/src/views/settings/SeoGeoSettingsCard.tsx +310 -0
- package/src/views/settings/SeoSettingsTab.tsx +4 -0
- package/src/views/settings/components.tsx +10 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import { AlertTriangle, Bot, Info, Loader2, Play, RefreshCw } from 'lucide-react';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import { SEO_AUTOPILOT_LIMITS, fetchAutopilotSettings, runAutopilotNow, updateAutopilotSettings, } from '../../lib/seo-service.js';
|
|
7
|
+
import { brandAlignmentLabel } from '../../lib/brand-voice-client.js';
|
|
8
|
+
import { Skeleton } from '../../components/ui/Skeleton.js';
|
|
9
|
+
import { relativeAge } from '../seo/relative-age.js';
|
|
10
|
+
import { SettingsCard, SettingsToggleRow } from './components.js';
|
|
11
|
+
const INPUT_CLASS = 'w-full rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60';
|
|
12
|
+
const CHECKBOX_CLASS = 'border-border accent-primary mt-1 h-4 w-4 shrink-0 rounded focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60';
|
|
13
|
+
const SECONDARY_BUTTON_CLASS = 'border-border text-foreground hover:bg-accent inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60';
|
|
14
|
+
const LINK_BUTTON_CLASS = 'text-primary rounded-sm underline-offset-2 hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none';
|
|
15
|
+
/**
|
|
16
|
+
* The fix types offered, grouped for display. Must cover `SEO_AUTOPILOT_ISSUE_TYPES`
|
|
17
|
+
* exactly once each — the id type keeps stray ids out, and the render test
|
|
18
|
+
* pins the set against the mirrored constant so a type added server-side
|
|
19
|
+
* cannot silently go un-offered. Exported for that test only.
|
|
20
|
+
*/
|
|
21
|
+
export const AUTOPILOT_TYPE_GROUPS = [
|
|
22
|
+
{
|
|
23
|
+
label: 'Titles',
|
|
24
|
+
options: [
|
|
25
|
+
{
|
|
26
|
+
id: 'missing-meta-title',
|
|
27
|
+
label: 'Missing meta title',
|
|
28
|
+
help: 'Writes a page title when none is set.',
|
|
29
|
+
producer: 'AI',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'meta-title-length',
|
|
33
|
+
label: 'Meta title length',
|
|
34
|
+
help: 'Trims or rewrites titles outside 30–60 characters.',
|
|
35
|
+
producer: 'Rule-based',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'duplicate-meta-title',
|
|
39
|
+
label: 'Duplicate meta title',
|
|
40
|
+
help: 'Rewrites a title shared with another page.',
|
|
41
|
+
producer: 'AI',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: 'Descriptions',
|
|
47
|
+
options: [
|
|
48
|
+
{
|
|
49
|
+
id: 'missing-meta-description',
|
|
50
|
+
label: 'Missing meta description',
|
|
51
|
+
help: 'Writes a description when none is set.',
|
|
52
|
+
producer: 'AI',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'meta-description-length',
|
|
56
|
+
label: 'Meta description length',
|
|
57
|
+
help: 'Trims or rewrites descriptions outside 120–160 characters.',
|
|
58
|
+
producer: 'Rule-based',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'duplicate-meta-description',
|
|
62
|
+
label: 'Duplicate meta description',
|
|
63
|
+
help: 'Rewrites a description shared with another page.',
|
|
64
|
+
producer: 'AI',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: 'Technical',
|
|
70
|
+
options: [
|
|
71
|
+
{
|
|
72
|
+
id: 'missing-canonical',
|
|
73
|
+
label: 'Missing canonical',
|
|
74
|
+
help: 'Sets the canonical URL to the page’s own URL.',
|
|
75
|
+
producer: 'Rule-based',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'accidental-noindex',
|
|
79
|
+
label: 'Accidental noindex',
|
|
80
|
+
help: 'Clears noindex on a published page.',
|
|
81
|
+
producer: 'Rule-based',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'missing-structured-data',
|
|
85
|
+
label: 'Missing structured data',
|
|
86
|
+
help: 'Adds the collection’s default Schema.org type.',
|
|
87
|
+
producer: 'Rule-based',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: 'Redirects',
|
|
93
|
+
options: [
|
|
94
|
+
{
|
|
95
|
+
id: 'redirect',
|
|
96
|
+
label: '404-recovery redirects',
|
|
97
|
+
help: 'Creates a 301 when a dead URL matches a live page above the confidence threshold.',
|
|
98
|
+
producer: 'AI',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
const TYPE_LABEL = Object.fromEntries(AUTOPILOT_TYPE_GROUPS.flatMap((g) => g.options.map((o) => [o.id, o.label])));
|
|
104
|
+
const PAUSE_LABEL = {
|
|
105
|
+
outcome_regression: 'a page lost traffic after an autopilot change',
|
|
106
|
+
owner_unavailable: 'the admin who enabled it is no longer active',
|
|
107
|
+
manual: 'it was paused manually',
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* `budget_exhausted` is the run's wall-clock budget, not the daily change cap:
|
|
111
|
+
* the cap is a gate reason (`cap_per_day`) and never halts a run. Remaining
|
|
112
|
+
* proposals are simply picked up by the next tick.
|
|
113
|
+
*/
|
|
114
|
+
const HALT_LABEL = {
|
|
115
|
+
disabled: 'The run stopped because autopilot was disabled',
|
|
116
|
+
paused: 'The run stopped because autopilot was paused',
|
|
117
|
+
owner_unavailable: 'The run stopped because the admin who enabled autopilot is no longer active',
|
|
118
|
+
already_running: 'A run was already in progress',
|
|
119
|
+
verify_failures: 'The run stopped after too many verification failures',
|
|
120
|
+
budget_exhausted: 'The run stopped because it ran out of time; remaining proposals wait for the next tick',
|
|
121
|
+
ledger_write_failed: 'The run stopped because a change could not be recorded (it was undone)',
|
|
122
|
+
apply_failed: 'The run stopped because applying a change failed unexpectedly; details are in the server log',
|
|
123
|
+
run_failed: 'The run stopped with an unexpected error; details are in the server log',
|
|
124
|
+
};
|
|
125
|
+
function isKnownHaltReason(reason) {
|
|
126
|
+
return Object.prototype.hasOwnProperty.call(HALT_LABEL, reason);
|
|
127
|
+
}
|
|
128
|
+
function haltLabel(reason) {
|
|
129
|
+
return isKnownHaltReason(reason) ? HALT_LABEL[reason] : `The run stopped with an error: ${reason}`;
|
|
130
|
+
}
|
|
131
|
+
/** `relativeAge` returns `''` for future or unparsable timestamps; fall back to an absolute date. */
|
|
132
|
+
function lastRunLabel(iso) {
|
|
133
|
+
if (!iso)
|
|
134
|
+
return 'Never run';
|
|
135
|
+
const age = relativeAge(iso);
|
|
136
|
+
if (age)
|
|
137
|
+
return `Last run ${age}`;
|
|
138
|
+
const t = new Date(iso).getTime();
|
|
139
|
+
return Number.isFinite(t) ? `Last run ${new Date(t).toLocaleDateString()}` : 'Last run: unknown';
|
|
140
|
+
}
|
|
141
|
+
function runToast(s) {
|
|
142
|
+
const counts = `${s.applied} applied, ${s.verifyFailed} verify-failed${s.reverted > 0 ? `, ${s.reverted} reverted` : ''}.`;
|
|
143
|
+
if (!s.halted || !s.haltedReason) {
|
|
144
|
+
toast.success(`Autopilot finished: ${counts}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (s.haltedReason === 'already_running') {
|
|
148
|
+
toast.success('A run was already in progress — nothing new was applied.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
toast.warning(`${haltLabel(s.haltedReason)}. ${counts}`);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Settings → SEO "SEO Autopilot" card (spec §6). Owns its own fetch/save like
|
|
155
|
+
* `SeoDigestCard`; every control PUTs a sparse patch immediately and the card
|
|
156
|
+
* adopts the server's resolved policy from the response. One mutation (a save
|
|
157
|
+
* or a manual run) is allowed in flight at a time; saves queue behind each
|
|
158
|
+
* other so a blur-commit followed by a click both land, in order.
|
|
159
|
+
*/
|
|
160
|
+
export function SeoAutopilotCard({ canEdit, onNavigate, }) {
|
|
161
|
+
const baseId = useId();
|
|
162
|
+
const [settings, setSettings] = useState(null);
|
|
163
|
+
/** Mirrors `settings` synchronously so a queued save reads the latest resolved policy. */
|
|
164
|
+
const settingsRef = useRef(null);
|
|
165
|
+
const [loading, setLoading] = useState(true);
|
|
166
|
+
const [loadError, setLoadError] = useState(null);
|
|
167
|
+
const [mutation, setMutation] = useState(null);
|
|
168
|
+
/** Mirrors `mutation` synchronously so a second call in the same tick cannot slip past. */
|
|
169
|
+
const mutationRef = useRef(null);
|
|
170
|
+
/** Saves are serialised through this chain instead of being dropped when one is in flight. */
|
|
171
|
+
const saveChainRef = useRef(Promise.resolve());
|
|
172
|
+
const mountedRef = useRef(true);
|
|
173
|
+
const busy = mutation !== null;
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
mountedRef.current = true;
|
|
176
|
+
return () => {
|
|
177
|
+
mountedRef.current = false;
|
|
178
|
+
};
|
|
179
|
+
}, []);
|
|
180
|
+
const commitSettings = useCallback((next) => {
|
|
181
|
+
settingsRef.current = next;
|
|
182
|
+
setSettings(next);
|
|
183
|
+
}, []);
|
|
184
|
+
const load = useCallback(async () => {
|
|
185
|
+
setLoading(true);
|
|
186
|
+
setLoadError(null);
|
|
187
|
+
const res = await fetchAutopilotSettings();
|
|
188
|
+
if (!mountedRef.current)
|
|
189
|
+
return;
|
|
190
|
+
if (res.settings)
|
|
191
|
+
commitSettings(res.settings);
|
|
192
|
+
else
|
|
193
|
+
setLoadError(res.error);
|
|
194
|
+
setLoading(false);
|
|
195
|
+
}, [commitSettings]);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
void load();
|
|
198
|
+
}, [load]);
|
|
199
|
+
/** Re-read without the skeleton (after a run); a failure is a toast, not a card-level error. */
|
|
200
|
+
const refresh = useCallback(async () => {
|
|
201
|
+
const res = await fetchAutopilotSettings();
|
|
202
|
+
if (!mountedRef.current)
|
|
203
|
+
return;
|
|
204
|
+
if (res.settings)
|
|
205
|
+
commitSettings(res.settings);
|
|
206
|
+
else
|
|
207
|
+
toast.error(res.error);
|
|
208
|
+
}, [commitSettings]);
|
|
209
|
+
const beginMutation = useCallback((kind) => {
|
|
210
|
+
if (mutationRef.current !== null)
|
|
211
|
+
return false;
|
|
212
|
+
mutationRef.current = kind;
|
|
213
|
+
setMutation(kind);
|
|
214
|
+
return true;
|
|
215
|
+
}, []);
|
|
216
|
+
const endMutation = useCallback(() => {
|
|
217
|
+
mutationRef.current = null;
|
|
218
|
+
if (mountedRef.current)
|
|
219
|
+
setMutation(null);
|
|
220
|
+
}, []);
|
|
221
|
+
/** An overlapping mutation is never dropped silently. */
|
|
222
|
+
const reportOverlap = useCallback(() => {
|
|
223
|
+
toast.info(mutationRef.current === 'run'
|
|
224
|
+
? 'Autopilot is still running — try again when it finishes.'
|
|
225
|
+
: 'Still saving the previous change…');
|
|
226
|
+
}, []);
|
|
227
|
+
/** Resolves `true` when the server accepted the patch. */
|
|
228
|
+
async function performSave(patch, successMessage) {
|
|
229
|
+
const current = settingsRef.current;
|
|
230
|
+
if (!current || !mountedRef.current)
|
|
231
|
+
return false;
|
|
232
|
+
if (!beginMutation('save')) {
|
|
233
|
+
reportOverlap();
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
// A pause for `owner_unavailable` is repaired by any save (the saver
|
|
238
|
+
// becomes the owner), so clear the stale reason in the same write —
|
|
239
|
+
// except when the save turns autopilot off: "disabled — resumed" would
|
|
240
|
+
// be contradictory and there is nothing to resume.
|
|
241
|
+
const resumes = current.pausedAt !== null &&
|
|
242
|
+
current.pausedReason === 'owner_unavailable' &&
|
|
243
|
+
patch.enabled !== false;
|
|
244
|
+
const body = resumes ? { ...patch, resume: true } : patch;
|
|
245
|
+
const res = await updateAutopilotSettings(body);
|
|
246
|
+
if (!mountedRef.current)
|
|
247
|
+
return false;
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
toast.error(res.error ?? 'Failed to update autopilot settings');
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
toast.success(resumes && !patch.resume ? `${successMessage} — autopilot resumed` : successMessage);
|
|
253
|
+
if (res.settings)
|
|
254
|
+
commitSettings(res.settings);
|
|
255
|
+
else
|
|
256
|
+
await refresh();
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
endMutation();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Queue a sparse patch behind any save already in flight. Callers await the
|
|
265
|
+
* result to know whether to keep or reset a local draft.
|
|
266
|
+
*/
|
|
267
|
+
function save(patch, successMessage) {
|
|
268
|
+
const next = saveChainRef.current.then(() => performSave(patch, successMessage));
|
|
269
|
+
saveChainRef.current = next.catch(() => undefined);
|
|
270
|
+
return next;
|
|
271
|
+
}
|
|
272
|
+
async function handleRun() {
|
|
273
|
+
if (!beginMutation('run')) {
|
|
274
|
+
reportOverlap();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const res = await runAutopilotNow();
|
|
279
|
+
if (!mountedRef.current)
|
|
280
|
+
return;
|
|
281
|
+
if (!res.ok || !res.summary) {
|
|
282
|
+
toast.error(res.error ?? 'Autopilot run failed');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
runToast(res.summary);
|
|
286
|
+
await refresh();
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
endMutation();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (loading) {
|
|
293
|
+
return (_jsx(SettingsCard, { title: "SEO Autopilot", children: _jsxs("div", { "aria-busy": "true", "aria-live": "polite", children: [_jsx("span", { className: "sr-only", children: "Loading autopilot settings\u2026" }), _jsx(Skeleton, { lines: 4 })] }) }));
|
|
294
|
+
}
|
|
295
|
+
if (!settings) {
|
|
296
|
+
return (_jsx(SettingsCard, { title: "SEO Autopilot", children: _jsxs("div", { role: "alert", className: "border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-4 text-sm", children: [_jsx(AlertTriangle, { size: 16, className: "shrink-0", "aria-hidden": "true" }), _jsxs("span", { className: "flex-1", children: ["Failed to load autopilot settings", loadError ? `: ${loadError}` : '', "."] }), _jsxs("button", { type: "button", onClick: () => void load(), className: "border-destructive/40 focus-visible:ring-ring inline-flex items-center gap-2 rounded-md border px-3 py-1 transition-colors hover:opacity-80 focus-visible:ring-2 focus-visible:outline-none", children: [_jsx(RefreshCw, { size: 14, "aria-hidden": "true" }), "Retry"] })] }) }));
|
|
297
|
+
}
|
|
298
|
+
const paused = settings.pausedAt !== null;
|
|
299
|
+
const pausedTypes = Object.entries(settings.pausedTypes);
|
|
300
|
+
// Inputs lock only for read-only users; a save in flight must not steal
|
|
301
|
+
// focus or swallow the next click (saves queue). Action buttons also lock while busy.
|
|
302
|
+
const disabled = !canEdit;
|
|
303
|
+
const actionDisabled = !canEdit || busy;
|
|
304
|
+
const running = mutation === 'run';
|
|
305
|
+
const lastRun = settings.lastRun;
|
|
306
|
+
const haltNeutral = lastRun?.haltedReason === 'already_running';
|
|
307
|
+
// Governance veto (spec §2): an enabled type whose AI feature has "Requires
|
|
308
|
+
// approval" on can never pass the gate. Warn only when it actually bites.
|
|
309
|
+
const anyMetaEnabled = AUTOPILOT_TYPE_GROUPS.some((g) => g.options.some((o) => o.id !== 'redirect' && settings.types[o.id] === true));
|
|
310
|
+
const vetoed = [];
|
|
311
|
+
if (anyMetaEnabled && settings.governance.issueFixApprovalRequired)
|
|
312
|
+
vetoed.push('Issue fixes');
|
|
313
|
+
if (settings.types.redirect === true && settings.governance.redirectApprovalRequired) {
|
|
314
|
+
vetoed.push('404-recovery redirects');
|
|
315
|
+
}
|
|
316
|
+
// Why "Run now" is unavailable — surfaced as `title` for pointer users and
|
|
317
|
+
// as an sr-only description for keyboard/AT users. `null` when runnable.
|
|
318
|
+
const runHint = !settings.enabled
|
|
319
|
+
? 'Enable autopilot to run it now'
|
|
320
|
+
: paused
|
|
321
|
+
? 'Resume autopilot to run it now'
|
|
322
|
+
: null;
|
|
323
|
+
const runHintId = `${baseId}-run-hint`;
|
|
324
|
+
const settingsAiLink = (_jsx("button", { type: "button", className: LINK_BUTTON_CLASS, onClick: () => onNavigate?.('/settings?tab=ai'), children: "Settings \u2192 AI" }));
|
|
325
|
+
return (_jsx(SettingsCard, { title: "SEO Autopilot", description: "Apply SEO fixes without a click. Each fix type is opt-in; every unattended change is gated, recorded, and revertible from SEO \u2192 Proposals.", busy: busy, actions: _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "bg-primary text-primary-foreground focus-visible:ring-ring inline-flex items-center gap-2 rounded-md px-3 py-2 text-base font-medium transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60", onClick: () => void handleRun(), disabled: actionDisabled || runHint !== null, "aria-busy": running || undefined, title: runHint ?? undefined, "aria-describedby": runHint ? runHintId : undefined, children: [running ? (_jsx(Loader2, { size: 16, className: "motion-safe:animate-spin", "aria-hidden": "true" })) : (_jsx(Play, { size: 16, "aria-hidden": "true" })), "Run now"] }), runHint ? (_jsx("span", { id: runHintId, className: "sr-only", children: runHint })) : null] }), children: _jsxs("div", { className: "flex flex-col gap-5", children: [_jsx(SettingsToggleRow, { label: "Enable autopilot", description: "Runs daily after the search-data sync. The admin who enables it is the author of record for every change.", checked: settings.enabled, onChange: (v) => void save({ enabled: v }, v ? 'Autopilot enabled' : 'Autopilot disabled'), disabled: disabled }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("p", { className: "text-muted-foreground flex flex-wrap items-center gap-x-2 text-sm", role: "status", "aria-label": "Last run", children: [_jsx(Bot, { size: 16, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: settings.lastRunAt ? (_jsx("time", { dateTime: settings.lastRunAt, title: new Date(settings.lastRunAt).toLocaleString(), children: lastRunLabel(settings.lastRunAt) })) : (lastRunLabel(null)) }), lastRun ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7" }), _jsxs("span", { children: [lastRun.applied, " applied"] }), _jsx("span", { "aria-hidden": "true", children: "\u00B7" }), _jsxs("span", { children: [lastRun.verifyFailed, " verify-failed"] }), _jsx("span", { "aria-hidden": "true", children: "\u00B7" }), _jsxs("span", { children: [lastRun.reverted, " reverted"] })] })) : null] }), lastRun?.halted && lastRun.haltedReason ? (_jsxs("p", { className: `flex items-start gap-2 text-sm ${haltNeutral ? 'text-muted-foreground' : 'text-warning'}`, role: "status", "aria-label": "Run stopped", children: [haltNeutral ? (_jsx(Info, { size: 16, className: "mt-0.5 shrink-0", "aria-hidden": "true" })) : (_jsx(AlertTriangle, { size: 16, className: "mt-0.5 shrink-0", "aria-hidden": "true" })), _jsxs("span", { children: [haltLabel(lastRun.haltedReason), "."] })] })) : null] }), paused ? (_jsxs("section", { className: "border-warning/30 bg-warning/10 flex flex-wrap items-center justify-between gap-3 rounded-lg border px-4 py-3", "aria-label": "Autopilot is paused", children: [_jsxs("p", { className: "text-foreground flex items-start gap-2 text-sm", children: [_jsx(AlertTriangle, { size: 16, className: "text-warning mt-0.5 shrink-0", "aria-hidden": "true" }), _jsxs("span", { children: ["Autopilot is ", _jsx("span", { className: "font-medium", children: "paused" }), settings.pausedReason ? ` because ${PAUSE_LABEL[settings.pausedReason]}` : '', ".", settings.pausedReason === 'owner_unavailable'
|
|
326
|
+
? ' Saving any setting resumes autopilot with you as the author of record.'
|
|
327
|
+
: ''] })] }), _jsx("button", { type: "button", className: SECONDARY_BUTTON_CLASS, onClick: () => void save({ resume: true }, 'Autopilot resumed'), disabled: actionDisabled, children: "Resume" })] })) : null, pausedTypes.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", "aria-label": "Paused fix types", children: pausedTypes.map(([type, reason]) => (_jsxs("li", { className: "border-border bg-muted flex flex-wrap items-center justify-between gap-3 rounded-lg border px-4 py-2 text-sm", children: [_jsxs("span", { className: "text-foreground", children: [_jsx("span", { className: "font-medium", children: TYPE_LABEL[type] ?? type }), " \u2014 ", reason] }), _jsxs("button", { type: "button", className: SECONDARY_BUTTON_CLASS, onClick: () => void save({ resumeTypes: [type] }, `Resumed ${TYPE_LABEL[type] ?? type}`), disabled: actionDisabled, children: ["Resume ", TYPE_LABEL[type] ?? type] })] }, type))) })) : null, _jsxs("fieldset", { className: "flex flex-col gap-4", disabled: disabled, children: [_jsx("legend", { className: "text-foreground mb-2 text-base font-medium", children: "Fix types" }), AUTOPILOT_TYPE_GROUPS.map((group) => (_jsxs("div", { children: [_jsx("p", { className: "text-muted-foreground mb-2 text-sm font-medium", children: group.label }), _jsx("ul", { className: "flex flex-col gap-2", children: group.options.map((opt) => {
|
|
328
|
+
const inputId = `${baseId}-${opt.id}`;
|
|
329
|
+
const helpId = `${inputId}-help`;
|
|
330
|
+
const typePaused = settings.pausedTypes[opt.id] !== undefined;
|
|
331
|
+
return (_jsxs("li", { className: "flex items-start gap-3", children: [_jsx("input", { id: inputId, type: "checkbox", className: CHECKBOX_CLASS, checked: settings.types[opt.id] === true, onChange: (e) => void save({ types: { [opt.id]: e.target.checked } }, `${opt.label} ${e.target.checked ? 'enabled' : 'disabled'}`), disabled: disabled, "aria-describedby": helpId }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("label", { htmlFor: inputId, className: "text-foreground text-sm font-medium", children: opt.label }), _jsx("span", { className: "bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-medium", children: opt.producer }), typePaused ? (_jsxs("span", { className: "text-warning inline-flex items-center gap-1 text-xs font-medium", children: [_jsx(AlertTriangle, { size: 12, "aria-hidden": "true" }), "Paused"] })) : null] }), _jsx("p", { id: helpId, className: "text-muted-foreground text-sm", children: opt.help })] })] }, opt.id));
|
|
332
|
+
}) })] }, group.label)))] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(NumberField, { id: `${baseId}-brand`, label: "Minimum brand alignment", hint: `${settings.minBrandAlignment} = ${brandAlignmentLabel(settings.minBrandAlignment)}. 0–100.`, value: settings.minBrandAlignment, min: SEO_AUTOPILOT_LIMITS.minBrandAlignment.min, max: SEO_AUTOPILOT_LIMITS.minBrandAlignment.max, disabled: disabled, onCommit: (v) => save({ minBrandAlignment: v }, 'Brand threshold saved') }), _jsx(NumberField, { id: `${baseId}-redirect`, label: "Minimum redirect confidence", hint: "50\u2013100.", value: settings.redirectMinConfidence, min: SEO_AUTOPILOT_LIMITS.redirectMinConfidence.min, max: SEO_AUTOPILOT_LIMITS.redirectMinConfidence.max, disabled: disabled, onCommit: (v) => save({ redirectMinConfidence: v }, 'Redirect threshold saved') }), _jsx(NumberField, { id: `${baseId}-per-run`, label: "Max changes per run", hint: "1\u201325.", value: settings.maxPerRun, min: SEO_AUTOPILOT_LIMITS.maxPerRun.min, max: SEO_AUTOPILOT_LIMITS.maxPerRun.max, disabled: disabled, onCommit: (v) => save({ maxPerRun: v }, 'Per-run cap saved') }), _jsx(NumberField, { id: `${baseId}-per-day`, label: "Max changes per day", hint: "1\u2013100.", value: settings.maxPerDay, min: SEO_AUTOPILOT_LIMITS.maxPerDay.min, max: SEO_AUTOPILOT_LIMITS.maxPerDay.max, disabled: disabled, onCommit: (v) => save({ maxPerDay: v }, 'Per-day cap saved') }), _jsx(NumberField, { id: `${baseId}-verify`, label: "Stop a run after N verification failures", hint: "1\u201310.", value: settings.verifyFailuresPerRun, min: SEO_AUTOPILOT_LIMITS.verifyFailuresPerRun.min, max: SEO_AUTOPILOT_LIMITS.verifyFailuresPerRun.max, disabled: disabled, onCommit: (v) => save({ verifyFailuresPerRun: v }, 'Breaker saved') })] }), _jsxs("div", { className: "border-border flex flex-col gap-4 border-t pt-4", children: [_jsx(SettingsToggleRow, { label: "Outcome watch", description: "Compare Search Console clicks before and after each change; revert and pause the type on a regression.", checked: settings.outcomeWatch.enabled, onChange: (v) => void save({ outcomeWatch: { enabled: v } }, v ? 'Outcome watch enabled' : 'Outcome watch disabled'), disabled: disabled }), _jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [_jsx(NumberField, { id: `${baseId}-days`, label: "Window (days)", hint: "7\u201328.", value: settings.outcomeWatch.days, min: SEO_AUTOPILOT_LIMITS.outcomeDays.min, max: SEO_AUTOPILOT_LIMITS.outcomeDays.max, disabled: disabled || !settings.outcomeWatch.enabled, onCommit: (v) => save({ outcomeWatch: { days: v } }, 'Window saved') }), _jsx(NumberField, { id: `${baseId}-drop`, label: "Regression threshold (%)", hint: "20\u201380.", value: Math.round(settings.outcomeWatch.dropRatio * 100), min: SEO_AUTOPILOT_LIMITS.dropPercent.min, max: SEO_AUTOPILOT_LIMITS.dropPercent.max, disabled: disabled || !settings.outcomeWatch.enabled, onCommit: (v) => save({ outcomeWatch: { dropRatio: v / 100 } }, 'Threshold saved') }), _jsx(NumberField, { id: `${baseId}-prior`, label: "Minimum prior clicks", hint: "Pages below this are not judged.", value: settings.outcomeWatch.minPriorClicks, min: SEO_AUTOPILOT_LIMITS.minPriorClicks.min, max: SEO_AUTOPILOT_LIMITS.minPriorClicks.max, disabled: disabled || !settings.outcomeWatch.enabled, onCommit: (v) => save({ outcomeWatch: { minPriorClicks: v } }, 'Floor saved') })] })] }), vetoed.length > 0 ? (_jsxs("section", { className: "border-warning/30 bg-warning/10 flex items-start gap-2 rounded-lg border px-4 py-3 text-sm", "aria-label": "Governance warning", children: [_jsx(AlertTriangle, { size: 16, className: "text-warning mt-0.5 shrink-0", "aria-hidden": "true" }), _jsxs("p", { className: "text-foreground", children: [vetoed.join(' and '), " ", vetoed.length > 1 ? 'are' : 'is', " enabled here but the AI feature still requires approval, so autopilot will skip", ' ', vetoed.length > 1 ? 'them' : 'it', ". Turn off \u201CRequires approval\u201D in ", settingsAiLink, ' ', "to let it run."] })] })) : (_jsxs("p", { className: "text-muted-foreground text-sm", children: ["AI governance still applies: a feature with \u201CRequires approval\u201D on in ", settingsAiLink, ' ', "is never auto-applied, whatever is enabled here."] }))] }) }));
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* A bounded integer input that commits on blur (or Enter). The draft is local
|
|
336
|
+
* so typing never fires a PUT per keystroke; an unchanged or unparsable blur
|
|
337
|
+
* restores the saved value without a request. Out-of-range values are clamped
|
|
338
|
+
* to the server's limits before they are sent. A rejected commit (`onCommit`
|
|
339
|
+
* resolves `false`) falls back to the last saved value so the input never
|
|
340
|
+
* shows text the server refused.
|
|
341
|
+
*/
|
|
342
|
+
function NumberField({ id, label, hint, value, min, max, disabled, onCommit, }) {
|
|
343
|
+
const [draft, setDraft] = useState(String(value));
|
|
344
|
+
const [syncedValue, setSyncedValue] = useState(value);
|
|
345
|
+
// Adopt a new server value without an effect: reset the draft during render
|
|
346
|
+
// when the prop changes (React's recommended "derive state from props" form).
|
|
347
|
+
if (value !== syncedValue) {
|
|
348
|
+
setSyncedValue(value);
|
|
349
|
+
setDraft(String(value));
|
|
350
|
+
}
|
|
351
|
+
const hintId = `${id}-hint`;
|
|
352
|
+
function commit() {
|
|
353
|
+
const n = Number(draft);
|
|
354
|
+
if (draft.trim() === '' || !Number.isFinite(n)) {
|
|
355
|
+
setDraft(String(value));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const next = Math.max(min, Math.min(max, Math.round(n)));
|
|
359
|
+
if (next === value) {
|
|
360
|
+
setDraft(String(value));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
void onCommit(next).then((ok) => {
|
|
364
|
+
// On success the parent adopts the server value and the derive-from-props
|
|
365
|
+
// branch above resets the draft; on failure `value` is unchanged, so reset here.
|
|
366
|
+
if (!ok)
|
|
367
|
+
setDraft(String(value));
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return (_jsxs("div", { children: [_jsx("label", { htmlFor: id, className: "text-foreground mb-1 block text-sm font-medium", children: label }), _jsx("input", { id: id, type: "number", inputMode: "numeric", className: INPUT_CLASS, min: min, max: max, step: 1, value: draft, disabled: disabled, "aria-describedby": hintId, onChange: (e) => setDraft(e.target.value), onBlur: commit, onKeyDown: (e) => {
|
|
371
|
+
if (e.key === 'Enter')
|
|
372
|
+
e.currentTarget.blur();
|
|
373
|
+
} }), _jsx("p", { id: hintId, className: "text-muted-foreground mt-1 text-sm", children: hint })] }));
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=SeoAutopilotCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeoAutopilotCard.js","sourceRoot":"","sources":["../../../src/views/settings/SeoAutopilotCard.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings → SEO "AI visibility" card (spec 2026-09-08 §6): the measurement
|
|
3
|
+
* kill switch, daily-bucket retention, and the one-line middleware setup for
|
|
4
|
+
* Next and Astro hosts. Owns its own fetch/save like `SeoAutopilotCard`; every
|
|
5
|
+
* control PUTs a sparse patch immediately, saves queue behind each other, and
|
|
6
|
+
* the card re-reads the server's resolved settings after each success. Takes
|
|
7
|
+
* no `onNavigate`, so it names the AI visibility tab in copy without linking.
|
|
8
|
+
*/
|
|
9
|
+
export declare function SeoGeoSettingsCard({ canEdit }: {
|
|
10
|
+
canEdit: boolean;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=SeoGeoSettingsCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeoGeoSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/views/settings/SeoGeoSettingsCard.tsx"],"names":[],"mappings":"AAiCA;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,2CAyKnE"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import { SEO_GEO_RETENTION, fetchGeoSettings, updateGeoSettings, } from '../../lib/seo-service.js';
|
|
7
|
+
import { Skeleton } from '../../components/ui/Skeleton.js';
|
|
8
|
+
import { SettingsCard, SettingsToggleRow } from './components.js';
|
|
9
|
+
const INPUT_CLASS = 'w-32 rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60 aria-invalid:border-destructive';
|
|
10
|
+
/** Horizontally scrollable, so focusable (`tabIndex={0}`) for keyboard scrolling. */
|
|
11
|
+
const SNIPPET_CLASS = 'bg-muted text-foreground focus-visible:ring-ring overflow-x-auto rounded-md p-3 text-sm focus-visible:ring-2 focus-visible:outline-none';
|
|
12
|
+
/**
|
|
13
|
+
* The two host-side one-liners. Kept as plain strings so the test can pin the
|
|
14
|
+
* identifiers (`observeGeoTraffic`, `createAstroRedirectMiddleware`) against
|
|
15
|
+
* the `@prenta/core/site` and `@prenta/core/astro/redirects` exports.
|
|
16
|
+
*/
|
|
17
|
+
const NEXT_SNIPPET = `import { applyPublicRedirect, observeGeoTraffic } from '@prenta/core/site'
|
|
18
|
+
// inside proxy(), just before the applyPublicRedirect call:
|
|
19
|
+
observeGeoTraffic(request, { baseUrl: origin, adminPaths: ['/admin'] })`;
|
|
20
|
+
const ASTRO_SNIPPET = `// src/middleware.ts — GEO capture is on by default:
|
|
21
|
+
import { createAstroRedirectMiddleware } from '@prenta/core/astro/redirects'
|
|
22
|
+
export const onRequest = createAstroRedirectMiddleware({ baseUrl: import.meta.env.PRENTA_CMS_URL })`;
|
|
23
|
+
/**
|
|
24
|
+
* Settings → SEO "AI visibility" card (spec 2026-09-08 §6): the measurement
|
|
25
|
+
* kill switch, daily-bucket retention, and the one-line middleware setup for
|
|
26
|
+
* Next and Astro hosts. Owns its own fetch/save like `SeoAutopilotCard`; every
|
|
27
|
+
* control PUTs a sparse patch immediately, saves queue behind each other, and
|
|
28
|
+
* the card re-reads the server's resolved settings after each success. Takes
|
|
29
|
+
* no `onNavigate`, so it names the AI visibility tab in copy without linking.
|
|
30
|
+
*/
|
|
31
|
+
export function SeoGeoSettingsCard({ canEdit }) {
|
|
32
|
+
const baseId = useId();
|
|
33
|
+
const [settings, setSettings] = useState(null);
|
|
34
|
+
/** Mirrors `settings` synchronously so a queued save reads the latest resolved values. */
|
|
35
|
+
const settingsRef = useRef(null);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [busy, setBusy] = useState(false);
|
|
38
|
+
/** Saves are serialised through this chain instead of being dropped when one is in flight. */
|
|
39
|
+
const saveChainRef = useRef(Promise.resolve());
|
|
40
|
+
const mountedRef = useRef(true);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
mountedRef.current = true;
|
|
43
|
+
return () => {
|
|
44
|
+
mountedRef.current = false;
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
const commitSettings = useCallback((next) => {
|
|
48
|
+
settingsRef.current = next;
|
|
49
|
+
setSettings(next);
|
|
50
|
+
}, []);
|
|
51
|
+
const load = useCallback(async () => {
|
|
52
|
+
setLoading(true);
|
|
53
|
+
const s = await fetchGeoSettings();
|
|
54
|
+
if (!mountedRef.current)
|
|
55
|
+
return;
|
|
56
|
+
if (s)
|
|
57
|
+
commitSettings(s);
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}, [commitSettings]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
void load();
|
|
62
|
+
}, [load]);
|
|
63
|
+
/**
|
|
64
|
+
* Re-read without the skeleton after a successful save. The PUT answers only
|
|
65
|
+
* `{ ok }`, so the server-resolved values come from a fresh GET; if that GET
|
|
66
|
+
* fails, the accepted patch is applied locally — it is exactly what the
|
|
67
|
+
* server stored (the card clamps retention the way the server does).
|
|
68
|
+
*/
|
|
69
|
+
const refresh = useCallback(async (accepted) => {
|
|
70
|
+
const s = await fetchGeoSettings();
|
|
71
|
+
if (!mountedRef.current)
|
|
72
|
+
return;
|
|
73
|
+
if (s)
|
|
74
|
+
commitSettings(s);
|
|
75
|
+
else if (settingsRef.current)
|
|
76
|
+
commitSettings({ ...settingsRef.current, ...accepted });
|
|
77
|
+
}, [commitSettings]);
|
|
78
|
+
/** Resolves `true` when the server accepted the patch. */
|
|
79
|
+
async function performSave(patch, successMessage) {
|
|
80
|
+
if (!settingsRef.current || !mountedRef.current)
|
|
81
|
+
return false;
|
|
82
|
+
setBusy(true);
|
|
83
|
+
try {
|
|
84
|
+
const res = await updateGeoSettings(patch);
|
|
85
|
+
if (!mountedRef.current)
|
|
86
|
+
return false;
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
toast.error(res.error ?? 'Failed to update AI visibility settings');
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
toast.success(successMessage);
|
|
92
|
+
await refresh(patch);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (mountedRef.current)
|
|
97
|
+
setBusy(false);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Queue a sparse patch behind any save already in flight. Callers await the
|
|
102
|
+
* result to know whether to keep or reset a local draft.
|
|
103
|
+
*/
|
|
104
|
+
function save(patch, successMessage) {
|
|
105
|
+
const next = saveChainRef.current.then(() => performSave(patch, successMessage));
|
|
106
|
+
saveChainRef.current = next.catch(() => undefined);
|
|
107
|
+
return next;
|
|
108
|
+
}
|
|
109
|
+
if (loading) {
|
|
110
|
+
return (_jsx(SettingsCard, { title: "AI visibility", children: _jsxs("div", { "aria-busy": "true", "aria-live": "polite", children: [_jsx("span", { className: "sr-only", children: "Loading AI visibility settings\u2026" }), _jsx(Skeleton, { lines: 4 })] }) }));
|
|
111
|
+
}
|
|
112
|
+
if (!settings) {
|
|
113
|
+
return (_jsx(SettingsCard, { title: "AI visibility", children: _jsxs("div", { role: "alert", className: "border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-4 text-sm", children: [_jsx(AlertTriangle, { size: 16, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "flex-1", children: "Failed to load AI visibility settings." }), _jsxs("button", { type: "button", onClick: () => void load(), className: "border-destructive/40 focus-visible:ring-ring inline-flex items-center gap-2 rounded-md border px-3 py-1 transition-colors hover:opacity-80 focus-visible:ring-2 focus-visible:outline-none", children: [_jsx(RefreshCw, { size: 14, "aria-hidden": "true" }), "Retry"] })] }) }));
|
|
114
|
+
}
|
|
115
|
+
// Controls lock only for read-only users; a save in flight must not steal
|
|
116
|
+
// focus (saves queue), so the card is marked busy instead.
|
|
117
|
+
const disabled = !canEdit;
|
|
118
|
+
return (_jsx(SettingsCard, { title: "AI visibility", description: "Record which AI and search crawlers fetch your pages and which answer engines send visitors. Only bot names, engine names, and your own paths are stored \u2014 never IPs, user agents, or referrer URLs.", busy: busy, children: _jsxs("div", { className: "flex flex-col gap-5", children: [_jsx(SettingsToggleRow, { label: "Record AI crawler and referral activity", description: "When off, the server drops incoming beacons and the AI visibility tab in SEO shows a disabled state. Existing data stays until retention prunes it.", checked: settings.enabled, onChange: (v) => void save({ enabled: v }, v ? 'AI visibility measurement enabled' : 'AI visibility measurement disabled'), disabled: disabled }), _jsx(RetentionField, { id: `${baseId}-retention`, value: settings.retentionDays, disabled: disabled, onCommit: (v) => save({ retentionDays: v }, `Retention set to ${v} days`) }), _jsxs("div", { className: "border-border flex flex-col gap-3 border-t pt-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-foreground text-base font-medium", children: "Site middleware setup" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-sm", children: "Counts arrive only once the public site reports them. Add one line to the host that serves your pages." })] }), _jsxs("div", { children: [_jsxs("p", { className: "text-muted-foreground mb-1 text-sm", children: ["Next.js \u2014 ", _jsx("code", { className: "text-foreground", children: "proxy.ts" })] }), _jsx("pre", { className: SNIPPET_CLASS, tabIndex: 0, "aria-label": "Next.js middleware snippet", children: _jsx("code", { children: NEXT_SNIPPET }) })] }), _jsxs("div", { children: [_jsx("p", { className: "text-muted-foreground mb-1 text-sm", children: "Astro" }), _jsx("pre", { className: SNIPPET_CLASS, tabIndex: 0, "aria-label": "Astro middleware snippet", children: _jsx("code", { children: ASTRO_SNIPPET }) })] })] })] }) }));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Retention input that commits on blur (or Enter). The draft is local so typing
|
|
122
|
+
* never fires a PUT per keystroke; an unchanged blur restores the saved value
|
|
123
|
+
* without a request. Out-of-range integers are clamped to 7–365 before they are
|
|
124
|
+
* sent (the server clamps identically). An empty, non-numeric, or fractional
|
|
125
|
+
* draft is a client-side validation failure: it shows an inline error and sends
|
|
126
|
+
* nothing. A rejected commit (`onCommit` resolves `false`) falls back to the
|
|
127
|
+
* last saved value so the input never shows text the server refused.
|
|
128
|
+
*/
|
|
129
|
+
function RetentionField({ id, value, disabled, onCommit, }) {
|
|
130
|
+
const [draft, setDraft] = useState(String(value));
|
|
131
|
+
const [syncedValue, setSyncedValue] = useState(value);
|
|
132
|
+
const [error, setError] = useState(null);
|
|
133
|
+
// Adopt a new server value without an effect: reset the draft during render
|
|
134
|
+
// when the prop changes (React's recommended "derive state from props" form).
|
|
135
|
+
if (value !== syncedValue) {
|
|
136
|
+
setSyncedValue(value);
|
|
137
|
+
setDraft(String(value));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Latest confirmed value, readable from a commit's `.then` after later
|
|
141
|
+
* renders. Saves queue, so a second commit can fail after an earlier one
|
|
142
|
+
* changed `value`; reverting to the closed-over `value` would show the
|
|
143
|
+
* pre-first-save number while the server holds the first save.
|
|
144
|
+
*/
|
|
145
|
+
const valueRef = useRef(value);
|
|
146
|
+
valueRef.current = value;
|
|
147
|
+
const hintId = `${id}-hint`;
|
|
148
|
+
const errorId = `${id}-error`;
|
|
149
|
+
const { min, max } = SEO_GEO_RETENTION;
|
|
150
|
+
function commit() {
|
|
151
|
+
const n = Number(draft);
|
|
152
|
+
if (draft.trim() === '' || !Number.isInteger(n)) {
|
|
153
|
+
setError('Retention must be a whole number of days');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const next = Math.max(min, Math.min(max, n));
|
|
157
|
+
if (next === valueRef.current) {
|
|
158
|
+
setDraft(String(valueRef.current));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
void onCommit(next).then((ok) => {
|
|
162
|
+
// On success the parent adopts the server value and the derive-from-props
|
|
163
|
+
// branch above resets the draft; on failure revert to the latest confirmed
|
|
164
|
+
// value (not the one closed over when this commit started).
|
|
165
|
+
if (!ok)
|
|
166
|
+
setDraft(String(valueRef.current));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return (_jsxs("div", { children: [_jsx("label", { htmlFor: id, className: "text-foreground mb-1 block text-sm font-medium", children: "Retention (days)" }), _jsx("input", { id: id, type: "number", inputMode: "numeric", className: INPUT_CLASS, min: min, max: max, step: 1, value: draft, disabled: disabled, "aria-invalid": error ? true : undefined, "aria-describedby": error ? `${hintId} ${errorId}` : hintId, onChange: (e) => {
|
|
170
|
+
setDraft(e.target.value);
|
|
171
|
+
if (error)
|
|
172
|
+
setError(null);
|
|
173
|
+
}, onBlur: commit, onKeyDown: (e) => {
|
|
174
|
+
if (e.key === 'Enter')
|
|
175
|
+
e.currentTarget.blur();
|
|
176
|
+
} }), _jsxs("p", { id: hintId, className: "text-muted-foreground mt-1 text-sm", children: ["Daily counts older than this are deleted by the nightly cleanup. ", min, "\u2013", max, " days."] }), error ? (_jsxs("p", { id: errorId, className: "text-destructive mt-1 flex items-center gap-1 text-sm", children: [_jsx(AlertTriangle, { size: 14, className: "shrink-0", "aria-hidden": "true" }), error] })) : null] }));
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=SeoGeoSettingsCard.js.map
|