@prenta/admin 1.13.0 → 1.15.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 +31 -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-proposals.test.d.ts +2 -0
- package/dist/__tests__/lib/seo-service-proposals.test.d.ts.map +1 -0
- package/dist/__tests__/lib/seo-service-proposals.test.js +99 -0
- package/dist/__tests__/lib/seo-service-proposals.test.js.map +1 -0
- package/dist/__tests__/router/seo-tab-for-path.test.js +1 -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-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-proposals-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.js +431 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-settings.render.test.js +41 -4
- package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
- package/dist/components/seo/ChangePreview.d.ts +6 -0
- package/dist/components/seo/ChangePreview.d.ts.map +1 -0
- package/dist/components/seo/ChangePreview.js +18 -0
- package/dist/components/seo/ChangePreview.js.map +1 -0
- package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +1 -14
- package/dist/components/seo/SeoIssueFixPanel.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 +293 -2
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +170 -0
- 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 +28 -9
- package/dist/views/SEO.js.map +1 -1
- 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 +7 -0
- package/dist/views/seo/ProposalsTab.d.ts.map +1 -0
- package/dist/views/seo/ProposalsTab.js +334 -0
- package/dist/views/seo/ProposalsTab.js.map +1 -0
- 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/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/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +2 -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-proposals.test.ts +108 -0
- package/src/__tests__/router/seo-tab-for-path.test.ts +1 -0
- package/src/__tests__/views/redirects-pane.render.test.tsx +36 -16
- 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-proposals-tab.render.test.tsx +531 -0
- package/src/__tests__/views/seo-settings.render.test.tsx +47 -7
- package/src/components/seo/ChangePreview.tsx +47 -0
- package/src/components/seo/SeoIssueFixPanel.tsx +1 -44
- package/src/lib/api.ts +19 -3
- package/src/lib/seo-service.ts +455 -2
- package/src/views/SEO.tsx +41 -8
- package/src/views/seo/AutopilotAppliedSection.tsx +597 -0
- package/src/views/seo/ProposalsTab.tsx +761 -0
- package/src/views/seo/RedirectsTab.tsx +39 -60
- package/src/views/seo/relative-age.ts +15 -0
- package/src/views/settings/SeoAutopilotCard.tsx +826 -0
- package/src/views/settings/SeoSettingsTab.tsx +2 -0
- package/src/views/settings/components.tsx +10 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { useEffect, useId, useRef, useState } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
Plus,
|
|
6
6
|
Trash2,
|
|
@@ -46,6 +46,8 @@ import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../../components/
|
|
|
46
46
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
47
47
|
import { useSeoResource } from './useSeoResource.js'
|
|
48
48
|
|
|
49
|
+
const SEO_SETTINGS_PATH = '/settings?tab=seo'
|
|
50
|
+
|
|
49
51
|
function OverviewCards({ onRefetchKey }: { onRefetchKey: number }) {
|
|
50
52
|
const { data, loading, error, refetch } = useSeoResource(fetchRedirectOverview, [onRefetchKey])
|
|
51
53
|
if (loading) return <SeoLoading label="Loading redirect overview…" />
|
|
@@ -70,10 +72,16 @@ function OverviewCards({ onRefetchKey }: { onRefetchKey: number }) {
|
|
|
70
72
|
)
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
/**
|
|
76
|
+
* 404 recovery settings. Only AI re-ranking lives here; the auto-apply policy
|
|
77
|
+
* (threshold, budget, gate) moved to SEO Autopilot, so the card points there
|
|
78
|
+
* instead of exposing a second copy of the controls.
|
|
79
|
+
*/
|
|
80
|
+
function RecoverySettingsCard({ onNavigate }: { onNavigate?: (path: string) => void }) {
|
|
74
81
|
const { data, loading, error, refetch } = useSeoResource(fetchRedirectRecoverySettings, [])
|
|
75
82
|
const [saving, setSaving] = useState(false)
|
|
76
83
|
const [local, setLocal] = useState<RedirectRecoverySettings | null>(null)
|
|
84
|
+
const aiRerankId = useId()
|
|
77
85
|
|
|
78
86
|
useEffect(() => {
|
|
79
87
|
if (data) setLocal(data)
|
|
@@ -82,7 +90,7 @@ function RecoverySettingsCard() {
|
|
|
82
90
|
async function handleSave() {
|
|
83
91
|
if (!local) return
|
|
84
92
|
setSaving(true)
|
|
85
|
-
const res = await saveRedirectRecoverySettings(local)
|
|
93
|
+
const res = await saveRedirectRecoverySettings({ aiRerank: local.aiRerank })
|
|
86
94
|
setSaving(false)
|
|
87
95
|
if (res.error) return toast.error(res.error)
|
|
88
96
|
toast.success('Recovery settings saved.')
|
|
@@ -96,58 +104,39 @@ function RecoverySettingsCard() {
|
|
|
96
104
|
return (
|
|
97
105
|
<SectionCard
|
|
98
106
|
title="404 recovery settings"
|
|
99
|
-
description="Control
|
|
107
|
+
description="Control how suggested redirects are ranked before you review them"
|
|
100
108
|
>
|
|
101
109
|
<div className="space-y-4">
|
|
102
|
-
<
|
|
110
|
+
<div className="flex items-start gap-3">
|
|
103
111
|
<input
|
|
112
|
+
id={aiRerankId}
|
|
104
113
|
type="checkbox"
|
|
105
|
-
className="border-border mt-1 h-4 w-4 rounded"
|
|
114
|
+
className="border-border focus-visible:ring-ring mt-1 h-4 w-4 rounded focus-visible:ring-2 focus-visible:outline-none"
|
|
106
115
|
checked={local.aiRerank}
|
|
107
116
|
onChange={(e) => setLocal({ ...local, aiRerank: e.target.checked })}
|
|
117
|
+
aria-describedby={`${aiRerankId}-help`}
|
|
108
118
|
/>
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
119
|
+
<div>
|
|
120
|
+
<label htmlFor={aiRerankId} className="text-foreground text-sm font-medium">
|
|
121
|
+
AI re-ranking
|
|
122
|
+
</label>
|
|
123
|
+
<p id={`${aiRerankId}-help`} className="text-muted-foreground text-sm">
|
|
112
124
|
Use semantic search to resolve ambiguous matches (40–84% heuristic confidence).
|
|
113
|
-
</
|
|
114
|
-
</
|
|
115
|
-
</
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Create 301 redirects automatically when confidence meets the threshold (off by
|
|
129
|
-
default).
|
|
130
|
-
</span>
|
|
131
|
-
</span>
|
|
132
|
-
</label>
|
|
133
|
-
<label className="block max-w-xs">
|
|
134
|
-
<span className="text-foreground text-sm font-medium">Auto-apply minimum confidence</span>
|
|
135
|
-
<input
|
|
136
|
-
type="number"
|
|
137
|
-
min={50}
|
|
138
|
-
max={100}
|
|
139
|
-
value={local.autoApplyMinConfidence}
|
|
140
|
-
disabled={!local.autoApply}
|
|
141
|
-
onChange={(e) =>
|
|
142
|
-
setLocal({
|
|
143
|
-
...local,
|
|
144
|
-
autoApplyMinConfidence: Math.max(50, Math.min(100, Number(e.target.value) || 85)),
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
className="border-border bg-input-background text-foreground focus-visible:ring-ring mt-1 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
|
|
148
|
-
/>
|
|
149
|
-
</label>
|
|
150
|
-
<button className={btnSecondary} onClick={handleSave} disabled={saving}>
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<p className="text-muted-foreground text-sm">
|
|
129
|
+
Auto-apply for high-confidence redirects is managed by{' '}
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
className="text-primary focus-visible:ring-ring rounded-sm font-medium underline-offset-2 hover:underline focus-visible:ring-2 focus-visible:outline-none"
|
|
133
|
+
onClick={() => onNavigate?.(SEO_SETTINGS_PATH)}
|
|
134
|
+
>
|
|
135
|
+
SEO Autopilot
|
|
136
|
+
</button>
|
|
137
|
+
.
|
|
138
|
+
</p>
|
|
139
|
+
<button type="button" className={btnSecondary} onClick={handleSave} disabled={saving}>
|
|
151
140
|
{saving ? 'Saving…' : 'Save settings'}
|
|
152
141
|
</button>
|
|
153
142
|
</div>
|
|
@@ -167,17 +156,7 @@ function Ai404RecoveryCard({ onApplied }: { onApplied: () => void }) {
|
|
|
167
156
|
toast.error(res.error)
|
|
168
157
|
return
|
|
169
158
|
}
|
|
170
|
-
|
|
171
|
-
res.autoApplied && res.autoApplied > 0
|
|
172
|
-
? ` ${res.autoApplied} high-confidence redirect${res.autoApplied === 1 ? '' : 's'} applied automatically.`
|
|
173
|
-
: ''
|
|
174
|
-
toast.success(`Generated 404 recovery suggestions.${autoMsg}`)
|
|
175
|
-
if (res.governanceBlocked) {
|
|
176
|
-
// AI governance vetoed the auto-apply (eligible plans were stored as
|
|
177
|
-
// pending suggestions). Tell the operator why, since the SEO module's
|
|
178
|
-
// "Auto-apply" toggle alone no longer decides.
|
|
179
|
-
toast.info(`Auto-apply skipped: ${res.governanceBlocked}`)
|
|
180
|
-
}
|
|
159
|
+
toast.success('Generated 404 recovery suggestions.')
|
|
181
160
|
refetch()
|
|
182
161
|
}
|
|
183
162
|
|
|
@@ -313,7 +292,7 @@ function ChainMapCard({ onFlattened }: { onFlattened: () => void }) {
|
|
|
313
292
|
)
|
|
314
293
|
}
|
|
315
294
|
|
|
316
|
-
export function RedirectsTab() {
|
|
295
|
+
export function RedirectsTab({ onNavigate }: { onNavigate?: (path: string) => void }) {
|
|
317
296
|
const [refetchKey, setRefetchKey] = useState(0)
|
|
318
297
|
const { data, loading, error, refetch } = useSeoResource(fetchRedirectRules, [refetchKey])
|
|
319
298
|
const [editor, setEditor] = useState<{ rule: RedirectRule | null } | null>(null)
|
|
@@ -371,7 +350,7 @@ export function RedirectsTab() {
|
|
|
371
350
|
<OverviewCards onRefetchKey={refetchKey} />
|
|
372
351
|
|
|
373
352
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
|
374
|
-
<RecoverySettingsCard />
|
|
353
|
+
<RecoverySettingsCard onNavigate={onNavigate} />
|
|
375
354
|
<Ai404RecoveryCard onApplied={refetchAll} />
|
|
376
355
|
<ChainMapCard onFlattened={refetchAll} />
|
|
377
356
|
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact "how long ago" label shared by the SEO review surfaces (proposal age,
|
|
3
|
+
* autopilot ledger). Returns `''` for missing, unparsable, or future timestamps
|
|
4
|
+
* so callers can fall back to an absolute date.
|
|
5
|
+
*/
|
|
6
|
+
export function relativeAge(iso: string | null, now: number = Date.now()): string {
|
|
7
|
+
if (!iso) return ''
|
|
8
|
+
const ms = now - new Date(iso).getTime()
|
|
9
|
+
if (!Number.isFinite(ms) || ms < 0) return ''
|
|
10
|
+
const minutes = Math.round(ms / 60_000)
|
|
11
|
+
if (minutes < 60) return `${Math.max(1, minutes)}m ago`
|
|
12
|
+
const hours = Math.round(minutes / 60)
|
|
13
|
+
if (hours < 48) return `${hours}h ago`
|
|
14
|
+
return `${Math.round(hours / 24)}d ago`
|
|
15
|
+
}
|