@actuate-media/cms-admin 0.14.1 → 0.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/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +148 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
- package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
- package/dist/__tests__/components/posts-badges.test.js +68 -0
- package/dist/__tests__/components/posts-badges.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
- package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +137 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-service.test.js +140 -0
- package/dist/__tests__/lib/pages-service.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +185 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
- package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/posts-service.test.js +244 -0
- package/dist/__tests__/lib/posts-service.test.js.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.js +98 -0
- package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/pages/tag-badges.d.ts +21 -0
- package/dist/components/pages/tag-badges.d.ts.map +1 -0
- package/dist/components/pages/tag-badges.js +56 -0
- package/dist/components/pages/tag-badges.js.map +1 -0
- package/dist/components/posts/badges.d.ts +25 -0
- package/dist/components/posts/badges.d.ts.map +1 -0
- package/dist/components/posts/badges.js +123 -0
- package/dist/components/posts/badges.js.map +1 -0
- package/dist/components/posts/icons.d.ts +12 -0
- package/dist/components/posts/icons.d.ts.map +1 -0
- package/dist/components/posts/icons.js +26 -0
- package/dist/components/posts/icons.js.map +1 -0
- package/dist/layout/Header.d.ts +14 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js +10 -3
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +78 -2
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +16 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +176 -49
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +45 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -0
- package/dist/lib/page-editor-service.js +153 -0
- package/dist/lib/page-editor-service.js.map +1 -0
- package/dist/lib/pages-service.d.ts +143 -0
- package/dist/lib/pages-service.d.ts.map +1 -0
- package/dist/lib/pages-service.js +444 -0
- package/dist/lib/pages-service.js.map +1 -0
- package/dist/lib/post-editor-service.d.ts +57 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -0
- package/dist/lib/post-editor-service.js +234 -0
- package/dist/lib/post-editor-service.js.map +1 -0
- package/dist/lib/posts-service.d.ts +73 -0
- package/dist/lib/posts-service.d.ts.map +1 -0
- package/dist/lib/posts-service.js +245 -0
- package/dist/lib/posts-service.js.map +1 -0
- package/dist/lib/preview-link.d.ts +32 -0
- package/dist/lib/preview-link.d.ts.map +1 -0
- package/dist/lib/preview-link.js +40 -0
- package/dist/lib/preview-link.js.map +1 -0
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +9 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +147 -62
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/Pages/PageTagManager.d.ts +14 -0
- package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
- package/dist/views/Pages/PageTagManager.js +145 -0
- package/dist/views/Pages/PageTagManager.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts +18 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -0
- package/dist/views/Pages/PagesListView.js +287 -0
- package/dist/views/Pages/PagesListView.js.map +1 -0
- package/dist/views/Posts/NewPostPicker.d.ts +16 -0
- package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
- package/dist/views/Posts/NewPostPicker.js +59 -0
- package/dist/views/Posts/NewPostPicker.js.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
- package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +26 -0
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
- package/dist/views/Posts/PostTypesView.js +52 -0
- package/dist/views/Posts/PostTypesView.js.map +1 -0
- package/dist/views/Posts/PostsListView.d.ts +25 -0
- package/dist/views/Posts/PostsListView.d.ts.map +1 -0
- package/dist/views/Posts/PostsListView.js +239 -0
- package/dist/views/Posts/PostsListView.js.map +1 -0
- package/dist/views/Posts/post-type-presets.d.ts +32 -0
- package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
- package/dist/views/Posts/post-type-presets.js +72 -0
- package/dist/views/Posts/post-type-presets.js.map +1 -0
- package/dist/views/page-builder/Widgets.d.ts +13 -0
- package/dist/views/page-builder/Widgets.d.ts.map +1 -0
- package/dist/views/page-builder/Widgets.js +15 -0
- package/dist/views/page-builder/Widgets.js.map +1 -0
- package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
- package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
- package/dist/views/page-editor/AddSectionModal.js +22 -0
- package/dist/views/page-editor/AddSectionModal.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
- package/dist/views/page-editor/EditorCanvas.js +64 -0
- package/dist/views/page-editor/EditorCanvas.js.map +1 -0
- package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
- package/dist/views/page-editor/EditorTopBar.js +42 -0
- package/dist/views/page-editor/EditorTopBar.js.map +1 -0
- package/dist/views/page-editor/PagePreview.d.ts +13 -0
- package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
- package/dist/views/page-editor/PagePreview.js +37 -0
- package/dist/views/page-editor/PagePreview.js.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.js +272 -0
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.js +52 -0
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts +17 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
- package/dist/views/page-editor/SectionInspector.js +98 -0
- package/dist/views/page-editor/SectionInspector.js.map +1 -0
- package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
- package/dist/views/page-editor/SectionsPanel.js +43 -0
- package/dist/views/page-editor/SectionsPanel.js.map +1 -0
- package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
- package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
- package/dist/views/page-editor/ValidationSummary.js +10 -0
- package/dist/views/page-editor/ValidationSummary.js.map +1 -0
- package/dist/views/page-editor/section-icons.d.ts +4 -0
- package/dist/views/page-editor/section-icons.d.ts.map +1 -0
- package/dist/views/page-editor/section-icons.js +20 -0
- package/dist/views/page-editor/section-icons.js.map +1 -0
- package/dist/views/page-editor/section-types.d.ts +11 -0
- package/dist/views/page-editor/section-types.d.ts.map +1 -0
- package/dist/views/page-editor/section-types.js +10 -0
- package/dist/views/page-editor/section-types.js.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.js +17 -0
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.js +12 -0
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
- package/dist/views/page-editor/sections/index.d.ts +14 -0
- package/dist/views/page-editor/sections/index.d.ts.map +1 -0
- package/dist/views/page-editor/sections/index.js +12 -0
- package/dist/views/page-editor/sections/index.js.map +1 -0
- package/dist/views/page-editor/sections/parts.d.ts +55 -0
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
- package/dist/views/page-editor/sections/parts.js +60 -0
- package/dist/views/page-editor/sections/parts.js.map +1 -0
- package/dist/views/page-editor/viewports.d.ts +14 -0
- package/dist/views/page-editor/viewports.d.ts.map +1 -0
- package/dist/views/page-editor/viewports.js +12 -0
- package/dist/views/page-editor/viewports.js.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.js +58 -0
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.js +53 -0
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
- package/dist/views/post-editor/PostHeader.d.ts +15 -0
- package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeader.js +27 -0
- package/dist/views/post-editor/PostHeader.js.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.js +21 -0
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
- package/dist/views/post-editor/PostPreview.d.ts +14 -0
- package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
- package/dist/views/post-editor/PostPreview.js +53 -0
- package/dist/views/post-editor/PostPreview.js.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.js +290 -0
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.js +144 -0
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +213 -2
- package/src/__tests__/components/posts-badges.test.tsx +77 -0
- package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
- package/src/__tests__/lib/page-editor-service.test.ts +173 -0
- package/src/__tests__/lib/pages-service.test.ts +157 -0
- package/src/__tests__/lib/post-editor-service.test.ts +215 -0
- package/src/__tests__/lib/posts-service.test.ts +279 -0
- package/src/__tests__/views/posts-list-view.test.ts +110 -0
- package/src/components/pages/tag-badges.tsx +104 -0
- package/src/components/posts/badges.tsx +170 -0
- package/src/components/posts/icons.ts +40 -0
- package/src/layout/Header.tsx +90 -43
- package/src/layout/Layout.tsx +166 -1
- package/src/layout/Sidebar.tsx +287 -101
- package/src/lib/page-editor-service.ts +233 -0
- package/src/lib/pages-service.ts +599 -0
- package/src/lib/post-editor-service.ts +342 -0
- package/src/lib/posts-service.ts +366 -0
- package/src/lib/preview-link.ts +63 -0
- package/src/router/index.ts +10 -1
- package/src/views/Dashboard.tsx +270 -131
- package/src/views/Pages/PageTagManager.tsx +436 -0
- package/src/views/Pages/PagesListView.tsx +944 -0
- package/src/views/Posts/NewPostPicker.tsx +147 -0
- package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
- package/src/views/Posts/PostTypesView.tsx +194 -0
- package/src/views/Posts/PostsListView.tsx +869 -0
- package/src/views/Posts/post-type-presets.ts +105 -0
- package/src/views/page-builder/Widgets.tsx +44 -0
- package/src/views/page-editor/AddSectionModal.tsx +56 -0
- package/src/views/page-editor/EditorCanvas.tsx +127 -0
- package/src/views/page-editor/EditorTopBar.tsx +233 -0
- package/src/views/page-editor/PagePreview.tsx +96 -0
- package/src/views/page-editor/PageSectionEditor.tsx +476 -0
- package/src/views/page-editor/PageSettingsModal.tsx +154 -0
- package/src/views/page-editor/SectionInspector.tsx +599 -0
- package/src/views/page-editor/SectionsPanel.tsx +270 -0
- package/src/views/page-editor/ValidationSummary.tsx +53 -0
- package/src/views/page-editor/section-icons.ts +33 -0
- package/src/views/page-editor/section-types.ts +27 -0
- package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
- package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
- package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
- package/src/views/page-editor/sections/GallerySection.tsx +54 -0
- package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
- package/src/views/page-editor/sections/MissionSection.tsx +22 -0
- package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
- package/src/views/page-editor/sections/index.ts +13 -0
- package/src/views/page-editor/sections/parts.tsx +113 -0
- package/src/views/page-editor/viewports.ts +25 -0
- package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
- package/src/views/post-editor/PostFieldsModal.tsx +196 -0
- package/src/views/post-editor/PostHeader.tsx +87 -0
- package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
- package/src/views/post-editor/PostPreview.tsx +114 -0
- package/src/views/post-editor/PostSectionEditor.tsx +506 -0
- package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cmsApi } from './api.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mint a signed, time-boxed preview token for a single document. The token is
|
|
5
|
+
* bound to the (collection, documentId) pair server-side, so it can only
|
|
6
|
+
* unlock the exact draft it was issued for. Used to build public preview URLs
|
|
7
|
+
* (`/<prefix>/<slug>?preview=<token>`) that render a draft through the
|
|
8
|
+
* consumer's real front-end — see {@link buildPublicPreviewUrl}.
|
|
9
|
+
*/
|
|
10
|
+
export async function createPreviewToken(
|
|
11
|
+
collection: string,
|
|
12
|
+
documentId: string,
|
|
13
|
+
ttlSeconds?: number,
|
|
14
|
+
): Promise<{ token: string; expiresAt: string; slug?: string | null }> {
|
|
15
|
+
const res = await cmsApi<{ token: string; expiresAt: string; slug?: string | null }>(
|
|
16
|
+
'/preview/token',
|
|
17
|
+
{
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: JSON.stringify({ collection, documentId, ttlSeconds }),
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
if (res.error || !res.data) {
|
|
23
|
+
throw new Error(res.error ?? 'Failed to create preview token')
|
|
24
|
+
}
|
|
25
|
+
return res.data
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PublicPreviewUrlParts {
|
|
29
|
+
/** Absolute site origin, e.g. `https://example.com`. Trailing slash optional. */
|
|
30
|
+
siteUrl: string
|
|
31
|
+
/** Collection URL prefix (e.g. `blog`). Empty for top-level page collections. */
|
|
32
|
+
urlPrefix?: string | null
|
|
33
|
+
/** Document slug used in the public path. */
|
|
34
|
+
slug: string
|
|
35
|
+
/** Signed preview token from {@link createPreviewToken}. */
|
|
36
|
+
token: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the public URL a draft preview opens at — the SAME path the
|
|
41
|
+
* `/resolve` endpoint matches: `<urlPrefix>/<slug>` (or `/<slug>` for
|
|
42
|
+
* top-level pages), with the preview token on the query string so the
|
|
43
|
+
* public route can drop the `PUBLISHED` filter for this one document.
|
|
44
|
+
*/
|
|
45
|
+
export function buildPublicPreviewUrl({
|
|
46
|
+
siteUrl,
|
|
47
|
+
urlPrefix,
|
|
48
|
+
slug,
|
|
49
|
+
token,
|
|
50
|
+
}: PublicPreviewUrlParts): string {
|
|
51
|
+
const base = (siteUrl ?? '').replace(/\/+$/, '')
|
|
52
|
+
const prefix = (urlPrefix ?? '').replace(/^\/+|\/+$/g, '')
|
|
53
|
+
const cleanSlug = slug.replace(/^\/+|\/+$/g, '')
|
|
54
|
+
const path = prefix ? `/${prefix}/${cleanSlug}` : `/${cleanSlug}`
|
|
55
|
+
return `${base}${path}?preview=${encodeURIComponent(token)}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve the public site origin: explicit config first, else the current tab's origin. */
|
|
59
|
+
export function resolveSiteUrl(configSiteUrl?: string | null): string {
|
|
60
|
+
if (configSiteUrl && configSiteUrl.trim()) return configSiteUrl.trim()
|
|
61
|
+
if (typeof window !== 'undefined') return window.location.origin
|
|
62
|
+
return ''
|
|
63
|
+
}
|
package/src/router/index.ts
CHANGED
|
@@ -44,6 +44,15 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
|
44
44
|
window.history.pushState({ adminPath: normalizedPath }, '', fullUrl)
|
|
45
45
|
}, [])
|
|
46
46
|
|
|
47
|
+
// Build an absolute (basePath-prefixed) admin URL without navigating. Used
|
|
48
|
+
// for links that must load a real document — e.g. opening a preview in a
|
|
49
|
+
// new browser tab, where `pushState` (which only mutates the current tab)
|
|
50
|
+
// is not an option.
|
|
51
|
+
const buildHref = useCallback((path: string) => {
|
|
52
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
53
|
+
return normalizedPath === '/' ? baseRef.current : `${baseRef.current}${normalizedPath}`
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
47
56
|
const matchRoute = useCallback(
|
|
48
57
|
(pattern: string): RouteParams | null => {
|
|
49
58
|
const patternParts = pattern.split('/').filter(Boolean)
|
|
@@ -75,5 +84,5 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
|
75
84
|
[currentPath],
|
|
76
85
|
)
|
|
77
86
|
|
|
78
|
-
return { currentPath, navigate, matchRoute }
|
|
87
|
+
return { currentPath, navigate, matchRoute, buildHref }
|
|
79
88
|
}
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
Database,
|
|
34
34
|
ChevronRight,
|
|
35
35
|
Loader2,
|
|
36
|
+
TrendingUp,
|
|
37
|
+
TrendingDown,
|
|
38
|
+
Gauge,
|
|
39
|
+
AlertCircle,
|
|
36
40
|
type LucideIcon,
|
|
37
41
|
} from 'lucide-react'
|
|
38
42
|
import { useApiData } from '../lib/useApiData.js'
|
|
@@ -64,6 +68,28 @@ interface HealthData {
|
|
|
64
68
|
models: Record<string, boolean>
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
interface DeliveryStats {
|
|
72
|
+
hasData: boolean
|
|
73
|
+
requests24h: number
|
|
74
|
+
requestsPrev24h: number
|
|
75
|
+
deltaPercent: number | null
|
|
76
|
+
errorRate: number
|
|
77
|
+
errorCount: number
|
|
78
|
+
avgLatencyMs: number
|
|
79
|
+
p95LatencyMs: number
|
|
80
|
+
maxLatencyMs: number
|
|
81
|
+
webhookCount: number
|
|
82
|
+
webhookActiveCount: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ContentHealthData {
|
|
86
|
+
score: number
|
|
87
|
+
label: 'Good' | 'Fair' | 'Poor'
|
|
88
|
+
counts: Record<string, number>
|
|
89
|
+
totalIssues: number
|
|
90
|
+
lastScanAt: string | null
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
interface CollectionMeta {
|
|
68
94
|
slug: string
|
|
69
95
|
type?: string
|
|
@@ -114,6 +140,27 @@ function timeOfDayGreeting(): string {
|
|
|
114
140
|
return 'Good evening'
|
|
115
141
|
}
|
|
116
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Return the user's first name for the greeting. Falls back to the
|
|
145
|
+
* email local-part, then a generic "there". Strips role-style names
|
|
146
|
+
* like "Admin" / "Administrator" / "Editor" — a literal
|
|
147
|
+
* `Good morning, Admin` reads as bot-output, not a person.
|
|
148
|
+
*/
|
|
149
|
+
function firstNameForGreeting(session: any): string {
|
|
150
|
+
const raw = (session?.user?.name ?? session?.name ?? '').trim()
|
|
151
|
+
const email = session?.user?.email ?? session?.email
|
|
152
|
+
const generic = new Set(['admin', 'administrator', 'editor', 'user', 'root'])
|
|
153
|
+
if (raw) {
|
|
154
|
+
const first = raw.split(/\s+/)[0] ?? raw
|
|
155
|
+
if (!generic.has(first.toLowerCase())) return first
|
|
156
|
+
}
|
|
157
|
+
if (typeof email === 'string' && email.length > 0) {
|
|
158
|
+
const local = email.split('@')[0] ?? email
|
|
159
|
+
return local.charAt(0).toUpperCase() + local.slice(1)
|
|
160
|
+
}
|
|
161
|
+
return 'there'
|
|
162
|
+
}
|
|
163
|
+
|
|
117
164
|
function todayDateString(): string {
|
|
118
165
|
return new Date().toLocaleDateString('en-US', {
|
|
119
166
|
weekday: 'long',
|
|
@@ -200,10 +247,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
200
247
|
const nav = (path: string) => onNavigate?.(path)
|
|
201
248
|
const { data: stats, loading, error, exhausted, refetch } = useApiData<DashboardStats>('/stats')
|
|
202
249
|
const { data: health } = useApiData<HealthData>('/health')
|
|
250
|
+
const { data: delivery, loading: deliveryLoading } =
|
|
251
|
+
useApiData<DeliveryStats>('/stats/api-delivery')
|
|
252
|
+
const { data: contentHealthData, loading: contentHealthLoading } =
|
|
253
|
+
useApiData<ContentHealthData>('/stats/content-health')
|
|
203
254
|
|
|
204
255
|
const greeting = useMemo(() => timeOfDayGreeting(), [])
|
|
205
256
|
const dateStr = useMemo(() => todayDateString(), [])
|
|
206
|
-
const userName =
|
|
257
|
+
const userName = useMemo(() => firstNameForGreeting(session), [session])
|
|
207
258
|
|
|
208
259
|
const collections = useMemo(() => resolveCollections(config), [config])
|
|
209
260
|
|
|
@@ -301,18 +352,35 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
301
352
|
}, [stats, collections])
|
|
302
353
|
|
|
303
354
|
// ── Quick actions ───────────────────────────────────────────────────────
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
355
|
+
// 5-button strip matching the reference dashboard. The "+ New Post" entry
|
|
356
|
+
// intentionally duplicates the top-bar primary action: the top bar
|
|
357
|
+
// surface is for chrome-level navigation, the quick-action row is the
|
|
358
|
+
// dashboard's content-creation toolbar. Order mirrors the natural
|
|
359
|
+
// authoring flow (post → page → media → form → integration).
|
|
307
360
|
const quickActions = useMemo(() => {
|
|
361
|
+
const postTypes = collections.filter((c) => c.type === 'post')
|
|
308
362
|
const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page')
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
363
|
+
const pagesSlug = pages?.slug ?? 'pages'
|
|
364
|
+
// "New Post": one Post Type → straight to its editor; several →
|
|
365
|
+
// the picker; none → the Post Type setup instructions (avoids a
|
|
366
|
+
// broken slug-based new URL).
|
|
367
|
+
const newPostPath =
|
|
368
|
+
postTypes.length === 1
|
|
369
|
+
? `/${postTypes[0]!.slug}/new`
|
|
370
|
+
: postTypes.length > 1
|
|
371
|
+
? '/posts/new'
|
|
372
|
+
: '/posts/types/new'
|
|
373
|
+
const items: { label: string; icon: LucideIcon; onClick: () => void }[] = [
|
|
374
|
+
{
|
|
375
|
+
label: 'New Post',
|
|
376
|
+
icon: Plus,
|
|
377
|
+
onClick: () => nav(newPostPath),
|
|
378
|
+
},
|
|
379
|
+
{ label: 'New Page', icon: Plus, onClick: () => nav(`/${pagesSlug}/new`) },
|
|
380
|
+
{ label: 'Upload Media', icon: Upload, onClick: () => nav('/media') },
|
|
381
|
+
{ label: 'New Form', icon: Plus, onClick: () => nav('/forms') },
|
|
382
|
+
{ label: 'View API', icon: Globe, onClick: () => nav('/api-keys') },
|
|
383
|
+
]
|
|
316
384
|
return items
|
|
317
385
|
}, [collections, onNavigate]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
318
386
|
|
|
@@ -351,51 +419,63 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
351
419
|
})
|
|
352
420
|
}, [stats, collections])
|
|
353
421
|
|
|
354
|
-
// ── Content health
|
|
422
|
+
// ── Content health (sourced from /stats/content-health) ─────────────────
|
|
423
|
+
// Persisted by the nightly content-health cron. The four issue rows
|
|
424
|
+
// map 1:1 to ContentIssueType in the schema; the labels are
|
|
425
|
+
// user-facing and intentionally read like the reference dashboard.
|
|
355
426
|
const contentHealth = useMemo(() => {
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (score > 0 && score < 70) {
|
|
427
|
+
const counts = contentHealthData?.counts ?? {}
|
|
428
|
+
const score = contentHealthData?.score ?? 100
|
|
429
|
+
const label = contentHealthData?.label ?? 'Good'
|
|
430
|
+
const tone: 'ok' | 'warn' | 'err' | 'muted' =
|
|
431
|
+
score >= 70 ? 'ok' : score >= 40 ? 'warn' : score > 0 ? 'err' : 'muted'
|
|
432
|
+
|
|
433
|
+
// Issue list — only render rows that have non-zero counts so the
|
|
434
|
+
// card stays compact when the corpus is healthy. The empty state
|
|
435
|
+
// below picks up when all four are zero.
|
|
436
|
+
const issues: { kind: string; label: string; count: number; tone: 'warn' | 'err' }[] = []
|
|
437
|
+
const missingMeta = counts['MISSING_META_DESCRIPTION'] ?? 0
|
|
438
|
+
const brokenLinks = counts['BROKEN_INTERNAL_LINK'] ?? 0
|
|
439
|
+
const missingAlt = counts['MISSING_ALT_TEXT'] ?? 0
|
|
440
|
+
const outdated = counts['OUTDATED_CONTENT'] ?? 0
|
|
441
|
+
if (missingMeta > 0)
|
|
372
442
|
issues.push({
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
443
|
+
kind: 'MISSING_META_DESCRIPTION',
|
|
444
|
+
label: 'Missing meta descriptions',
|
|
445
|
+
count: missingMeta,
|
|
446
|
+
tone: 'warn',
|
|
376
447
|
})
|
|
377
|
-
|
|
448
|
+
if (brokenLinks > 0)
|
|
449
|
+
issues.push({
|
|
450
|
+
kind: 'BROKEN_INTERNAL_LINK',
|
|
451
|
+
label: 'Broken internal links',
|
|
452
|
+
count: brokenLinks,
|
|
453
|
+
tone: 'err',
|
|
454
|
+
})
|
|
455
|
+
if (missingAlt > 0)
|
|
456
|
+
issues.push({
|
|
457
|
+
kind: 'MISSING_ALT_TEXT',
|
|
458
|
+
label: 'Missing alt text',
|
|
459
|
+
count: missingAlt,
|
|
460
|
+
tone: 'warn',
|
|
461
|
+
})
|
|
462
|
+
if (outdated > 0)
|
|
463
|
+
issues.push({
|
|
464
|
+
kind: 'OUTDATED_CONTENT',
|
|
465
|
+
label: 'Outdated content (90+ days)',
|
|
466
|
+
count: outdated,
|
|
467
|
+
tone: 'warn',
|
|
468
|
+
})
|
|
469
|
+
|
|
378
470
|
return {
|
|
379
471
|
score,
|
|
380
|
-
label
|
|
381
|
-
tone
|
|
472
|
+
label,
|
|
473
|
+
tone,
|
|
382
474
|
issues,
|
|
475
|
+
hasData: contentHealthData !== null,
|
|
476
|
+
loading: contentHealthLoading,
|
|
383
477
|
} as const
|
|
384
|
-
}, [
|
|
385
|
-
|
|
386
|
-
// ── Content delivery tiles ──────────────────────────────────────────────
|
|
387
|
-
const delivery = useMemo(() => {
|
|
388
|
-
const sched = stats?.statusCounts?.['SCHEDULED'] ?? 0
|
|
389
|
-
return {
|
|
390
|
-
totalDocs: stats?.totalDocuments ?? 0,
|
|
391
|
-
forms: stats?.formCount ?? 0,
|
|
392
|
-
scheduled: sched,
|
|
393
|
-
webhooks: {
|
|
394
|
-
total: stats?.webhookCount ?? 0,
|
|
395
|
-
active: stats?.webhookActiveCount ?? 0,
|
|
396
|
-
},
|
|
397
|
-
}
|
|
398
|
-
}, [stats])
|
|
478
|
+
}, [contentHealthData, contentHealthLoading])
|
|
399
479
|
|
|
400
480
|
const totalIssues = contentHealth.issues.reduce((s, i) => s + i.count, 0)
|
|
401
481
|
|
|
@@ -409,10 +489,6 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
409
489
|
)
|
|
410
490
|
}
|
|
411
491
|
|
|
412
|
-
const heroPostSlug =
|
|
413
|
-
collections.find((c) => c.slug === 'posts' || c.type === 'post')?.slug ?? 'posts'
|
|
414
|
-
const siteUrl = config?.site?.url ?? config?.seo?.siteUrl ?? null
|
|
415
|
-
|
|
416
492
|
return (
|
|
417
493
|
<div className="w-full space-y-5 p-4 sm:p-6 lg:px-8">
|
|
418
494
|
{/* Health banners ────────────────────────────────────────────── */}
|
|
@@ -452,40 +528,25 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
452
528
|
</div>
|
|
453
529
|
)}
|
|
454
530
|
|
|
455
|
-
{/*
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
<button
|
|
473
|
-
onClick={() => nav(`/${heroPostSlug}/new`)}
|
|
474
|
-
className="inline-flex items-center gap-1.5 rounded-lg bg-violet-600 px-3.5 py-2 text-sm font-medium text-white transition-colors hover:bg-violet-700"
|
|
475
|
-
>
|
|
476
|
-
<Plus className="h-3.5 w-3.5" /> New Post
|
|
477
|
-
</button>
|
|
478
|
-
{siteUrl && (
|
|
479
|
-
<a
|
|
480
|
-
href={siteUrl}
|
|
481
|
-
target="_blank"
|
|
482
|
-
rel="noopener noreferrer"
|
|
483
|
-
className="border-border bg-card hover:bg-accent hover:text-accent-foreground hidden items-center gap-1.5 rounded-lg border px-3 py-2 text-sm transition-colors sm:inline-flex"
|
|
484
|
-
>
|
|
485
|
-
<ExternalLink className="h-3.5 w-3.5" /> View Site
|
|
486
|
-
</a>
|
|
531
|
+
{/* Hero greeting ─────────────────────────────────────────────────
|
|
532
|
+
Primary "+ New Post" and "View Site" actions live in the top bar
|
|
533
|
+
(Header.tsx page-action slot), not here, so the hero is purely
|
|
534
|
+
informational and the dashboard's chrome-level affordances stay
|
|
535
|
+
consistent across navigation. The on-page secondary action row
|
|
536
|
+
below covers the same creation paths plus authoring shortcuts. */}
|
|
537
|
+
<div className="flex flex-col gap-1 sm:gap-0.5">
|
|
538
|
+
<h1 className="text-foreground text-xl font-semibold tracking-tight sm:text-2xl">
|
|
539
|
+
{greeting}, {userName} <span aria-hidden>👋</span>
|
|
540
|
+
</h1>
|
|
541
|
+
<p className="text-muted-foreground text-sm">
|
|
542
|
+
{dateStr}
|
|
543
|
+
{totalIssues > 0 && (
|
|
544
|
+
<span className="hidden sm:inline">
|
|
545
|
+
{' · '}
|
|
546
|
+
{totalIssues} content item{totalIssues === 1 ? '' : 's'} need attention
|
|
547
|
+
</span>
|
|
487
548
|
)}
|
|
488
|
-
</
|
|
549
|
+
</p>
|
|
489
550
|
</div>
|
|
490
551
|
|
|
491
552
|
{/* Quick actions ──────────────────────────────────────────────── */}
|
|
@@ -763,12 +824,21 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
763
824
|
</aside>
|
|
764
825
|
</div>
|
|
765
826
|
|
|
766
|
-
{/*
|
|
827
|
+
{/* Delivery API ──────────────────────────────────────────────────
|
|
828
|
+
Headless content delivery metrics aggregated over the last
|
|
829
|
+
24h from `actuate_api_request_metrics`. Populated in real-time
|
|
830
|
+
by the request logger in handler-factory.ts — no cron
|
|
831
|
+
required. Empty-state messaging differentiates "we haven't
|
|
832
|
+
collected enough data yet" from "we have data and it shows
|
|
833
|
+
0" so editors don't think the dashboard is broken on a
|
|
834
|
+
quiet weekend. */}
|
|
767
835
|
<section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
|
|
768
836
|
<header className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
769
837
|
<div className="min-w-0">
|
|
770
|
-
<h2 className="text-foreground text-sm font-semibold">
|
|
771
|
-
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
838
|
+
<h2 className="text-foreground text-sm font-semibold">Delivery API</h2>
|
|
839
|
+
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
840
|
+
Headless content delivery · last 24 hours
|
|
841
|
+
</p>
|
|
772
842
|
</div>
|
|
773
843
|
<button
|
|
774
844
|
className="inline-flex items-center gap-1 text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
|
|
@@ -777,52 +847,114 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
777
847
|
API docs <ExternalLink className="h-3 w-3" />
|
|
778
848
|
</button>
|
|
779
849
|
</header>
|
|
780
|
-
<
|
|
781
|
-
<DeliveryTile
|
|
782
|
-
icon={Activity}
|
|
783
|
-
label="Total Documents"
|
|
784
|
-
value={delivery.totalDocs.toLocaleString()}
|
|
785
|
-
sub="across all collections"
|
|
786
|
-
/>
|
|
787
|
-
<DeliveryTile
|
|
788
|
-
icon={Clock}
|
|
789
|
-
label="Scheduled Posts"
|
|
790
|
-
value={delivery.scheduled.toLocaleString()}
|
|
791
|
-
sub={delivery.scheduled > 0 ? 'in publishing queue' : 'none queued'}
|
|
792
|
-
tone={delivery.scheduled > 0 ? 'ok' : 'muted'}
|
|
793
|
-
/>
|
|
794
|
-
<DeliveryTile
|
|
795
|
-
icon={ClipboardList}
|
|
796
|
-
label="Form Responses"
|
|
797
|
-
value={delivery.forms.toLocaleString()}
|
|
798
|
-
sub={delivery.forms > 0 ? 'total received' : 'none yet'}
|
|
799
|
-
tone={delivery.forms > 0 ? 'ok' : 'muted'}
|
|
800
|
-
/>
|
|
801
|
-
<DeliveryTile
|
|
802
|
-
icon={Zap}
|
|
803
|
-
label="Active Webhooks"
|
|
804
|
-
value={`${delivery.webhooks.active} / ${delivery.webhooks.total || 0}`}
|
|
805
|
-
sub={
|
|
806
|
-
delivery.webhooks.total === 0
|
|
807
|
-
? 'none configured'
|
|
808
|
-
: delivery.webhooks.active === delivery.webhooks.total
|
|
809
|
-
? 'all active'
|
|
810
|
-
: `${delivery.webhooks.total - delivery.webhooks.active} paused`
|
|
811
|
-
}
|
|
812
|
-
tone={
|
|
813
|
-
delivery.webhooks.total === 0
|
|
814
|
-
? 'muted'
|
|
815
|
-
: delivery.webhooks.active === delivery.webhooks.total
|
|
816
|
-
? 'ok'
|
|
817
|
-
: 'warn'
|
|
818
|
-
}
|
|
819
|
-
/>
|
|
820
|
-
</div>
|
|
850
|
+
<DeliveryGrid delivery={delivery} loading={deliveryLoading} />
|
|
821
851
|
</section>
|
|
822
852
|
</div>
|
|
823
853
|
)
|
|
824
854
|
}
|
|
825
855
|
|
|
856
|
+
// ─── Delivery API grid ──────────────────────────────────────────────────────
|
|
857
|
+
|
|
858
|
+
function DeliveryGrid({ delivery, loading }: { delivery: DeliveryStats | null; loading: boolean }) {
|
|
859
|
+
if (loading && !delivery) {
|
|
860
|
+
return (
|
|
861
|
+
<div
|
|
862
|
+
className="divide-border grid grid-cols-2 divide-x divide-y lg:grid-cols-4 lg:divide-y-0"
|
|
863
|
+
aria-busy
|
|
864
|
+
>
|
|
865
|
+
{[0, 1, 2, 3].map((i) => (
|
|
866
|
+
<div key={i} className="px-4 py-3.5">
|
|
867
|
+
<div className="bg-muted mb-2 h-3 w-24 rounded" />
|
|
868
|
+
<div className="bg-muted mb-1.5 h-7 w-20 rounded" />
|
|
869
|
+
<div className="bg-muted h-3 w-28 rounded" />
|
|
870
|
+
</div>
|
|
871
|
+
))}
|
|
872
|
+
</div>
|
|
873
|
+
)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const hasData = delivery?.hasData ?? false
|
|
877
|
+
const requests = delivery?.requests24h ?? 0
|
|
878
|
+
const delta: number | null = delivery?.deltaPercent ?? null
|
|
879
|
+
const errorRate = delivery?.errorRate ?? 0
|
|
880
|
+
const avgLatency = delivery?.avgLatencyMs ?? 0
|
|
881
|
+
const p95Latency = delivery?.p95LatencyMs ?? 0
|
|
882
|
+
const webhookTotal = delivery?.webhookCount ?? 0
|
|
883
|
+
const webhookActive = delivery?.webhookActiveCount ?? 0
|
|
884
|
+
|
|
885
|
+
// Pretty-print large numbers (14_823 → "14.8K") so the tile stays
|
|
886
|
+
// readable at the dashboard's compact density.
|
|
887
|
+
const requestsLabel = formatLargeNumber(requests)
|
|
888
|
+
const errorRatePct = (errorRate * 100).toFixed(2)
|
|
889
|
+
|
|
890
|
+
return (
|
|
891
|
+
<div className="divide-border grid grid-cols-2 divide-x divide-y lg:grid-cols-4 lg:divide-y-0">
|
|
892
|
+
<DeliveryTile
|
|
893
|
+
icon={Activity}
|
|
894
|
+
label="API Requests (24h)"
|
|
895
|
+
value={hasData ? requestsLabel : '—'}
|
|
896
|
+
sub={
|
|
897
|
+
!hasData
|
|
898
|
+
? 'no traffic recorded yet'
|
|
899
|
+
: delta === null
|
|
900
|
+
? 'no prior baseline'
|
|
901
|
+
: `${delta >= 0 ? '+' : ''}${delta.toFixed(1)}% vs yesterday`
|
|
902
|
+
}
|
|
903
|
+
subIcon={
|
|
904
|
+
hasData && typeof delta === 'number'
|
|
905
|
+
? delta >= 0
|
|
906
|
+
? TrendingUp
|
|
907
|
+
: TrendingDown
|
|
908
|
+
: undefined
|
|
909
|
+
}
|
|
910
|
+
tone={hasData ? (delta && delta >= 0 ? 'ok' : 'muted') : 'muted'}
|
|
911
|
+
/>
|
|
912
|
+
<DeliveryTile
|
|
913
|
+
icon={AlertCircle}
|
|
914
|
+
label="Error Rate"
|
|
915
|
+
value={hasData ? `${errorRatePct}%` : '—'}
|
|
916
|
+
sub={
|
|
917
|
+
!hasData
|
|
918
|
+
? 'no traffic recorded yet'
|
|
919
|
+
: errorRate < 0.005
|
|
920
|
+
? 'healthy'
|
|
921
|
+
: errorRate < 0.05
|
|
922
|
+
? 'elevated'
|
|
923
|
+
: 'investigate'
|
|
924
|
+
}
|
|
925
|
+
tone={!hasData ? 'muted' : errorRate < 0.005 ? 'ok' : errorRate < 0.05 ? 'warn' : 'err'}
|
|
926
|
+
/>
|
|
927
|
+
<DeliveryTile
|
|
928
|
+
icon={Gauge}
|
|
929
|
+
label="Avg Response Time"
|
|
930
|
+
value={hasData ? `${Math.round(avgLatency)}ms` : '—'}
|
|
931
|
+
sub={hasData ? `p95: ${p95Latency}ms` : 'no traffic recorded yet'}
|
|
932
|
+
tone={!hasData ? 'muted' : avgLatency < 200 ? 'ok' : avgLatency < 600 ? 'warn' : 'err'}
|
|
933
|
+
/>
|
|
934
|
+
<DeliveryTile
|
|
935
|
+
icon={Zap}
|
|
936
|
+
label="Active Webhooks"
|
|
937
|
+
value={`${webhookActive} / ${webhookTotal}`}
|
|
938
|
+
sub={
|
|
939
|
+
webhookTotal === 0
|
|
940
|
+
? 'none configured'
|
|
941
|
+
: webhookActive === webhookTotal
|
|
942
|
+
? 'All active'
|
|
943
|
+
: `${webhookTotal - webhookActive} paused`
|
|
944
|
+
}
|
|
945
|
+
tone={webhookTotal === 0 ? 'muted' : webhookActive === webhookTotal ? 'ok' : 'warn'}
|
|
946
|
+
/>
|
|
947
|
+
</div>
|
|
948
|
+
)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function formatLargeNumber(n: number): string {
|
|
952
|
+
if (n < 1000) return n.toLocaleString()
|
|
953
|
+
if (n < 10_000) return `${(n / 1000).toFixed(1)}K`
|
|
954
|
+
if (n < 1_000_000) return `${(n / 1000).toFixed(0)}K`
|
|
955
|
+
return `${(n / 1_000_000).toFixed(1)}M`
|
|
956
|
+
}
|
|
957
|
+
|
|
826
958
|
// ─── Sub-components (kept in-file: smaller bundle, no extra module hops) ────
|
|
827
959
|
|
|
828
960
|
function EmptyState({
|
|
@@ -852,12 +984,14 @@ function DeliveryTile({
|
|
|
852
984
|
label,
|
|
853
985
|
value,
|
|
854
986
|
sub,
|
|
987
|
+
subIcon: SubIcon,
|
|
855
988
|
tone = 'muted',
|
|
856
989
|
}: {
|
|
857
990
|
icon: LucideIcon
|
|
858
991
|
label: string
|
|
859
992
|
value: string
|
|
860
993
|
sub?: string
|
|
994
|
+
subIcon?: LucideIcon
|
|
861
995
|
tone?: 'ok' | 'warn' | 'err' | 'muted'
|
|
862
996
|
}) {
|
|
863
997
|
const subTone =
|
|
@@ -875,7 +1009,12 @@ function DeliveryTile({
|
|
|
875
1009
|
<span className="text-[11px] font-medium">{label}</span>
|
|
876
1010
|
</div>
|
|
877
1011
|
<p className="text-foreground text-xl leading-tight font-semibold tracking-tight">{value}</p>
|
|
878
|
-
{sub &&
|
|
1012
|
+
{sub && (
|
|
1013
|
+
<p className={`mt-0.5 inline-flex items-center gap-1 text-[11px] ${subTone}`}>
|
|
1014
|
+
{SubIcon && <SubIcon className="h-3 w-3 shrink-0" aria-hidden />}
|
|
1015
|
+
{sub}
|
|
1016
|
+
</p>
|
|
1017
|
+
)}
|
|
879
1018
|
</div>
|
|
880
1019
|
)
|
|
881
1020
|
}
|