@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/__tests__/lib/api.test.d.ts +2 -0
  3. package/dist/__tests__/lib/api.test.d.ts.map +1 -0
  4. package/dist/__tests__/lib/api.test.js +58 -0
  5. package/dist/__tests__/lib/api.test.js.map +1 -0
  6. package/dist/__tests__/lib/seo-service-autopilot.test.d.ts +2 -0
  7. package/dist/__tests__/lib/seo-service-autopilot.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/seo-service-autopilot.test.js +171 -0
  9. package/dist/__tests__/lib/seo-service-autopilot.test.js.map +1 -0
  10. package/dist/__tests__/lib/seo-service-proposals.test.d.ts +2 -0
  11. package/dist/__tests__/lib/seo-service-proposals.test.d.ts.map +1 -0
  12. package/dist/__tests__/lib/seo-service-proposals.test.js +99 -0
  13. package/dist/__tests__/lib/seo-service-proposals.test.js.map +1 -0
  14. package/dist/__tests__/router/seo-tab-for-path.test.js +1 -0
  15. package/dist/__tests__/router/seo-tab-for-path.test.js.map +1 -1
  16. package/dist/__tests__/views/redirects-pane.render.test.js +30 -12
  17. package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
  18. package/dist/__tests__/views/seo-autopilot-applied.render.test.d.ts +2 -0
  19. package/dist/__tests__/views/seo-autopilot-applied.render.test.d.ts.map +1 -0
  20. package/dist/__tests__/views/seo-autopilot-applied.render.test.js +330 -0
  21. package/dist/__tests__/views/seo-autopilot-applied.render.test.js.map +1 -0
  22. package/dist/__tests__/views/seo-autopilot-card.render.test.d.ts +2 -0
  23. package/dist/__tests__/views/seo-autopilot-card.render.test.d.ts.map +1 -0
  24. package/dist/__tests__/views/seo-autopilot-card.render.test.js +512 -0
  25. package/dist/__tests__/views/seo-autopilot-card.render.test.js.map +1 -0
  26. package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts +2 -0
  27. package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts.map +1 -0
  28. package/dist/__tests__/views/seo-proposals-tab.render.test.js +431 -0
  29. package/dist/__tests__/views/seo-proposals-tab.render.test.js.map +1 -0
  30. package/dist/__tests__/views/seo-settings.render.test.js +41 -4
  31. package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
  32. package/dist/components/seo/ChangePreview.d.ts +6 -0
  33. package/dist/components/seo/ChangePreview.d.ts.map +1 -0
  34. package/dist/components/seo/ChangePreview.js +18 -0
  35. package/dist/components/seo/ChangePreview.js.map +1 -0
  36. package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
  37. package/dist/components/seo/SeoIssueFixPanel.js +1 -14
  38. package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
  39. package/dist/lib/api.d.ts +13 -2
  40. package/dist/lib/api.d.ts.map +1 -1
  41. package/dist/lib/api.js +1 -2
  42. package/dist/lib/api.js.map +1 -1
  43. package/dist/lib/seo-service.d.ts +293 -2
  44. package/dist/lib/seo-service.d.ts.map +1 -1
  45. package/dist/lib/seo-service.js +170 -0
  46. package/dist/lib/seo-service.js.map +1 -1
  47. package/dist/prenta-admin.css +1 -1
  48. package/dist/views/SEO.d.ts +5 -1
  49. package/dist/views/SEO.d.ts.map +1 -1
  50. package/dist/views/SEO.js +28 -9
  51. package/dist/views/SEO.js.map +1 -1
  52. package/dist/views/seo/AutopilotAppliedSection.d.ts +10 -0
  53. package/dist/views/seo/AutopilotAppliedSection.d.ts.map +1 -0
  54. package/dist/views/seo/AutopilotAppliedSection.js +295 -0
  55. package/dist/views/seo/AutopilotAppliedSection.js.map +1 -0
  56. package/dist/views/seo/ProposalsTab.d.ts +7 -0
  57. package/dist/views/seo/ProposalsTab.d.ts.map +1 -0
  58. package/dist/views/seo/ProposalsTab.js +334 -0
  59. package/dist/views/seo/ProposalsTab.js.map +1 -0
  60. package/dist/views/seo/RedirectsTab.d.ts +3 -1
  61. package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
  62. package/dist/views/seo/RedirectsTab.js +14 -19
  63. package/dist/views/seo/RedirectsTab.js.map +1 -1
  64. package/dist/views/seo/relative-age.d.ts +7 -0
  65. package/dist/views/seo/relative-age.d.ts.map +1 -0
  66. package/dist/views/seo/relative-age.js +20 -0
  67. package/dist/views/seo/relative-age.js.map +1 -0
  68. package/dist/views/settings/SeoAutopilotCard.d.ts +30 -0
  69. package/dist/views/settings/SeoAutopilotCard.d.ts.map +1 -0
  70. package/dist/views/settings/SeoAutopilotCard.js +375 -0
  71. package/dist/views/settings/SeoAutopilotCard.js.map +1 -0
  72. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  73. package/dist/views/settings/SeoSettingsTab.js +2 -1
  74. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  75. package/dist/views/settings/components.d.ts +3 -1
  76. package/dist/views/settings/components.d.ts.map +1 -1
  77. package/dist/views/settings/components.js +2 -2
  78. package/dist/views/settings/components.js.map +1 -1
  79. package/package.json +3 -3
  80. package/src/__tests__/lib/api.test.ts +76 -0
  81. package/src/__tests__/lib/seo-service-autopilot.test.ts +196 -0
  82. package/src/__tests__/lib/seo-service-proposals.test.ts +108 -0
  83. package/src/__tests__/router/seo-tab-for-path.test.ts +1 -0
  84. package/src/__tests__/views/redirects-pane.render.test.tsx +36 -16
  85. package/src/__tests__/views/seo-autopilot-applied.render.test.tsx +382 -0
  86. package/src/__tests__/views/seo-autopilot-card.render.test.tsx +610 -0
  87. package/src/__tests__/views/seo-proposals-tab.render.test.tsx +531 -0
  88. package/src/__tests__/views/seo-settings.render.test.tsx +47 -7
  89. package/src/components/seo/ChangePreview.tsx +47 -0
  90. package/src/components/seo/SeoIssueFixPanel.tsx +1 -44
  91. package/src/lib/api.ts +19 -3
  92. package/src/lib/seo-service.ts +455 -2
  93. package/src/views/SEO.tsx +41 -8
  94. package/src/views/seo/AutopilotAppliedSection.tsx +597 -0
  95. package/src/views/seo/ProposalsTab.tsx +761 -0
  96. package/src/views/seo/RedirectsTab.tsx +39 -60
  97. package/src/views/seo/relative-age.ts +15 -0
  98. package/src/views/settings/SeoAutopilotCard.tsx +826 -0
  99. package/src/views/settings/SeoSettingsTab.tsx +2 -0
  100. 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
- function RecoverySettingsCard() {
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 AI re-ranking and optional auto-redirect for high-confidence matches"
107
+ description="Control how suggested redirects are ranked before you review them"
100
108
  >
101
109
  <div className="space-y-4">
102
- <label className="flex items-start gap-3">
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
- <span>
110
- <span className="text-foreground text-sm font-medium">AI re-ranking</span>
111
- <span className="text-muted-foreground block text-sm">
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
- </span>
114
- </span>
115
- </label>
116
- <label className="flex items-start gap-3">
117
- <input
118
- type="checkbox"
119
- className="border-border mt-1 h-4 w-4 rounded"
120
- checked={local.autoApply}
121
- onChange={(e) => setLocal({ ...local, autoApply: e.target.checked })}
122
- />
123
- <span>
124
- <span className="text-foreground text-sm font-medium">
125
- Auto-apply high-confidence redirects
126
- </span>
127
- <span className="text-muted-foreground block text-sm">
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
- const autoMsg =
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
+ }