@coywolf/emdash 0.0.0-stage → 0.35.13
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/LICENSE +21 -0
- package/README.md +201 -2
- package/package.json +70 -5
- package/src/admin/admin-css.ts +57 -0
- package/src/admin/ai.tsx +915 -0
- package/src/admin/backups.tsx +613 -0
- package/src/admin/breadcrumbs.tsx +542 -0
- package/src/admin/codeBlocks.tsx +204 -0
- package/src/admin/customBlocks.tsx +321 -0
- package/src/admin/dirty.ts +20 -0
- package/src/admin/discovery.tsx +463 -0
- package/src/admin/download.ts +28 -0
- package/src/admin/feature-switches.tsx +124 -0
- package/src/admin/features.tsx +142 -0
- package/src/admin/files.tsx +791 -0
- package/src/admin/formUploads.tsx +445 -0
- package/src/admin/guides.tsx +18 -0
- package/src/admin/headings.tsx +262 -0
- package/src/admin/images.tsx +825 -0
- package/src/admin/index.tsx +62 -0
- package/src/admin/links.tsx +980 -0
- package/src/admin/performance.tsx +330 -0
- package/src/admin/preview.tsx +63 -0
- package/src/admin/redirects-removed.tsx +266 -0
- package/src/admin/redirects.tsx +470 -0
- package/src/admin/reviews.tsx +232 -0
- package/src/admin/robots-bots.tsx +647 -0
- package/src/admin/robots-shared.tsx +109 -0
- package/src/admin/robots-wizard.tsx +748 -0
- package/src/admin/robots.tsx +911 -0
- package/src/admin/save-bar.tsx +134 -0
- package/src/admin/schema.tsx +1187 -0
- package/src/admin/search.tsx +355 -0
- package/src/admin/settings-ui.tsx +87 -0
- package/src/admin/trash-prompt-core.ts +135 -0
- package/src/admin/trash-prompt.tsx +156 -0
- package/src/admin/usage.tsx +301 -0
- package/src/admin/videos-display.tsx +248 -0
- package/src/admin/videos.tsx +1202 -0
- package/src/admin/wpImport.tsx +1516 -0
- package/src/ai/entities.ts +59 -0
- package/src/ai/export.ts +61 -0
- package/src/ai/logic.test.ts +187 -0
- package/src/ai/logic.ts +606 -0
- package/src/ai/pack.ts +121 -0
- package/src/ai/prompts.ts +87 -0
- package/src/ai/providers.ts +276 -0
- package/src/ai/queue.test.ts +97 -0
- package/src/ai/routes.ts +359 -0
- package/src/ai/store.ts +301 -0
- package/src/ai/wikidata.test.ts +184 -0
- package/src/ai/wikidata.ts +87 -0
- package/src/ai/worker.ts +530 -0
- package/src/astro/astro-modules.d.ts +6 -0
- package/src/astro/breadcrumbs/Breadcrumbs.astro +129 -0
- package/src/astro/breadcrumbs/BreadcrumbsBlock.astro +12 -0
- package/src/astro/codeBlocks/CodeBlock.astro +62 -0
- package/src/astro/codeBlocks/EmDashCode.astro +61 -0
- package/src/astro/customBlocks/CustomBlock.astro +64 -0
- package/src/astro/customBlocks/Details.astro +8 -0
- package/src/astro/customBlocks/Disclosure.astro +8 -0
- package/src/astro/customBlocks/Note.astro +8 -0
- package/src/astro/customBlocks/Podcast.astro +8 -0
- package/src/astro/customBlocks/Quote.astro +8 -0
- package/src/astro/customBlocks/Testimonial.astro +8 -0
- package/src/astro/files/FileCard.astro +103 -0
- package/src/astro/headings/HeadingBlock.astro +129 -0
- package/src/astro/headings/TableOfContents.astro +45 -0
- package/src/astro/headings/request.ts +19 -0
- package/src/astro/images/ResponsiveImage.astro +60 -0
- package/src/astro/index.ts +84 -0
- package/src/astro/reviews/CoywolfReview.astro +27 -0
- package/src/astro/reviews/Review.astro +49 -0
- package/src/astro/search/SearchBox.astro +61 -0
- package/src/astro/search/SearchBoxForm.astro +509 -0
- package/src/astro/tables/CaptionedTable.astro +72 -0
- package/src/astro/videos/CoywolfVideo.astro +448 -0
- package/src/backups/download.ts +112 -0
- package/src/backups/dump.test.mjs +287 -0
- package/src/backups/dump.ts +288 -0
- package/src/backups/module.ts +276 -0
- package/src/backups/pack.ts +33 -0
- package/src/backups/restore.test.mjs +103 -0
- package/src/backups/restore.ts +209 -0
- package/src/backups/store.ts +409 -0
- package/src/breadcrumbs/pack.ts +113 -0
- package/src/breadcrumbs/settings.ts +102 -0
- package/src/breadcrumbs/stamp.ts +40 -0
- package/src/breadcrumbs/trail.ts +123 -0
- package/src/codeBlocks/contrast.ts +179 -0
- package/src/codeBlocks/hljs.d.ts +5 -0
- package/src/codeBlocks/pack.ts +110 -0
- package/src/codeBlocks/render.test.ts +152 -0
- package/src/codeBlocks/render.ts +304 -0
- package/src/codeBlocks/settings.ts +18 -0
- package/src/codeBlocks/themes.generated.ts +304 -0
- package/src/codeBlocks/themes.ts +114 -0
- package/src/core/compose.ts +244 -0
- package/src/core/content-url.ts +888 -0
- package/src/core/csv.ts +119 -0
- package/src/core/d1-batch.ts +65 -0
- package/src/core/entry-docs.ts +175 -0
- package/src/core/features-module.ts +48 -0
- package/src/core/features.ts +392 -0
- package/src/core/generation.ts +88 -0
- package/src/core/guide-content.ts +166 -0
- package/src/core/module.ts +104 -0
- package/src/core/read-failures.ts +57 -0
- package/src/core/request-memo.ts +71 -0
- package/src/core/secrets.ts +83 -0
- package/src/core/settings-enhance.ts +74 -0
- package/src/core/site.ts +36 -0
- package/src/core/storage.ts +27 -0
- package/src/customBlocks/pack.ts +260 -0
- package/src/customBlocks/render.ts +424 -0
- package/src/customBlocks/rich.ts +313 -0
- package/src/customBlocks/settings.ts +46 -0
- package/src/discovery/build.ts +252 -0
- package/src/discovery/feed-build.ts +217 -0
- package/src/discovery/feed.ts +172 -0
- package/src/discovery/indexnow.ts +114 -0
- package/src/discovery/llms.ts +79 -0
- package/src/discovery/markdown.ts +294 -0
- package/src/discovery/memory-cache.ts +97 -0
- package/src/discovery/middleware.ts +186 -0
- package/src/discovery/module.ts +353 -0
- package/src/discovery/news.ts +99 -0
- package/src/discovery/pack.ts +58 -0
- package/src/discovery/settings.ts +91 -0
- package/src/discovery/urls.ts +35 -0
- package/src/env.d.ts +10 -0
- package/src/files/card.ts +231 -0
- package/src/files/counts.test.ts +114 -0
- package/src/files/counts.ts +60 -0
- package/src/files/export.ts +44 -0
- package/src/files/format.test.ts +93 -0
- package/src/files/format.ts +163 -0
- package/src/files/module.ts +596 -0
- package/src/files/pack.ts +92 -0
- package/src/files/r2-rules.test.ts +30 -0
- package/src/files/r2-rules.ts +41 -0
- package/src/files/range.ts +50 -0
- package/src/files/s3.ts +114 -0
- package/src/files/serve.ts +141 -0
- package/src/files/sigv4.test.ts +64 -0
- package/src/files/sigv4.ts +160 -0
- package/src/files/site.ts +137 -0
- package/src/files/walker.ts +58 -0
- package/src/formUploads/lib.ts +266 -0
- package/src/formUploads/pack.ts +55 -0
- package/src/formUploads/wrap.ts +592 -0
- package/src/forms.ts +16 -0
- package/src/headings/headings.test.ts +159 -0
- package/src/headings/pack.ts +154 -0
- package/src/headings/render.ts +139 -0
- package/src/headings/settings.ts +117 -0
- package/src/headings/slug.ts +68 -0
- package/src/headings/stamp.ts +142 -0
- package/src/headings/test-hooks.mjs +14 -0
- package/src/headings/toc.ts +48 -0
- package/src/images/cloudflare.ts +329 -0
- package/src/images/lib.ts +221 -0
- package/src/images/module.ts +372 -0
- package/src/images/pack.ts +804 -0
- package/src/images/pending.ts +37 -0
- package/src/images/settings.ts +48 -0
- package/src/images/setup.ts +192 -0
- package/src/images/variants-estimate.ts +162 -0
- package/src/images/variants-store.ts +475 -0
- package/src/images/variants.ts +311 -0
- package/src/index.ts +143 -0
- package/src/links/check.ts +101 -0
- package/src/links/classify.ts +270 -0
- package/src/links/export.ts +59 -0
- package/src/links/module.ts +652 -0
- package/src/links/pack.ts +43 -0
- package/src/links/pt.ts +325 -0
- package/src/links/store.ts +704 -0
- package/src/middleware.ts +287 -0
- package/src/modules.ts +53 -0
- package/src/options.ts +66 -0
- package/src/pageCache/lib.test.mjs +224 -0
- package/src/pageCache/lib.ts +274 -0
- package/src/pageCache/pack.ts +240 -0
- package/src/pageCache/usage-core.ts +152 -0
- package/src/pageCache/usage.ts +262 -0
- package/src/pageCache/warm.test.mjs +670 -0
- package/src/pageCache/warm.ts +722 -0
- package/src/redirects/middleware.ts +169 -0
- package/src/redirects/module.ts +124 -0
- package/src/redirects/pack.ts +89 -0
- package/src/redirects/removed-core.ts +151 -0
- package/src/redirects/removed.ts +293 -0
- package/src/redirects/rules.ts +355 -0
- package/src/redirects/slug-changes.ts +207 -0
- package/src/redirects/slug-core.test.ts +50 -0
- package/src/redirects/slug-core.ts +49 -0
- package/src/redirects/transfer.ts +92 -0
- package/src/reviews/lib.ts +624 -0
- package/src/reviews/pack.ts +135 -0
- package/src/reviews/schema.ts +128 -0
- package/src/reviews/settings.ts +26 -0
- package/src/robots/bots.ts +321 -0
- package/src/robots/data/bots.json +696 -0
- package/src/robots/data/verified.json +582 -0
- package/src/robots/directory.ts +26 -0
- package/src/robots/explain.ts +191 -0
- package/src/robots/guided.ts +300 -0
- package/src/robots/importer.ts +263 -0
- package/src/robots/middleware.ts +151 -0
- package/src/robots/module.ts +446 -0
- package/src/robots/pack.ts +69 -0
- package/src/robots/radar.ts +129 -0
- package/src/robots/rep.test.ts +335 -0
- package/src/robots/rep.ts +362 -0
- package/src/robots/rules.test.ts +251 -0
- package/src/robots/rules.ts +883 -0
- package/src/robots/transfer.ts +45 -0
- package/src/robots/validate.ts +524 -0
- package/src/schema/catalog.ts +146 -0
- package/src/schema/graph.test.mjs +522 -0
- package/src/schema/graph.ts +791 -0
- package/src/schema/module.ts +1138 -0
- package/src/schema/og-image.ts +100 -0
- package/src/schema/pack.ts +58 -0
- package/src/search/engine.ts +578 -0
- package/src/search/fallback.ts +80 -0
- package/src/search/live-cache.ts +116 -0
- package/src/search/live-client.ts +770 -0
- package/src/search/live-serve.ts +282 -0
- package/src/search/module.ts +261 -0
- package/src/search/pack.ts +57 -0
- package/src/search/query.ts +132 -0
- package/src/search/ratelimit-core.ts +118 -0
- package/src/search/ratelimit.ts +79 -0
- package/src/search/snippet.ts +135 -0
- package/src/search/title-index.ts +139 -0
- package/src/search/title-match.ts +192 -0
- package/src/shared.ts +71 -0
- package/src/version.ts +2 -0
- package/src/videos/date.ts +40 -0
- package/src/videos/engagement.ts +75 -0
- package/src/videos/lib.ts +679 -0
- package/src/videos/module.ts +1160 -0
- package/src/videos/pack.ts +187 -0
- package/src/videos/pause.ts +42 -0
- package/src/videos/poster.ts +324 -0
- package/src/videos/render.ts +336 -0
- package/src/videos/store.ts +551 -0
- package/src/videos/stream.ts +134 -0
- package/src/wpImport/convert.ts +702 -0
- package/src/wpImport/guests.ts +329 -0
- package/src/wpImport/gutenberg.ts +157 -0
- package/src/wpImport/markers.ts +66 -0
- package/src/wpImport/pack.ts +451 -0
- package/src/wpImport/parents.ts +202 -0
- package/src/wpImport/prepare.ts +592 -0
- package/src/wpImport/redirects.ts +358 -0
- package/src/wpImport/sources.ts +67 -0
- package/src/wpImport/stream.ts +160 -0
- package/src/wpImport/urls.ts +354 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trash prompt (feature "redirects.trashPrompt"): right after an editor
|
|
3
|
+
* trashes or unpublishes published entries anywhere in the admin, ask what
|
|
4
|
+
* their old URLs should do. Mounted once, in its own React root, by the
|
|
5
|
+
* admin module (index.tsx). Decide later (or Escape) leaves them listed under
|
|
6
|
+
* Plugins → Redirects → Removed content. See trash-prompt-core.ts for how
|
|
7
|
+
* removals are noticed.
|
|
8
|
+
*/
|
|
9
|
+
import { Banner, Button, Dialog } from "@cloudflare/kumo";
|
|
10
|
+
import { apiFetch, parseApiResponse } from "emdash/plugin-utils";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { createRoot } from "react-dom/client";
|
|
13
|
+
|
|
14
|
+
import { REMOVED_API, RedirectDialog, RemovedItemRow, errorText, resolve } from "./redirects-removed.js";
|
|
15
|
+
import { type Pending, collectRemovals, watchContentRemovals } from "./trash-prompt-core.js";
|
|
16
|
+
|
|
17
|
+
let push: ((items: Pending[]) => void) | null = null;
|
|
18
|
+
const early: Pending[][] = [];
|
|
19
|
+
|
|
20
|
+
async function fetchPending(): Promise<Pending[] | null> {
|
|
21
|
+
const response = await apiFetch(REMOVED_API);
|
|
22
|
+
if (!response.ok) return null; // 404: feature off; 403: can't manage plugins.
|
|
23
|
+
return (await parseApiResponse<{ items: Pending[] }>(response, "Could not load removed content")).items;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function TrashPrompt() {
|
|
27
|
+
const [items, setItems] = React.useState<Pending[]>([]);
|
|
28
|
+
const [status, setStatus] = React.useState<Record<string, string>>({});
|
|
29
|
+
const [busy, setBusy] = React.useState<string | null>(null);
|
|
30
|
+
const [error, setError] = React.useState<string>();
|
|
31
|
+
const [redirecting, setRedirecting] = React.useState<Pending | null>(null);
|
|
32
|
+
const firstButton = React.useRef<HTMLButtonElement>(null);
|
|
33
|
+
const doneButton = React.useRef<HTMLButtonElement>(null);
|
|
34
|
+
const open = items.length > 0;
|
|
35
|
+
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
push = (found) =>
|
|
38
|
+
setItems((current) => [...current, ...found.filter((item) => !current.some((c) => c.id === item.id))]);
|
|
39
|
+
for (const found of early.splice(0)) push(found);
|
|
40
|
+
return () => {
|
|
41
|
+
push = null;
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Focus the first "Redirect to…" once the dialog has opened (it focuses itself first).
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (!open) return;
|
|
48
|
+
const timer = setTimeout(() => firstButton.current?.focus(), 50);
|
|
49
|
+
return () => clearTimeout(timer);
|
|
50
|
+
}, [open]);
|
|
51
|
+
|
|
52
|
+
// After a decision its buttons go away: move focus to the next undecided entry, or to Done.
|
|
53
|
+
const decided = Object.keys(status).length;
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (!decided) return;
|
|
56
|
+
const timer = setTimeout(() => (firstButton.current ?? doneButton.current)?.focus(), 50);
|
|
57
|
+
return () => clearTimeout(timer);
|
|
58
|
+
}, [decided]);
|
|
59
|
+
|
|
60
|
+
const close = () => {
|
|
61
|
+
setItems([]);
|
|
62
|
+
setStatus({});
|
|
63
|
+
setError(undefined);
|
|
64
|
+
setRedirecting(null);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const gone = async (item: Pending) => {
|
|
68
|
+
setBusy(item.id);
|
|
69
|
+
setError(undefined);
|
|
70
|
+
try {
|
|
71
|
+
await resolve(item.id, "gone");
|
|
72
|
+
setStatus((s) => ({ ...s, [item.id]: `${item.url} now returns 410 Gone.` }));
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
setError(errorText(cause, "The request failed"));
|
|
75
|
+
} finally {
|
|
76
|
+
setBusy(null);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const one = items.length === 1 ? items[0] : null;
|
|
81
|
+
const settled = open && items.every((item) => status[item.id]);
|
|
82
|
+
const firstOpen = items.find((item) => !status[item.id]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Dialog.Root open={open} onOpenChange={(next) => !next && busy === null && close()}>
|
|
86
|
+
<Dialog className="p-6" size="lg">
|
|
87
|
+
<Dialog.Title className="text-lg font-semibold">
|
|
88
|
+
{one ? "What should the old URL do?" : "What should the old URLs do?"}
|
|
89
|
+
</Dialog.Title>
|
|
90
|
+
<Dialog.Description className="mt-1 text-sm text-kumo-subtle">
|
|
91
|
+
{one ? (
|
|
92
|
+
<>
|
|
93
|
+
“{one.title}” was live at <code>{one.url}</code>.{settled ? "" : " Until you decide, that URL returns 404."}
|
|
94
|
+
</>
|
|
95
|
+
) : (
|
|
96
|
+
`These ${items.length} entries were live.${settled ? "" : " Until you decide, their URLs return 404."}`
|
|
97
|
+
)}
|
|
98
|
+
</Dialog.Description>
|
|
99
|
+
<ul className="mt-4 max-h-80 divide-y divide-kumo-line overflow-y-auto rounded-lg border border-kumo-line" aria-live="polite">
|
|
100
|
+
{items.map((item) => (
|
|
101
|
+
<RemovedItemRow
|
|
102
|
+
key={item.id}
|
|
103
|
+
item={item}
|
|
104
|
+
busy={busy !== null}
|
|
105
|
+
status={status[item.id]}
|
|
106
|
+
redirectRef={item === firstOpen ? firstButton : undefined}
|
|
107
|
+
onRedirect={() => setRedirecting(item)}
|
|
108
|
+
onGone={() => void gone(item)}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
{error && (
|
|
113
|
+
<div className="mt-4">
|
|
114
|
+
<Banner variant="error" role="alert" description={error} />
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
118
|
+
<p className="flex-1 text-xs text-kumo-subtle">
|
|
119
|
+
{settled ? "" : "You can decide any time under Plugins → Redirects → Removed content."}
|
|
120
|
+
</p>
|
|
121
|
+
<Button ref={doneButton} variant={settled ? "primary" : "secondary"} disabled={busy !== null} onClick={close}>
|
|
122
|
+
{settled ? "Done" : "Decide later"}
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
<RedirectDialog
|
|
126
|
+
item={redirecting}
|
|
127
|
+
onClose={() => setRedirecting(null)}
|
|
128
|
+
onDone={(message) => {
|
|
129
|
+
const item = redirecting;
|
|
130
|
+
setRedirecting(null);
|
|
131
|
+
if (item) setStatus((s) => ({ ...s, [item.id]: message }));
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
</Dialog>
|
|
135
|
+
</Dialog.Root>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Start watching for removals and mount the prompt (browser only, once). */
|
|
140
|
+
export function mountTrashPrompt(): void {
|
|
141
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
142
|
+
const removed = collectRemovals({
|
|
143
|
+
fetchPending,
|
|
144
|
+
onReady: (found) => (push ? push(found) : early.push(found)),
|
|
145
|
+
});
|
|
146
|
+
if (!watchContentRemovals(removed, window)) return;
|
|
147
|
+
const mount = () => {
|
|
148
|
+
if (document.getElementById("cw-trash-prompt")) return;
|
|
149
|
+
const host = document.createElement("div");
|
|
150
|
+
host.id = "cw-trash-prompt";
|
|
151
|
+
document.body.appendChild(host);
|
|
152
|
+
createRoot(host).render(<TrashPrompt />);
|
|
153
|
+
};
|
|
154
|
+
if (document.body) mount();
|
|
155
|
+
else document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
156
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare usage: a card on the Performance page and a dashboard widget.
|
|
3
|
+
* This site's D1 and Workers usage, the account's against its plan, the
|
|
4
|
+
* estimated bill, and the cache warmer's share (an estimate from sampled
|
|
5
|
+
* Workers Logs). Data from cache/usage (kept an hour; Refresh reads again).
|
|
6
|
+
*/
|
|
7
|
+
import { Banner, Button, Loader } from "@cloudflare/kumo";
|
|
8
|
+
import { ArrowsClockwise } from "@phosphor-icons/react";
|
|
9
|
+
import { apiFetch, parseApiResponse } from "emdash/plugin-utils";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
|
|
12
|
+
import { FREE, PAID } from "../pageCache/usage-core.js";
|
|
13
|
+
import type { UsageResult } from "../pageCache/usage.js";
|
|
14
|
+
import { errorText } from "./settings-ui.js";
|
|
15
|
+
|
|
16
|
+
const API = "/_emdash/api/plugins/coywolf-pack/cache/usage";
|
|
17
|
+
const ADMIN = "/_emdash/admin";
|
|
18
|
+
|
|
19
|
+
type Report = Extract<UsageResult, { configured: true }>;
|
|
20
|
+
type Totals = Report["periods"]["day"]["site"];
|
|
21
|
+
|
|
22
|
+
function useUsage() {
|
|
23
|
+
const [data, setData] = React.useState<UsageResult>();
|
|
24
|
+
const [error, setError] = React.useState<string>();
|
|
25
|
+
const [loading, setLoading] = React.useState(false);
|
|
26
|
+
const load = React.useCallback(async (refresh = false) => {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
setError(undefined);
|
|
29
|
+
try {
|
|
30
|
+
setData(await parseApiResponse<UsageResult>(await apiFetch(`${API}${refresh ? "?refresh=1" : ""}`), "Couldn't load usage"));
|
|
31
|
+
} catch (cause) {
|
|
32
|
+
setError(errorText(cause, "Couldn't load usage"));
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}, []);
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
void load();
|
|
39
|
+
}, [load]);
|
|
40
|
+
return { data, error, loading, load };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
|
|
44
|
+
const num = (n: number) => (n < 10_000 ? Math.round(n).toLocaleString("en-US") : compact.format(n));
|
|
45
|
+
const cpu = (ms: number) => (ms < 1000 ? `${Math.round(ms)} ms` : ms < 3_600_000 ? `${num(ms / 1000)} s` : `${(ms / 3_600_000).toFixed(1)} h`);
|
|
46
|
+
const bytes = (b: number | null) => (b === null ? "—" : b < 1e6 ? `${Math.round(b / 1e3)} KB` : b < 1e9 ? `${(b / 1e6).toFixed(1)} MB` : `${(b / 1e9).toFixed(2)} GB`);
|
|
47
|
+
const money = (n: number) => `$${n.toFixed(2)}`;
|
|
48
|
+
const pct = (used: number, of: number) => {
|
|
49
|
+
const p = of > 0 ? (used / of) * 100 : 0;
|
|
50
|
+
return p === 0 ? "0%" : p < 0.1 ? "<0.1%" : p < 10 ? `${p.toFixed(1)}%` : `${Math.round(p)}%`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** The account against the plan: Paid compares the last 30 days with the monthly allowance; Free, the last 24 hours with the daily limits. */
|
|
54
|
+
function allowances(r: Report) {
|
|
55
|
+
if (r.plan === "free") {
|
|
56
|
+
const a = r.periods.day.account;
|
|
57
|
+
return {
|
|
58
|
+
window: "today (UTC), against Workers Free's daily limits, which reset at 00:00 UTC",
|
|
59
|
+
rows: [
|
|
60
|
+
{ label: "Workers requests", used: a.workers.requests, of: FREE.daily.requests, show: num },
|
|
61
|
+
{ label: "D1 rows read", used: a.d1.rowsRead, of: FREE.daily.rowsRead, show: num },
|
|
62
|
+
{ label: "D1 rows written", used: a.d1.rowsWritten, of: FREE.daily.rowsWritten, show: num },
|
|
63
|
+
{ label: "D1 storage", used: r.storage.accountBytes, of: FREE.storageBytes, show: (b: number) => bytes(b) },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const a = r.periods.month.account;
|
|
68
|
+
const m = PAID.monthly;
|
|
69
|
+
return {
|
|
70
|
+
window: "the last 30 days (UTC dates, including today), against what Workers Paid includes each month",
|
|
71
|
+
rows: [
|
|
72
|
+
{ label: "Workers requests", used: a.workers.requests, of: m.requests, show: num },
|
|
73
|
+
{ label: "Workers CPU time", used: a.workers.cpuMs, of: m.cpuMs, show: cpu },
|
|
74
|
+
{ label: "D1 rows read", used: a.d1.rowsRead, of: m.rowsRead, show: num },
|
|
75
|
+
{ label: "D1 rows written", used: a.d1.rowsWritten, of: m.rowsWritten, show: num },
|
|
76
|
+
{ label: "D1 storage", used: r.storage.accountBytes, of: m.storageBytes, show: (b: number) => bytes(b) },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const planName = (r: Report) => (r.plan === "free" ? "Workers Free" : r.plan === "paid" ? "Workers Paid" : "Workers Paid (assumed)");
|
|
82
|
+
|
|
83
|
+
function billText(r: Report): string {
|
|
84
|
+
if (r.plan === "free") return "Workers Free costs nothing; requests over the daily limits fail instead of being billed.";
|
|
85
|
+
const extra = Object.entries(r.bill.overage).filter(([, v]) => v > 0);
|
|
86
|
+
const names: Record<string, string> = { requests: "requests", cpu: "CPU time", rowsRead: "D1 rows read", rowsWritten: "D1 rows written", storage: "D1 storage" };
|
|
87
|
+
return extra.length
|
|
88
|
+
? `About ${money(r.bill.total)} a month: the ${money(r.bill.base)} base plus ${extra.map(([k, v]) => `${money(v)} for ${names[k]}`).join(", ")}.`
|
|
89
|
+
: `About ${money(r.bill.total)} a month: the ${money(r.bill.base)} Workers Paid base, with nothing over the included amounts.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SITE_ROWS: Array<{ label: string; value: (t: Totals) => string }> = [
|
|
93
|
+
{ label: "Workers requests", value: (t) => num(t.workers.requests) },
|
|
94
|
+
{ label: "Workers CPU time", value: (t) => cpu(t.workers.cpuMs) },
|
|
95
|
+
{ label: "D1 rows read", value: (t) => num(t.d1.rowsRead) },
|
|
96
|
+
{ label: "D1 rows written", value: (t) => num(t.d1.rowsWritten) },
|
|
97
|
+
{ label: "D1 queries", value: (t) => num(t.d1.readQueries + t.d1.writeQueries) },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function Setup(props: { reason: string }) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="space-y-3 text-sm">
|
|
103
|
+
<p className="text-kumo-subtle">{props.reason}</p>
|
|
104
|
+
<ol className="space-y-1" style={{ listStyle: "decimal", paddingInlineStart: "1.25rem" }}>
|
|
105
|
+
<li>Cloudflare dashboard → Manage Account → Account API Tokens → Create Token → Create Custom Token.</li>
|
|
106
|
+
<li>Add Account → Account Analytics → Read.</li>
|
|
107
|
+
<li>Add Workers → Metadata Read-Only, for All Workers (finds this site's Worker and database, and reads Workers Logs for the cache-warming estimate).</li>
|
|
108
|
+
<li>Add Account → Account Settings → Read (detects your plan).</li>
|
|
109
|
+
<li>Create the token and copy it (it's shown once).</li>
|
|
110
|
+
<li>
|
|
111
|
+
Set it as the <code>USAGE_API_TOKEN</code> Worker secret (one token works for every site in the account), or paste it in Cloudflare usage on the plugin's Settings page.
|
|
112
|
+
</li>
|
|
113
|
+
</ol>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The Performance page card. */
|
|
119
|
+
export function UsageCard() {
|
|
120
|
+
const { data, error, loading, load } = useUsage();
|
|
121
|
+
return (
|
|
122
|
+
<section className="rounded-lg border border-kumo-line" aria-labelledby="cw-usage-title">
|
|
123
|
+
<div className="flex flex-wrap items-start justify-between gap-4 border-b border-kumo-line p-4">
|
|
124
|
+
<div className="min-w-0 max-w-2xl">
|
|
125
|
+
<h2 id="cw-usage-title" className="text-base font-semibold">
|
|
126
|
+
Cloudflare usage
|
|
127
|
+
</h2>
|
|
128
|
+
<p className="mt-1 text-sm leading-5 text-pretty text-kumo-subtle">
|
|
129
|
+
What this site uses on Cloudflare (Workers and its D1 database), and the whole account against its plan. Read from Cloudflare's analytics and kept for an hour.
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
{data?.configured && (
|
|
133
|
+
<Button className="shrink-0" variant="secondary" icon={<ArrowsClockwise />} disabled={loading} onClick={() => void load(true)}>
|
|
134
|
+
{loading ? "Refreshing…" : "Refresh"}
|
|
135
|
+
</Button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
<div className="space-y-6 p-4">
|
|
139
|
+
<p className="sr-only" role="status">
|
|
140
|
+
{loading ? "Loading Cloudflare usage…" : data?.configured ? `Cloudflare usage updated ${new Date(data.at).toLocaleString()}.` : ""}
|
|
141
|
+
</p>
|
|
142
|
+
{error && <Banner variant="error" role="alert" description={error} />}
|
|
143
|
+
{!data && !error && (
|
|
144
|
+
<div className="flex justify-center py-6">
|
|
145
|
+
<Loader />
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
{data && !data.configured && <Setup reason={data.reason} />}
|
|
149
|
+
{data?.configured && <Details r={data} />}
|
|
150
|
+
</div>
|
|
151
|
+
</section>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function Details(props: { r: Report }) {
|
|
156
|
+
const { r } = props;
|
|
157
|
+
const plan = allowances(r);
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
<div className="space-y-1">
|
|
161
|
+
<p className="text-base font-semibold">{billText(r)}</p>
|
|
162
|
+
<p className="text-sm text-kumo-subtle">
|
|
163
|
+
{planName(r)}
|
|
164
|
+
{r.planSource === "usage" ? " (worked out from usage over Free's limits)" : ""}. Updated {new Date(r.at).toLocaleString()}. An estimate for 30 days at this rate; Cloudflare's invoice is the final word.
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div>
|
|
169
|
+
<h3 className="text-sm font-medium">The account</h3>
|
|
170
|
+
<p className="text-sm text-kumo-subtle">All Workers and databases in the account, {plan.window}.</p>
|
|
171
|
+
<div className="mt-2 overflow-x-auto">
|
|
172
|
+
<table className="w-full text-left text-sm">
|
|
173
|
+
<thead className="text-kumo-subtle">
|
|
174
|
+
<tr>
|
|
175
|
+
<th scope="col" className="py-1 pr-4 font-medium">
|
|
176
|
+
Usage
|
|
177
|
+
</th>
|
|
178
|
+
<th scope="col" className="py-1 pr-4 font-medium">
|
|
179
|
+
Used
|
|
180
|
+
</th>
|
|
181
|
+
<th scope="col" className="py-1 pr-4 font-medium">
|
|
182
|
+
Included
|
|
183
|
+
</th>
|
|
184
|
+
<th scope="col" className="py-1 pr-4 font-medium">
|
|
185
|
+
Share
|
|
186
|
+
</th>
|
|
187
|
+
</tr>
|
|
188
|
+
</thead>
|
|
189
|
+
<tbody className="divide-y divide-kumo-line">
|
|
190
|
+
{plan.rows.map((row) => (
|
|
191
|
+
<tr key={row.label}>
|
|
192
|
+
<th scope="row" className="py-2 pr-4 font-normal">
|
|
193
|
+
{row.label}
|
|
194
|
+
</th>
|
|
195
|
+
<td className="py-2 pr-4">{row.show(row.used)}</td>
|
|
196
|
+
<td className="py-2 pr-4">{row.show(row.of)}</td>
|
|
197
|
+
<td className="py-2 pr-4">
|
|
198
|
+
<span className="inline-flex items-center gap-2">
|
|
199
|
+
<meter min={0} max={1} low={0.5} high={0.8} optimum={0} value={Math.min(1, row.of ? row.used / row.of : 0)} aria-label={`${row.label}: ${pct(row.used, row.of)} of the allowance`} />
|
|
200
|
+
{pct(row.used, row.of)}
|
|
201
|
+
</span>
|
|
202
|
+
</td>
|
|
203
|
+
</tr>
|
|
204
|
+
))}
|
|
205
|
+
</tbody>
|
|
206
|
+
</table>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div>
|
|
211
|
+
<h3 className="text-sm font-medium">This site</h3>
|
|
212
|
+
<p className="text-sm text-kumo-subtle">
|
|
213
|
+
{r.worker ? `Worker ${r.worker}` : "This site's Worker wasn't found"}
|
|
214
|
+
{r.databaseId ? `, D1 database ${bytes(r.storage.siteBytes)}` : ""}.
|
|
215
|
+
</p>
|
|
216
|
+
{r.worker && (
|
|
217
|
+
<div className="mt-2 overflow-x-auto">
|
|
218
|
+
<table className="w-full text-left text-sm">
|
|
219
|
+
<thead className="text-kumo-subtle">
|
|
220
|
+
<tr>
|
|
221
|
+
<th scope="col" className="py-1 pr-4 font-medium">
|
|
222
|
+
Usage
|
|
223
|
+
</th>
|
|
224
|
+
<th scope="col" className="py-1 pr-4 font-medium">
|
|
225
|
+
Today (UTC)
|
|
226
|
+
</th>
|
|
227
|
+
<th scope="col" className="py-1 pr-4 font-medium">
|
|
228
|
+
Last 7 days
|
|
229
|
+
</th>
|
|
230
|
+
<th scope="col" className="py-1 pr-4 font-medium">
|
|
231
|
+
Last 30 days
|
|
232
|
+
</th>
|
|
233
|
+
</tr>
|
|
234
|
+
</thead>
|
|
235
|
+
<tbody className="divide-y divide-kumo-line">
|
|
236
|
+
{SITE_ROWS.filter((row) => r.databaseId || !row.label.startsWith("D1")).map((row) => (
|
|
237
|
+
<tr key={row.label}>
|
|
238
|
+
<th scope="row" className="py-2 pr-4 font-normal">
|
|
239
|
+
{row.label}
|
|
240
|
+
</th>
|
|
241
|
+
<td className="py-2 pr-4">{row.value(r.periods.day.site)}</td>
|
|
242
|
+
<td className="py-2 pr-4">{row.value(r.periods.week.site)}</td>
|
|
243
|
+
<td className="py-2 pr-4">{row.value(r.periods.month.site)}</td>
|
|
244
|
+
</tr>
|
|
245
|
+
))}
|
|
246
|
+
</tbody>
|
|
247
|
+
</table>
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{r.warming && (
|
|
253
|
+
<div>
|
|
254
|
+
<h3 className="text-sm font-medium">Cache warming (estimate)</h3>
|
|
255
|
+
<p className="text-sm text-kumo-subtle">
|
|
256
|
+
{r.warming.visits
|
|
257
|
+
? `Last ${r.warming.days} days: about ${num(r.warming.visits)} warming visits, ${pct(r.warming.requestShare, 1)} of this site's requests and ${pct(r.warming.cpuShare, 1)} of its CPU time (${cpu(r.warming.cpuMs)}), and about ${num(r.warming.rowsRead)} D1 rows read. Estimated from sampled Workers Logs; D1 rows are this site's total times the warmer's share of requests.`
|
|
258
|
+
: `No warming visits in the last ${r.warming.days} days.`}
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{r.notes.length > 0 && <Banner variant="default" description={r.notes.join(" ")} />}
|
|
264
|
+
</>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The dashboard widget: the bill and the account's three biggest shares. */
|
|
269
|
+
export function UsageWidget() {
|
|
270
|
+
const { data, error } = useUsage();
|
|
271
|
+
if (error) return <p className="text-sm text-kumo-subtle">{error}</p>;
|
|
272
|
+
if (!data) return <Loader />;
|
|
273
|
+
const link = (
|
|
274
|
+
<a className="text-kumo-link hover:underline" href={`${ADMIN}/plugins/coywolf-pack/performance`}>
|
|
275
|
+
{data.configured ? "Open Performance" : "Set it up on Performance"}
|
|
276
|
+
</a>
|
|
277
|
+
);
|
|
278
|
+
if (!data.configured) {
|
|
279
|
+
return (
|
|
280
|
+
<div className="space-y-1 text-sm">
|
|
281
|
+
<p className="text-kumo-subtle">Add a read-only Cloudflare API token to see usage and the estimated bill.</p>
|
|
282
|
+
{link}
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const top = [...allowances(data).rows].sort((a, b) => b.used / (b.of || 1) - a.used / (a.of || 1)).slice(0, 3);
|
|
287
|
+
return (
|
|
288
|
+
<div className="space-y-1 text-sm">
|
|
289
|
+
<p className="font-medium">{data.plan === "free" ? "Workers Free" : `About ${money(data.bill.total)} a month`}</p>
|
|
290
|
+
<ul className="text-kumo-subtle">
|
|
291
|
+
{top.map((row) => (
|
|
292
|
+
<li key={row.label}>
|
|
293
|
+
{row.label}: {pct(row.used, row.of)} of the {data.plan === "free" ? "daily limit" : "monthly allowance"}
|
|
294
|
+
</li>
|
|
295
|
+
))}
|
|
296
|
+
</ul>
|
|
297
|
+
{data.warming && data.warming.visits > 0 && <p className="text-kumo-subtle">Cache warming: about {pct(data.warming.requestShare, 1)} of this site's requests (estimate)</p>}
|
|
298
|
+
{link}
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|