smoodly 0.0.51 → 0.0.53
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/README.md +14 -13
- package/dist/admin/auth.d.ts +3 -11
- package/dist/admin/auth.js +1 -30
- package/dist/admin/client-ops.d.ts +5 -4
- package/dist/admin/client-ops.js +15 -12
- package/dist/admin/delivery-mode.d.ts +1 -1
- package/dist/admin/delivery-mode.js +2 -2
- package/dist/admin/drift.d.ts +0 -2
- package/dist/admin/drift.js +1 -1
- package/dist/admin/editor/EditorView.d.ts +0 -3
- package/dist/admin/editor/EditorView.js +11 -16
- package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
- package/dist/admin/editor/FloatingToolbar.js +1 -1
- package/dist/admin/editor/SeoSettings.d.ts +0 -2
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/editor/canvas-address.d.ts +8 -0
- package/dist/admin/editor/canvas-address.js +18 -2
- package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
- package/dist/admin/editor/canvas-diagnosis.js +3 -0
- package/dist/admin/editor/inline-dom.d.ts +0 -4
- package/dist/admin/editor/inline-dom.js +1 -1
- package/dist/admin/editor/preview-grant.d.ts +6 -0
- package/dist/admin/editor/preview-grant.js +25 -0
- package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/fixed-nodes.d.ts +0 -6
- package/dist/admin/fixed-nodes.js +1 -81
- package/dist/admin/forms/CropEditor.d.ts +4 -2
- package/dist/admin/forms/CropEditor.js +58 -5
- package/dist/admin/forms/ImagePreview.d.ts +3 -1
- package/dist/admin/forms/ImagePreview.js +9 -3
- package/dist/admin/forms/MediaGrid.js +2 -1
- package/dist/admin/forms/MediaPicker.js +9 -6
- package/dist/admin/forms/MediaWidget.js +11 -9
- package/dist/admin/forms/assets-context.d.ts +7 -3
- package/dist/admin/forms/assets-context.js +5 -3
- package/dist/admin/forms/crop-editor.d.ts +24 -2
- package/dist/admin/forms/crop-editor.js +36 -2
- package/dist/admin/forms/date-picker.d.ts +0 -1
- package/dist/admin/forms/date-picker.js +1 -1
- package/dist/admin/forms/tabs.d.ts +0 -1
- package/dist/admin/forms/tabs.js +1 -1
- package/dist/admin/forms/upload.d.ts +18 -11
- package/dist/admin/forms/upload.js +62 -24
- package/dist/admin/global-items.d.ts +0 -3
- package/dist/admin/global-items.js +0 -54
- package/dist/admin/handler.d.ts +22 -28
- package/dist/admin/handler.js +146 -47
- package/dist/admin/health.d.ts +0 -4
- package/dist/admin/index.d.ts +6 -9
- package/dist/admin/index.js +3 -6
- package/dist/admin/ops.d.ts +40 -32
- package/dist/admin/page-data.d.ts +14 -6
- package/dist/admin/page-data.js +1 -1
- package/dist/admin/session-flow.d.ts +65 -0
- package/dist/admin/session-flow.js +121 -0
- package/dist/admin/shell/AdminApp.d.ts +8 -3
- package/dist/admin/shell/AdminApp.js +37 -58
- package/dist/admin/shell/ContentHealthPage.js +1 -3
- package/dist/admin/shell/GlobalFields.d.ts +0 -3
- package/dist/admin/shell/GlobalFields.js +1 -1
- package/dist/admin/shell/IconRail.js +10 -8
- package/dist/admin/shell/MediaScreen.js +9 -6
- package/dist/admin/shell/MembersPage.d.ts +1 -2
- package/dist/admin/shell/MembersPage.js +19 -90
- package/dist/admin/shell/SettingsView.d.ts +1 -2
- package/dist/admin/shell/SettingsView.js +2 -2
- package/dist/admin/shell/SignedOutView.d.ts +5 -0
- package/dist/admin/shell/SignedOutView.js +9 -0
- package/dist/admin/shell/console-links.d.ts +5 -0
- package/dist/admin/shell/console-links.js +11 -0
- package/dist/admin/shell/members-list.d.ts +1 -14
- package/dist/admin/shell/members-list.js +2 -23
- package/dist/admin/shell/pages-tree.d.ts +0 -2
- package/dist/admin/shell/pages-tree.js +1 -1
- package/dist/admin/shell/presence-socket.d.ts +10 -0
- package/dist/admin/shell/presence-socket.js +191 -0
- package/dist/admin/shell/presence.d.ts +6 -18
- package/dist/admin/shell/presence.js +4 -11
- package/dist/admin/shell/session.d.ts +19 -12
- package/dist/admin/shell/session.js +114 -47
- package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
- package/dist/admin/ui/LocaleSwitcher.js +2 -2
- package/dist/admin/ui/primitives.d.ts +4 -14
- package/dist/admin/ui/primitives.js +7 -9
- package/dist/admin/ui/theme.js +13 -2
- package/dist/admin/validate.js +10 -1
- package/dist/api-client.d.ts +26 -0
- package/dist/api-client.js +107 -0
- package/dist/assemble.d.ts +4 -41
- package/dist/assemble.js +5 -303
- package/dist/asset-store.d.ts +28 -30
- package/dist/asset-store.js +26 -121
- package/dist/astro/cache.d.ts +1 -1
- package/dist/astro/components/Head.astro +4 -1
- package/dist/astro/components/Image.astro +12 -17
- package/dist/astro/components/Video.astro +5 -2
- package/dist/astro/head.d.ts +3 -0
- package/dist/astro/head.js +24 -0
- package/dist/astro/index.js +61 -27
- package/dist/astro/locale-paths.d.ts +5 -2
- package/dist/astro/locale-paths.js +10 -2
- package/dist/astro/routes/admin.js +9 -7
- package/dist/astro/runtime.d.ts +3 -2
- package/dist/astro/runtime.js +5 -4
- package/dist/astro/server.d.ts +7 -11
- package/dist/astro/server.js +22 -28
- package/dist/astro/shell.d.ts +3 -3
- package/dist/astro/shell.js +1 -1
- package/dist/astro/static-media-node.d.ts +4 -0
- package/dist/astro/static-media-node.js +25 -0
- package/dist/astro/static-media.d.ts +17 -0
- package/dist/astro/static-media.js +55 -0
- package/dist/astro/static.d.ts +5 -10
- package/dist/astro/static.js +13 -29
- package/dist/canvas-marker.d.ts +14 -0
- package/dist/canvas-marker.js +36 -0
- package/dist/cli/api.d.ts +14 -0
- package/dist/cli/api.js +48 -0
- package/dist/cli/app-env.d.ts +2 -0
- package/dist/cli/app-env.js +20 -0
- package/dist/cli/export.d.ts +16 -2
- package/dist/cli/export.js +52 -35
- package/dist/cli/import.d.ts +7 -11
- package/dist/cli/import.js +50 -54
- package/dist/cli/load-config.d.ts +0 -1
- package/dist/cli/load-config.js +1 -1
- package/dist/cli/main.d.ts +4 -12
- package/dist/cli/main.js +26 -72
- package/dist/cli/push.d.ts +6 -0
- package/dist/cli/push.js +31 -0
- package/dist/collections.d.ts +0 -1
- package/dist/collections.js +1 -1
- package/dist/config.d.ts +20 -5
- package/dist/config.js +18 -1
- package/dist/content/content-api.d.ts +23 -23
- package/dist/content/content-api.js +48 -56
- package/dist/content/mime.d.ts +1 -0
- package/dist/content/mime.js +6 -0
- package/dist/css.js +1 -1
- package/dist/deploy-store.d.ts +19 -0
- package/dist/entry-store.d.ts +8 -94
- package/dist/entry-store.js +9 -557
- package/dist/env.d.ts +27 -20
- package/dist/env.js +38 -50
- package/dist/fields.d.ts +11 -3
- package/dist/fields.js +33 -2
- package/dist/global-id.d.ts +2 -0
- package/dist/global-id.js +4 -2
- package/dist/image/SmoodlyImage.d.ts +9 -5
- package/dist/image/SmoodlyImage.js +29 -15
- package/dist/image/SmoodlyVideo.d.ts +1 -1
- package/dist/image/SmoodlyVideo.js +3 -2
- package/dist/image/index.d.ts +2 -3
- package/dist/image/index.js +0 -2
- package/dist/image/markup.d.ts +53 -0
- package/dist/image/markup.js +111 -0
- package/dist/index.d.ts +13 -23
- package/dist/index.js +7 -20
- package/dist/media-cut.d.ts +44 -0
- package/dist/media-cut.js +70 -0
- package/dist/media.d.ts +58 -4
- package/dist/media.js +126 -16
- package/dist/member-store.d.ts +2 -50
- package/dist/member-store.js +0 -56
- package/dist/next/admin-route.d.ts +1 -1
- package/dist/next/admin-route.js +5 -3
- package/dist/next/meta.js +2 -3
- package/dist/next/page-renderer.d.ts +2 -2
- package/dist/next/page-renderer.js +22 -11
- package/dist/next/queries.d.ts +2 -2
- package/dist/path-index.d.ts +0 -24
- package/dist/path-index.js +0 -98
- package/dist/paths.d.ts +1 -1
- package/dist/paths.js +1 -1
- package/dist/push.d.ts +30 -0
- package/dist/push.js +52 -0
- package/dist/read-counter.d.ts +2 -0
- package/dist/read-counter.js +6 -0
- package/dist/reads.d.ts +7 -7
- package/dist/reads.js +3 -0
- package/dist/registry-json.d.ts +11 -0
- package/dist/registry-json.js +81 -0
- package/dist/render.d.ts +0 -1
- package/dist/render.js +1 -1
- package/dist/resolve.d.ts +12 -6
- package/dist/resolve.js +11 -8
- package/dist/revalidate.d.ts +8 -1
- package/dist/revalidate.js +8 -1
- package/dist/richtext/index.d.ts +1 -1
- package/dist/section-wrapper.d.ts +1 -1
- package/dist/section-wrapper.js +0 -9
- package/dist/signed-message.d.ts +26 -0
- package/dist/signed-message.js +103 -0
- package/dist/site.d.ts +59 -18
- package/dist/site.js +21 -252
- package/dist/space-transfer.d.ts +6 -3
- package/dist/space-transfer.js +18 -6
- package/dist/store.d.ts +2 -80
- package/dist/store.js +3 -489
- package/dist/toolset.d.ts +2 -1
- package/dist/toolset.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -15
- package/dist/admin/auth-config.d.ts +0 -6
- package/dist/admin/browser/fetch-op.d.ts +0 -10
- package/dist/admin/browser/fetch-op.js +0 -44
- package/dist/admin/browser/main.d.ts +0 -1
- package/dist/admin/browser/main.js +0 -23
- package/dist/admin/ops-impl.d.ts +0 -51
- package/dist/admin/ops-impl.js +0 -1403
- package/dist/admin/shell/InviteModal.d.ts +0 -6
- package/dist/admin/shell/InviteModal.js +0 -28
- package/dist/admin/shell/LoginView.d.ts +0 -5
- package/dist/admin/shell/LoginView.js +0 -80
- package/dist/admin/shell/SchemaNotice.d.ts +0 -4
- package/dist/admin/shell/SchemaNotice.js +0 -4
- package/dist/admin/shell/SetPasswordView.d.ts +0 -7
- package/dist/admin/shell/SetPasswordView.js +0 -41
- package/dist/admin/shell/presence-realtime.d.ts +0 -3
- package/dist/admin/shell/presence-realtime.js +0 -32
- package/dist/admin/supabase-auth.d.ts +0 -7
- package/dist/admin/supabase-auth.js +0 -30
- package/dist/astro/deploy-deps.d.ts +0 -10
- package/dist/astro/deploy-deps.js +0 -8
- package/dist/astro/image.d.ts +0 -27
- package/dist/astro/image.js +0 -29
- package/dist/bind.d.ts +0 -19
- package/dist/bind.js +0 -34
- package/dist/cli/add-admin.d.ts +0 -11
- package/dist/cli/add-admin.js +0 -25
- package/dist/cli/load-collections.d.ts +0 -2
- package/dist/cli/load-collections.js +0 -27
- package/dist/cli/migrate.d.ts +0 -43
- package/dist/cli/migrate.js +0 -138
- package/dist/cli/rebuild.d.ts +0 -23
- package/dist/cli/rebuild.js +0 -22
- package/dist/cli/space-io.d.ts +0 -41
- package/dist/cli/space-io.js +0 -84
- package/dist/client.d.ts +0 -11
- package/dist/client.js +0 -54
- package/dist/current-space.d.ts +0 -4
- package/dist/current-space.js +0 -9
- package/dist/document-stamp.d.ts +0 -7
- package/dist/document-stamp.js +0 -48
- package/dist/image/loaders.d.ts +0 -15
- package/dist/image/loaders.js +0 -5
- package/dist/image/next-config.d.ts +0 -18
- package/dist/image/next-config.js +0 -32
- package/dist/image/remote-pattern.d.ts +0 -1
- package/dist/image/remote-pattern.js +0 -5
- package/dist/migrations.d.ts +0 -37
- package/dist/migrations.js +0 -73
- package/dist/project.d.ts +0 -88
- package/dist/project.js +0 -231
- package/dist/published.d.ts +0 -142
- package/dist/published.js +0 -164
- package/dist/ref-index.d.ts +0 -19
- package/dist/ref-index.js +0 -0
- package/dist/schema-read.d.ts +0 -23
- package/dist/schema-read.js +0 -50
- package/dist/schema-version.d.ts +0 -19
- package/dist/schema-version.js +0 -34
- package/dist/sql-space.d.ts +0 -44
- package/dist/sql-space.js +0 -289
- package/dist/sql.d.ts +0 -15
- package/dist/sql.js +0 -556
- package/dist/supabase-asset-store.d.ts +0 -21
- package/dist/supabase-asset-store.js +0 -170
- package/dist/supabase-entry-store.d.ts +0 -92
- package/dist/supabase-entry-store.js +0 -824
- package/dist/supabase-member-store.d.ts +0 -21
- package/dist/supabase-member-store.js +0 -96
- package/dist/supabase-path-index.d.ts +0 -30
- package/dist/supabase-path-index.js +0 -138
- package/dist/supabase-published-store.d.ts +0 -56
- package/dist/supabase-published-store.js +0 -314
- package/dist/supabase-store.d.ts +0 -103
- package/dist/supabase-store.js +0 -678
- package/migrations/20260926120000_smoodly_v1.sql +0 -535
- package/migrations/20260926130000_smoodly_v2.sql +0 -26
- package/migrations/20260926140000_smoodly_v3.sql +0 -38
- package/migrations/20260926150000_smoodly_v4.sql +0 -41
- package/migrations/20260929120000_smoodly_v5.sql +0 -56
- package/migrations/20260929135523_smoodly_v6.sql +0 -200
- package/migrations/20260930040956_smoodly_v7.sql +0 -84
- package/migrations/20261007120000_smoodly_v8.sql +0 -154
- package/migrations/20261007200000_smoodly_v9.sql +0 -30
- package/migrations/20261008120000_smoodly_v10.sql +0 -47
- package/migrations/20261009120000_smoodly_v11.sql +0 -49
- /package/dist/{admin/auth-config.js → deploy-store.js} +0 -0
package/dist/admin/ops.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type { PageRecord } from "../store.ts";
|
|
|
2
2
|
import type { PageTree } from "../page-context.ts";
|
|
3
3
|
import type { EntryRecord, EntryUsage } from "../entry-store.ts";
|
|
4
4
|
import type { AssetRecord, AssetMeta } from "../media.ts";
|
|
5
|
-
import type { AssetPage, AssetQuery
|
|
6
|
-
import type {
|
|
5
|
+
import type { AssetPage, AssetQuery } from "../asset-store.ts";
|
|
6
|
+
import type { MemberRecord } from "../member-store.ts";
|
|
7
7
|
import type { MemberRole } from "../permissions.ts";
|
|
8
8
|
import type { FieldError } from "./validate.ts";
|
|
9
9
|
import type { DeployState } from "../deploy-state.ts";
|
|
@@ -11,9 +11,11 @@ import type { DraftToken } from "../draft-token.ts";
|
|
|
11
11
|
import type { PathTarget } from "../paths.ts";
|
|
12
12
|
import type { Finding } from "./drift.ts";
|
|
13
13
|
import type { HealthReport } from "./health.ts";
|
|
14
|
+
import type { DeployStore } from "../deploy-store.ts";
|
|
14
15
|
export type OpError = {
|
|
15
16
|
ok: false;
|
|
16
|
-
|
|
17
|
+
/** `no_schema`: the space has no registry yet — its site has not pushed one (spec 2026-10-16 the control plane §4). */
|
|
18
|
+
code: "not_found" | "conflict" | "blocked" | "validation" | "internal" | "unauthorized" | "no_schema";
|
|
17
19
|
message: string;
|
|
18
20
|
fields?: FieldError[];
|
|
19
21
|
/** A `conflict` from the token guard (spec 2026-09-26 concurrency §2): the draft moved since the client loaded it. */
|
|
@@ -28,6 +30,15 @@ export type OpResult<T> = {
|
|
|
28
30
|
ok: true;
|
|
29
31
|
data: T;
|
|
30
32
|
} | OpError;
|
|
33
|
+
/** A draft-cookie grant: the service signs it under the site's secret key (purpose "preview", body the
|
|
34
|
+
* action), the browser posts it to the site's preview route, which sets the cookie (spec 2026-10-14 §1). */
|
|
35
|
+
/** The service's signature over the preview action, for the site to verify (signed-message.ts), and the space it names. */
|
|
36
|
+
export type PreviewGrant = {
|
|
37
|
+
ts: number;
|
|
38
|
+
kid: string;
|
|
39
|
+
sig: string;
|
|
40
|
+
space: string;
|
|
41
|
+
};
|
|
31
42
|
export type PageCreateInput = {
|
|
32
43
|
parentId: string | null;
|
|
33
44
|
/** The one locale the page is created in; others are added with addLocale. */
|
|
@@ -115,16 +126,11 @@ export type AssetUsageItem = {
|
|
|
115
126
|
title: string;
|
|
116
127
|
href: string | null;
|
|
117
128
|
};
|
|
118
|
-
/** The Members page's read (spec 2026-09-25 §4): the rows
|
|
129
|
+
/** The Members page's read (spec 2026-09-25 §4): the rows; the dashboard manages them. */
|
|
119
130
|
export type MembersListResult = {
|
|
120
131
|
members: MemberRecord[];
|
|
121
|
-
consoleUrl: string | null;
|
|
122
132
|
};
|
|
123
133
|
/** `database` null = no smoodly_meta table (older than v1). Behind blocks the shell; ahead is silent (spec 2026-09-26 §5). */
|
|
124
|
-
export type SchemaStatus = {
|
|
125
|
-
database: number | null;
|
|
126
|
-
required: number;
|
|
127
|
-
};
|
|
128
134
|
/** The Deploy tile's state (spec 2026-10-09 static mode §6); `hook` false = SMOODLY_DEPLOY_HOOK is unset, Deploy disabled. */
|
|
129
135
|
export type DeployStatus = DeployState & {
|
|
130
136
|
hook: boolean;
|
|
@@ -241,32 +247,19 @@ export type AdminOps = {
|
|
|
241
247
|
/** Ready assets, newest first, paged by cursor; `kind` and `search` filter. */
|
|
242
248
|
list(query?: AssetQuery): Promise<OpResult<AssetPage>>;
|
|
243
249
|
get(id: string): Promise<OpResult<AssetRecord>>;
|
|
244
|
-
/** Validates type and size against config.media, inserts a pending row, mints the signed upload URL (spec 2026-09-08 §6). */
|
|
245
|
-
beginUpload(input: {
|
|
246
|
-
name: string;
|
|
247
|
-
mime: string;
|
|
248
|
-
size: number;
|
|
249
|
-
}): Promise<OpResult<UploadTicket>>;
|
|
250
|
-
/** Marks the row ready with what the browser measured. */
|
|
251
|
-
finishUpload(id: string, dims: FinishInput): Promise<OpResult<AssetRecord>>;
|
|
252
250
|
update(id: string, meta: AssetMeta): Promise<OpResult<AssetRecord>>;
|
|
253
251
|
/** Blocked while any draft or published version references the asset. */
|
|
254
252
|
delete(id: string): Promise<OpResult<null>>;
|
|
255
253
|
usage(id: string): Promise<OpResult<AssetUsageItem[]>>;
|
|
256
254
|
};
|
|
257
255
|
members: {
|
|
258
|
-
/** The space's members
|
|
256
|
+
/** The space's members, a read-only list; admins only. Invites, roles and removals are the dashboard's
|
|
257
|
+
* (spec 2026-10-16 sign-in and the dashboard §6). */
|
|
259
258
|
list(): Promise<OpResult<MembersListResult>>;
|
|
260
259
|
/** The caller's role; every member. The shell hides Settings from editors with it. */
|
|
261
260
|
me(): Promise<OpResult<{
|
|
262
261
|
role: MemberRole;
|
|
263
262
|
}>>;
|
|
264
|
-
/** Admins only, self-host only: invites (or adds an existing account), refuses a duplicate row (spec 2026-09-25 members management §3). */
|
|
265
|
-
invite(input: InviteInput): Promise<OpResult<InviteResult>>;
|
|
266
|
-
/** Admins only, self-host only; never your own row, never the last admin. */
|
|
267
|
-
setRole(userId: string, role: MemberRole): Promise<OpResult<MemberRecord>>;
|
|
268
|
-
/** Admins only, self-host only; the same two guards. The Auth account stays. */
|
|
269
|
-
remove(userId: string): Promise<OpResult<null>>;
|
|
270
263
|
};
|
|
271
264
|
/** Redirects by hand (spec 2026-09-26 redirects §5): open to every member, as renaming is; nothing here expires a cache. */
|
|
272
265
|
redirects: {
|
|
@@ -284,12 +277,9 @@ export type AdminOps = {
|
|
|
284
277
|
/** Every page, entry and global item against the registry, draft and live; unregistered collections counted. */
|
|
285
278
|
scan(): Promise<OpResult<HealthReport>>;
|
|
286
279
|
};
|
|
287
|
-
|
|
288
|
-
/** Every member. The shell calls it once on mount and blocks when behind. */
|
|
289
|
-
status(): Promise<OpResult<SchemaStatus>>;
|
|
290
|
-
};
|
|
280
|
+
/** enable answers the grant the service signed, which the browser posts to the site's preview route. */
|
|
291
281
|
preview: {
|
|
292
|
-
enable(): Promise<OpResult<null>>;
|
|
282
|
+
enable(): Promise<OpResult<PreviewGrant | null>>;
|
|
293
283
|
disable(): Promise<OpResult<null>>;
|
|
294
284
|
};
|
|
295
285
|
/** Static mode's Deploy tile (spec 2026-10-09 static mode §5–§7); on a live site both answer not_found. */
|
|
@@ -299,12 +289,30 @@ export type AdminOps = {
|
|
|
299
289
|
trigger(): Promise<OpResult<DeployStatus>>;
|
|
300
290
|
};
|
|
301
291
|
};
|
|
302
|
-
/** `token`: the editor's
|
|
303
|
-
*
|
|
304
|
-
* server gate (admin/auth.ts). */
|
|
292
|
+
/** `token`: the editor's pass for this space, attached by the client facade, sent as the Authorization header and
|
|
293
|
+
* verified by the service (spec 2026-10-16 sign-in and the dashboard §2). */
|
|
305
294
|
export type AdminOpCall = {
|
|
306
295
|
path: string;
|
|
307
296
|
args: unknown[];
|
|
308
297
|
token?: string;
|
|
309
298
|
};
|
|
310
299
|
export type AdminOpAction = (call: AdminOpCall) => Promise<OpResult<unknown>>;
|
|
300
|
+
export type AdminEffects = {
|
|
301
|
+
/** Expire one cache tag (`pageTag(id)` / `entryTag(id)`). */
|
|
302
|
+
revalidate?(tag: string): void;
|
|
303
|
+
/** enable may answer a grant (the service's effect); a site's own effect sets the cookie and answers nothing. */
|
|
304
|
+
draft?: {
|
|
305
|
+
enable(): Promise<PreviewGrant | void> | PreviewGrant | void;
|
|
306
|
+
disable(): void | Promise<void>;
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
/** What the deploy operations need (spec 2026-10-09 static mode §7): the live site's origin, where the marker is
|
|
310
|
+
* read from; the host's hook from SMOODLY_DEPLOY_HOOK (absent = Deploy disabled); the deploy store's four facts. */
|
|
311
|
+
export type DeployDeps = {
|
|
312
|
+
siteUrl: string;
|
|
313
|
+
hook?: string;
|
|
314
|
+
store: DeployStore;
|
|
315
|
+
/** Test seams. */
|
|
316
|
+
fetch?: typeof fetch;
|
|
317
|
+
now?: () => number;
|
|
318
|
+
};
|
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import type { PresenceConfig } from "../env.ts";
|
|
2
2
|
import type { AdminRegistry } from "./serialize.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SignInConfig } from "./session-flow.ts";
|
|
4
4
|
export type AdminPageData = {
|
|
5
5
|
registry: AdminRegistry;
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
/**
|
|
6
|
+
/** Where the browser sends a person to sign in and the dashboard it links to: public values only. */
|
|
7
|
+
signIn: SignInConfig;
|
|
8
|
+
/** Smoodly's API, without a trailing slash, and the site's space: the admin's session routes live under it. */
|
|
9
|
+
apiUrl: string;
|
|
10
|
+
spaceId: string;
|
|
11
|
+
/** Where the browser opens the editing-presence socket; null = presence is off. */
|
|
9
12
|
presence: PresenceConfig | null;
|
|
10
|
-
/** Where operations are posted. */
|
|
13
|
+
/** Where operations are posted: the service's admin API. */
|
|
11
14
|
opUrl: string;
|
|
15
|
+
/** The site's own route the browser posts a preview grant to, which sets the draft cookie. */
|
|
16
|
+
previewUrl: string;
|
|
12
17
|
/** The folder the package's files are served from, without a trailing slash. */
|
|
13
18
|
filesBase: string;
|
|
14
19
|
/** What the editor puts before a page's address to load its draft: "" where the framework serves drafts
|
|
15
20
|
* at the page's own address (Next), the preview address's base where it does not (Astro). */
|
|
16
21
|
previewBase: string;
|
|
22
|
+
/** How the canvas and History's frame mark what they want of a draft render: in the query, or in the path's
|
|
23
|
+
* last segment where the host strips the query (Next: Vercel keeps only a prerendered route's own params). */
|
|
24
|
+
canvasMarks: "path" | "query";
|
|
17
25
|
/** Static mode shows the Deploy tile (spec 2026-10-09 static mode §6); live by default. */
|
|
18
26
|
delivery: "live" | "static";
|
|
19
27
|
};
|
|
20
28
|
export declare const ADMIN_DATA_ID = "smoodly-admin-data";
|
|
21
29
|
export declare const ADMIN_ROOT_ID = "smoodly-admin-root";
|
|
22
|
-
/** The reserved segment under the admin path: the
|
|
30
|
+
/** The reserved segment under the admin path: the files and the revalidate and preview routes live here. */
|
|
23
31
|
export declare const ADMIN_INTERNAL = "_smoodly";
|
package/dist/admin/page-data.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export const ADMIN_DATA_ID = "smoodly-admin-data";
|
|
2
2
|
export const ADMIN_ROOT_ID = "smoodly-admin-root";
|
|
3
|
-
/** The reserved segment under the admin path: the
|
|
3
|
+
/** The reserved segment under the admin path: the files and the revalidate and preview routes live here. */
|
|
4
4
|
export const ADMIN_INTERNAL = "_smoodly";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { MemberRole } from "../permissions.ts";
|
|
2
|
+
/** Where the admin sends a person to sign in, and the dashboard its account menu and Members page link to (spec
|
|
3
|
+
* 2026-10-16 sign-in and the dashboard §1, §6): `GET /v1/auth` answers it from the Worker's env; the handler and the
|
|
4
|
+
* page data carry it. Public values only. */
|
|
5
|
+
export type SignInConfig = {
|
|
6
|
+
signInUrl: string;
|
|
7
|
+
consoleUrl: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const STORAGE_KEY: (space: string) => string;
|
|
10
|
+
/** The PKCE verifier, in this tab's `sessionStorage` only: the code is good for the browser tab that asked for it. */
|
|
11
|
+
export declare const VERIFIER_KEY = "smoodly-verifier";
|
|
12
|
+
/** Renew this long before the pass expires (spec §2). */
|
|
13
|
+
export declare const RENEW_BEFORE_MS = 300000;
|
|
14
|
+
export type StoredSession = {
|
|
15
|
+
pass: string;
|
|
16
|
+
refresh: string;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
user: {
|
|
19
|
+
userId: string;
|
|
20
|
+
email: string | null;
|
|
21
|
+
role: MemberRole;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/** Where the session lives (`localStorage`, shared by the site's tabs) and where the verifier waits (`sessionStorage`). */
|
|
25
|
+
export type SessionStores = {
|
|
26
|
+
local: Storage;
|
|
27
|
+
tab: Storage;
|
|
28
|
+
};
|
|
29
|
+
/** The session the site's tabs share, or null (none, or a value that is not one). */
|
|
30
|
+
export declare function readSession(local: Storage, space: string): StoredSession | null;
|
|
31
|
+
/** Where to send the person to sign in: a fresh verifier kept in this tab, its S256 as the challenge. */
|
|
32
|
+
export declare function signInAddress(config: SignInConfig, space: string, returnUrl: string, stores: Pick<SessionStores, "tab">): Promise<string>;
|
|
33
|
+
/** The one-time code the sign-in page put in the fragment, or null. */
|
|
34
|
+
export declare function codeFrom(hash: string): string | null;
|
|
35
|
+
/** What the admin shows when the service cannot be reached at sign-in: a refusal with a way back, never a blank screen. */
|
|
36
|
+
export declare const UNREACHABLE = "smoodly: couldn't reach Smoodly to sign you in \u2014 check the connection and sign in again.";
|
|
37
|
+
/** The code and this tab's verifier for a session, stored; or why not. The verifier is spent either way. A service
|
|
38
|
+
* that cannot be reached is a refusal too (UNREACHABLE), so the shell shows it rather than waiting forever. */
|
|
39
|
+
export declare function exchange(api: string, space: string, code: string, stores: SessionStores, doFetch?: typeof fetch): Promise<StoredSession | {
|
|
40
|
+
refused: string;
|
|
41
|
+
}>;
|
|
42
|
+
/** The load-time step with no code in the fragment: the stored session adopted as it is while its pass has more than
|
|
43
|
+
* RENEW_BEFORE_MS left, else renewed; a renewal the service refuses sends the person to sign in; one that cannot reach
|
|
44
|
+
* the service keeps a pass that is still valid (the next timer tries again) and refuses with UNREACHABLE only once the
|
|
45
|
+
* pass has expired. No stored session: sign in. */
|
|
46
|
+
export declare function resume(api: string, space: string, stores: Pick<SessionStores, "local">, now: number, doFetch?: typeof fetch, lock?: TabLock): Promise<{
|
|
47
|
+
kind: "adopt";
|
|
48
|
+
session: StoredSession;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "sign_in";
|
|
51
|
+
} | {
|
|
52
|
+
kind: "refused";
|
|
53
|
+
message: string;
|
|
54
|
+
}>;
|
|
55
|
+
/** Runs `fn` while holding a lock no other tab of this origin holds (Web Locks); with no Web Locks (an insecure
|
|
56
|
+
* context), at once. */
|
|
57
|
+
export type TabLock = <T>(name: string, fn: () => Promise<T>) => Promise<T>;
|
|
58
|
+
/** A renewed session, or null when the space refused the refresh (the stored session is then cleared). `held` is the
|
|
59
|
+
* refresh this tab holds: when the stored one differs, another tab renewed first and its session is adopted rather
|
|
60
|
+
* than posting a refresh that is already spent. The read and the post run under one lock per space across the site's
|
|
61
|
+
* tabs, so the tab that renews writes before the others read (spec §2), even when every tab's timer fires at once
|
|
62
|
+
* after a laptop wakes. A network failure throws and keeps the session. */
|
|
63
|
+
export declare function renew(api: string, space: string, stores: Pick<SessionStores, "local">, held: string | null, doFetch?: typeof fetch, lock?: TabLock): Promise<StoredSession | null>;
|
|
64
|
+
/** Sign out of this site: the space ends the session, and this browser forgets it even when the call fails. */
|
|
65
|
+
export declare function endSession(api: string, space: string, stores: Pick<SessionStores, "local">, doFetch?: typeof fetch): Promise<void>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const STORAGE_KEY = (space) => `smoodly-session:${space}`;
|
|
2
|
+
/** The PKCE verifier, in this tab's `sessionStorage` only: the code is good for the browser tab that asked for it. */
|
|
3
|
+
export const VERIFIER_KEY = "smoodly-verifier";
|
|
4
|
+
/** Renew this long before the pass expires (spec §2). */
|
|
5
|
+
export const RENEW_BEFORE_MS = 300000;
|
|
6
|
+
const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
7
|
+
const s256 = async (verifier) => base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
|
|
8
|
+
const sessionUrl = (api, space, path) => `${api.replace(/\/+$/, "")}/v1/spaces/${encodeURIComponent(space)}/admin/${path}`;
|
|
9
|
+
const isSession = (v) => {
|
|
10
|
+
const s = v;
|
|
11
|
+
return typeof s === "object" && s !== null && typeof s.pass === "string" && typeof s.refresh === "string" && typeof s.expiresAt === "number"
|
|
12
|
+
&& typeof s.user === "object" && s.user !== null && typeof s.user.userId === "string";
|
|
13
|
+
};
|
|
14
|
+
/** The session the site's tabs share, or null (none, or a value that is not one). */
|
|
15
|
+
export function readSession(local, space) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(local.getItem(STORAGE_KEY(space)) ?? "null");
|
|
18
|
+
return isSession(value) ? value : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const writeSession = (local, space, s) => local.setItem(STORAGE_KEY(space), JSON.stringify(s));
|
|
25
|
+
/** Where to send the person to sign in: a fresh verifier kept in this tab, its S256 as the challenge. */
|
|
26
|
+
export async function signInAddress(config, space, returnUrl, stores) {
|
|
27
|
+
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
28
|
+
stores.tab.setItem(VERIFIER_KEY, verifier);
|
|
29
|
+
const query = new URLSearchParams({ space, return: returnUrl, challenge: await s256(verifier) });
|
|
30
|
+
return `${config.signInUrl}?${query}`;
|
|
31
|
+
}
|
|
32
|
+
/** The one-time code the sign-in page put in the fragment, or null. */
|
|
33
|
+
export function codeFrom(hash) {
|
|
34
|
+
return new URLSearchParams(hash.replace(/^#/, "")).get("smoodly_code") || null;
|
|
35
|
+
}
|
|
36
|
+
async function post(doFetch, url, body) {
|
|
37
|
+
const res = await doFetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
38
|
+
const json = (await res.json().catch(() => null));
|
|
39
|
+
return { status: res.status, data: json?.ok ? json.data : null, message: typeof json?.message === "string" ? json.message : null };
|
|
40
|
+
}
|
|
41
|
+
/** What the admin shows when the service cannot be reached at sign-in: a refusal with a way back, never a blank screen. */
|
|
42
|
+
export const UNREACHABLE = "smoodly: couldn't reach Smoodly to sign you in — check the connection and sign in again.";
|
|
43
|
+
/** The code and this tab's verifier for a session, stored; or why not. The verifier is spent either way. A service
|
|
44
|
+
* that cannot be reached is a refusal too (UNREACHABLE), so the shell shows it rather than waiting forever. */
|
|
45
|
+
export async function exchange(api, space, code, stores, doFetch = fetch) {
|
|
46
|
+
const verifier = stores.tab.getItem(VERIFIER_KEY);
|
|
47
|
+
stores.tab.removeItem(VERIFIER_KEY);
|
|
48
|
+
if (!verifier)
|
|
49
|
+
return { refused: "smoodly: this sign-in was started in another tab or has already been used — sign in again." };
|
|
50
|
+
let answer;
|
|
51
|
+
try {
|
|
52
|
+
answer = await post(doFetch, sessionUrl(api, space, "session"), { code, verifier });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { refused: UNREACHABLE };
|
|
56
|
+
}
|
|
57
|
+
if (answer.status !== 200 || !isSession(answer.data))
|
|
58
|
+
return { refused: answer.message ?? `smoodly: the sign-in was refused (${answer.status}).` };
|
|
59
|
+
writeSession(stores.local, space, answer.data);
|
|
60
|
+
return answer.data;
|
|
61
|
+
}
|
|
62
|
+
/** The load-time step with no code in the fragment: the stored session adopted as it is while its pass has more than
|
|
63
|
+
* RENEW_BEFORE_MS left, else renewed; a renewal the service refuses sends the person to sign in; one that cannot reach
|
|
64
|
+
* the service keeps a pass that is still valid (the next timer tries again) and refuses with UNREACHABLE only once the
|
|
65
|
+
* pass has expired. No stored session: sign in. */
|
|
66
|
+
export async function resume(api, space, stores, now, doFetch = fetch, lock = tabLock) {
|
|
67
|
+
const stored = readSession(stores.local, space);
|
|
68
|
+
if (!stored)
|
|
69
|
+
return { kind: "sign_in" };
|
|
70
|
+
if (stored.expiresAt - now > RENEW_BEFORE_MS)
|
|
71
|
+
return { kind: "adopt", session: stored };
|
|
72
|
+
try {
|
|
73
|
+
const renewed = await renew(api, space, stores, stored.refresh, doFetch, lock);
|
|
74
|
+
return renewed ? { kind: "adopt", session: renewed } : { kind: "sign_in" };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return stored.expiresAt > now ? { kind: "adopt", session: stored } : { kind: "refused", message: UNREACHABLE };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const tabLock = (name, fn) => {
|
|
81
|
+
const locks = globalThis.navigator?.locks;
|
|
82
|
+
return locks ? locks.request(name, fn) : fn();
|
|
83
|
+
};
|
|
84
|
+
/** A renewed session, or null when the space refused the refresh (the stored session is then cleared). `held` is the
|
|
85
|
+
* refresh this tab holds: when the stored one differs, another tab renewed first and its session is adopted rather
|
|
86
|
+
* than posting a refresh that is already spent. The read and the post run under one lock per space across the site's
|
|
87
|
+
* tabs, so the tab that renews writes before the others read (spec §2), even when every tab's timer fires at once
|
|
88
|
+
* after a laptop wakes. A network failure throws and keeps the session. */
|
|
89
|
+
export function renew(api, space, stores, held, doFetch = fetch, lock = tabLock) {
|
|
90
|
+
return lock(`smoodly-renew:${space}`, () => renewHeld(api, space, stores, held, doFetch));
|
|
91
|
+
}
|
|
92
|
+
async function renewHeld(api, space, stores, held, doFetch) {
|
|
93
|
+
const stored = readSession(stores.local, space);
|
|
94
|
+
if (!stored)
|
|
95
|
+
return null;
|
|
96
|
+
if (held !== null && stored.refresh !== held)
|
|
97
|
+
return stored;
|
|
98
|
+
const answer = await post(doFetch, sessionUrl(api, space, "session/refresh"), { refresh: stored.refresh });
|
|
99
|
+
if (answer.status === 200 && isSession(answer.data)) {
|
|
100
|
+
writeSession(stores.local, space, answer.data);
|
|
101
|
+
return answer.data;
|
|
102
|
+
}
|
|
103
|
+
if (answer.status === 401 || answer.status === 403) {
|
|
104
|
+
// Cleared only if no other tab stored a newer one meanwhile.
|
|
105
|
+
if (readSession(stores.local, space)?.refresh === stored.refresh)
|
|
106
|
+
stores.local.removeItem(STORAGE_KEY(space));
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(answer.message ?? `smoodly: the session could not be renewed (${answer.status}).`);
|
|
110
|
+
}
|
|
111
|
+
/** Sign out of this site: the space ends the session, and this browser forgets it even when the call fails. */
|
|
112
|
+
export async function endSession(api, space, stores, doFetch = fetch) {
|
|
113
|
+
const stored = readSession(stores.local, space);
|
|
114
|
+
stores.local.removeItem(STORAGE_KEY(space));
|
|
115
|
+
if (!stored)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
await post(doFetch, sessionUrl(api, space, "session/end"), { refresh: stored.refresh });
|
|
119
|
+
}
|
|
120
|
+
catch { /* the session lapses on its own */ }
|
|
121
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { AdminOpAction } from "../ops.ts";
|
|
2
2
|
import type { AdminRegistry } from "../serialize.ts";
|
|
3
|
-
import {
|
|
3
|
+
import type { SignInConfig } from "../session-flow.ts";
|
|
4
4
|
import type { PresenceConfig } from "../../env.ts";
|
|
5
|
-
export declare function AdminApp({ registry, op,
|
|
5
|
+
export declare function AdminApp({ registry, op, signIn, api, presenceConfig }: {
|
|
6
6
|
registry: AdminRegistry;
|
|
7
7
|
op: AdminOpAction;
|
|
8
|
-
|
|
8
|
+
signIn: SignInConfig;
|
|
9
|
+
/** Smoodly's API and the site's space: the session routes live under it. */
|
|
10
|
+
api: {
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
spaceId: string;
|
|
13
|
+
};
|
|
9
14
|
presenceConfig?: PresenceConfig;
|
|
10
15
|
}): import("react").JSX.Element;
|
|
@@ -20,9 +20,8 @@ import { EntryForm } from "./EntryForm.js";
|
|
|
20
20
|
import { GlobalList } from "./GlobalList.js";
|
|
21
21
|
import { GlobalForm } from "./GlobalForm.js";
|
|
22
22
|
import { EditorView } from "../editor/EditorView.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { useAdminSession } from "./session.js";
|
|
23
|
+
import { SignedOutView } from "./SignedOutView.js";
|
|
24
|
+
import { useSpaceSession } from "./session.js";
|
|
26
25
|
import { IconRail } from "./IconRail.js";
|
|
27
26
|
import { ConfirmHost } from "../ui/ConfirmDialog.js";
|
|
28
27
|
import { notify } from "../ui/confirm.js";
|
|
@@ -33,23 +32,27 @@ import { ErrorPane } from "./ErrorPane.js";
|
|
|
33
32
|
import { MediaScreen } from "./MediaScreen.js";
|
|
34
33
|
import { SettingsView } from "./SettingsView.js";
|
|
35
34
|
import { AssetsProvider } from "../forms/assets-context.js";
|
|
35
|
+
import { xhrSend } from "../forms/upload.js";
|
|
36
36
|
import { ElementsProvider } from "../forms/elements-context.js";
|
|
37
37
|
import { TimeZoneProvider } from "../forms/time-zone-context.js";
|
|
38
|
-
import { SchemaNotice } from "./SchemaNotice.js";
|
|
39
|
-
import { schemaBehind } from "../../schema-version.js";
|
|
40
38
|
import { delivery, onPublished } from "../delivery-mode.js";
|
|
41
39
|
import { pollEvery } from "./deploy-tile.js";
|
|
42
|
-
import { createPresence
|
|
43
|
-
import {
|
|
40
|
+
import { createPresence } from "./presence.js";
|
|
41
|
+
import { connectSocket } from "./presence-socket.js";
|
|
44
42
|
import { PresenceProvider } from "./presence-context.js";
|
|
45
|
-
export function AdminApp({ registry, op,
|
|
46
|
-
const session =
|
|
47
|
-
//
|
|
48
|
-
|
|
43
|
+
export function AdminApp({ registry, op, signIn, api, presenceConfig }) {
|
|
44
|
+
const session = useSpaceSession({ apiUrl: api.apiUrl, spaceId: api.spaceId, signIn });
|
|
45
|
+
// The session's getters read refs, so the facade made once always sends the current pass; a refused pass renews
|
|
46
|
+
// once and the call is sent again, or the person is sent to sign in.
|
|
47
|
+
const renewRef = useRef(session.renew);
|
|
48
|
+
renewRef.current = session.renew;
|
|
49
|
+
const [ops] = useState(() => makeClientOps(op, session.token, () => renewRef.current()));
|
|
50
|
+
// Uploads go to the service's media route with the same pass and the same renewal (spec 2026-10-18 media §4).
|
|
51
|
+
const [upload] = useState(() => ({ send: xhrSend(`${api.apiUrl.replace(/\/+$/, "")}/v1/spaces/${encodeURIComponent(api.spaceId)}/admin/media`, session.token, () => renewRef.current()) }));
|
|
49
52
|
const route = useRoute(registry.adminPath);
|
|
50
53
|
const [head = "pages", tail] = route.segments.length ? route.segments : ["pages"];
|
|
51
54
|
const isCollection = registry.collections.some((c) => c.name === head);
|
|
52
|
-
const email = session.state.status === "signed_in" ? session.state.
|
|
55
|
+
const email = session.state.status === "signed_in" ? session.state.user.email ?? undefined : undefined;
|
|
53
56
|
// The role rides on members.me (members management §2): Settings hides
|
|
54
57
|
// from an editor, and the area itself refuses. Unknown until it answers.
|
|
55
58
|
const signedIn = session.state.status === "signed_in";
|
|
@@ -67,28 +70,13 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
|
|
|
67
70
|
setRole("editor"); });
|
|
68
71
|
return () => { live = false; };
|
|
69
72
|
}, [signedIn, ops]);
|
|
70
|
-
// The one version check (spec 2026-09-26 §5): behind blocks the shell,
|
|
71
|
-
// equal or ahead renders as usual. Unknown until it answers; a failed
|
|
72
|
-
// call renders as usual too — the notice is for a known-behind schema.
|
|
73
|
-
const [schema, setSchema] = useState(null);
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (!signedIn) {
|
|
76
|
-
setSchema(null);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
let live = true;
|
|
80
|
-
ops.schema.status()
|
|
81
|
-
.then((r) => { if (live && r.ok)
|
|
82
|
-
setSchema(r.data); })
|
|
83
|
-
.catch(() => { });
|
|
84
|
-
return () => { live = false; };
|
|
85
|
-
}, [signedIn, ops]);
|
|
86
73
|
// The Deploy tile (spec 2026-10-09 static mode §6): on a static site, deploy.status every 30 s, every 5 s
|
|
87
74
|
// while a build is in flight, and at once after any publish; the press records and re-reads.
|
|
88
75
|
const [deploy, setDeploy] = useState(null);
|
|
89
76
|
const [deployBusy, setDeployBusy] = useState(false);
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
// A publish is done when its pointer moved (spec 2026-10-15 the space object §3): the tile re-reads at once.
|
|
78
|
+
const [deployTick, setDeployTick] = useState(0);
|
|
79
|
+
useEffect(() => onPublished(() => setDeployTick((n) => n + 1)), []);
|
|
92
80
|
useEffect(() => {
|
|
93
81
|
if (!signedIn || delivery() !== "static")
|
|
94
82
|
return;
|
|
@@ -112,7 +100,7 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
|
|
|
112
100
|
void tick();
|
|
113
101
|
return () => { live = false; if (timer)
|
|
114
102
|
clearTimeout(timer); };
|
|
115
|
-
}, [signedIn, ops,
|
|
103
|
+
}, [signedIn, ops, deployTick]);
|
|
116
104
|
const onDeploy = async () => {
|
|
117
105
|
setDeployBusy(true);
|
|
118
106
|
try {
|
|
@@ -126,37 +114,31 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
|
|
|
126
114
|
setDeployBusy(false);
|
|
127
115
|
}
|
|
128
116
|
};
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
// one
|
|
132
|
-
//
|
|
133
|
-
// The token getter is a new closure every render — read it through a ref so
|
|
134
|
-
// the effect keys on the session, not on the render.
|
|
117
|
+
// Presence (spec 2026-09-26 concurrency §5; spec 2026-10-16 sign-in and the dashboard §3): one socket per signed-in
|
|
118
|
+
// tab to the service, opened with a one-time ticket asked with the current pass; every renewal reconnects with a
|
|
119
|
+
// fresh one. Absent config = presence off. The getters are new closures every render — read them through refs so the
|
|
120
|
+
// effect keys on the session, not on the render.
|
|
135
121
|
const [presence, setPresence] = useState(null);
|
|
136
122
|
const tokenRef = useRef(session.token);
|
|
137
123
|
tokenRef.current = session.token;
|
|
138
|
-
const
|
|
139
|
-
|
|
124
|
+
const onRenew = useRef(session.onRenew);
|
|
125
|
+
onRenew.current = session.onRenew;
|
|
126
|
+
const presenceApi = presenceConfig?.apiUrl, presenceSpace = presenceConfig?.spaceId;
|
|
140
127
|
useEffect(() => {
|
|
141
|
-
if (!signedIn || !
|
|
128
|
+
if (!signedIn || !presenceApi || !presenceSpace || !email) {
|
|
142
129
|
setPresence(null);
|
|
143
130
|
return;
|
|
144
131
|
}
|
|
145
|
-
const config = {
|
|
132
|
+
const config = { apiUrl: presenceApi, spaceId: presenceSpace };
|
|
146
133
|
const p = createPresence({
|
|
147
134
|
email,
|
|
148
|
-
connect: () => {
|
|
149
|
-
const token = tokenRef.current();
|
|
150
|
-
if (!token)
|
|
151
|
-
throw new Error("smoodly: no session token for presence");
|
|
152
|
-
return connectRealtime(config, email, token);
|
|
153
|
-
},
|
|
135
|
+
connect: () => connectSocket(config, email, { pass: () => tokenRef.current(), renew: () => renewRef.current() }),
|
|
154
136
|
});
|
|
155
137
|
p.join();
|
|
156
138
|
setPresence(p);
|
|
157
|
-
const stop =
|
|
139
|
+
const stop = onRenew.current((pass) => p.setAuth(pass));
|
|
158
140
|
return () => { stop(); p.dispose(); setPresence(null); };
|
|
159
|
-
}, [signedIn,
|
|
141
|
+
}, [signedIn, presenceApi, presenceSpace, email]);
|
|
160
142
|
if (session.state.status === "loading") {
|
|
161
143
|
return (_jsx("div", { style: { height: "100vh", display: "grid", placeItems: "center", color: "var(--text-muted)" }, children: "Loading\u2026" }));
|
|
162
144
|
}
|
|
@@ -164,13 +146,10 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
|
|
|
164
146
|
if (mismatch) {
|
|
165
147
|
return _jsx("div", { style: { height: "100vh", display: "flex" }, children: _jsx(ErrorPane, { message: mismatch, back: false }) });
|
|
166
148
|
}
|
|
167
|
-
if (session.state.status === "signed_out")
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return _jsx(SetPasswordView, { client: session.client, mode: session.arrival, onDone: session.clearArrival });
|
|
172
|
-
if (schema && schemaBehind(schema.database, schema.required))
|
|
173
|
-
return _jsx(SchemaNotice, { status: schema });
|
|
149
|
+
if (session.state.status === "signed_out") {
|
|
150
|
+
const message = session.state.reason === "refused" ? session.state.message : undefined;
|
|
151
|
+
return _jsx(SignedOutView, { host: window.location.host, message: message, onSignIn: () => { void session.signInAgain(); } });
|
|
152
|
+
}
|
|
174
153
|
let main;
|
|
175
154
|
if (head === "pages" && !tail)
|
|
176
155
|
main = _jsx(PagesList, { ops: ops, registry: registry });
|
|
@@ -185,10 +164,10 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
|
|
|
185
164
|
else if (head === "globals" && tail)
|
|
186
165
|
main = _jsx(GlobalForm, { ops: ops, registry: registry, name: decodeURIComponent(tail) });
|
|
187
166
|
else if (head === "settings")
|
|
188
|
-
main = _jsx(SettingsView, { ops: ops, registry: registry, tail: tail, role: role
|
|
167
|
+
main = _jsx(SettingsView, { ops: ops, registry: registry, tail: tail, role: role });
|
|
189
168
|
else if (head === "media")
|
|
190
169
|
main = _jsx(MediaScreen, {});
|
|
191
170
|
else
|
|
192
171
|
main = _jsx(ErrorPane, { message: "Not found." });
|
|
193
|
-
return (_jsx(AdminPathProvider, { value: registry.adminPath, children: _jsx(PresenceProvider, { value: presence, children: _jsx(ElementsProvider, { elements: registry.elements, children: _jsx(TimeZoneProvider, { zone: registry.timeZone, children: _jsxs(AssetsProvider, { ops: ops, media: registry.media, children: [_jsxs("div", { className: "sm-shell", children: [_jsx(IconRail, { registry: registry, active: head, email: email, settings: role !== null, deploy: deploy ? { status: deploy, busy: deployBusy, onDeploy: () => { void onDeploy(); } } : undefined, onSignOut: () => session.
|
|
172
|
+
return (_jsx(AdminPathProvider, { value: registry.adminPath, children: _jsx(PresenceProvider, { value: presence, children: _jsx(ElementsProvider, { elements: registry.elements, children: _jsx(TimeZoneProvider, { zone: registry.timeZone, children: _jsxs(AssetsProvider, { ops: ops, media: registry.media, upload: upload, children: [_jsxs("div", { className: "sm-shell", children: [_jsx(IconRail, { registry: registry, active: head, email: email, settings: role !== null, deploy: deploy ? { status: deploy, busy: deployBusy, onDeploy: () => { void onDeploy(); } } : undefined, onSignOut: () => { void session.signOut(); } }), _jsx("main", { className: "sm-main", children: main }, `${head}/${tail ?? ""}`)] }), _jsx(ConfirmHost, {})] }) }) }) }) }));
|
|
194
173
|
}
|
|
@@ -26,9 +26,7 @@ export function ContentHealthPage({ ops, registry, initial = null }) {
|
|
|
26
26
|
return _jsx(ErrorPane, { message: error, back: false, onRetry: scan });
|
|
27
27
|
const sections = report ? healthSections(report, registry) : null;
|
|
28
28
|
const brokenCount = sections ? sections.broken.length + sections.collections.length : 0;
|
|
29
|
-
return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx("span", { className: "sm-title", children: "Content health" }), right: _jsxs(_Fragment, { children: [report && _jsx("span", { className: "sm-hint", children: "Checked just now" }), _jsx(Button, { variant: "secondary", onClick: scan, disabled: !report, children: "Rescan" })] }) }), _jsx("div", { className: "sm-listwrap", children: _jsxs("div", { className: "sm-table sm-health", children: [!sections && _jsx("div", { className: "sm-empty", children: "Checking content\u2026" }),
|
|
30
|
-
? "Every published document is current."
|
|
31
|
-
: _jsxs(_Fragment, { children: [report.delivery.behind, " ", report.delivery.behind === 1 ? "document is" : "documents are", " behind the current stamp. Run ", _jsx("code", { children: "npx smoodly rebuild" }), " in the site's folder."] }) })), sections && brokenCount === 0 && sections.orphan.length === 0 && _jsx("div", { className: "sm-empty", children: "Everything matches the code." }), sections && brokenCount > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "sm-health__section", children: ["Blocks publishing \u00B7 ", brokenCount] }), sections.collections.map((c) => _jsx("p", { className: "sm-health__headline", children: c.headline }, c.name)), sections.broken.map((g) => _jsx(Group, { group: g }, g.key))] })), sections && sections.orphan.length > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "sm-health__section", children: ["Left over \u00B7 ", sections.orphan.length] }), sections.orphan.map((g) => _jsx(Group, { group: g }, g.key))] }))] }) })] }));
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx("span", { className: "sm-title", children: "Content health" }), right: _jsxs(_Fragment, { children: [report && _jsx("span", { className: "sm-hint", children: "Checked just now" }), _jsx(Button, { variant: "secondary", onClick: scan, disabled: !report, children: "Rescan" })] }) }), _jsx("div", { className: "sm-listwrap", children: _jsxs("div", { className: "sm-table sm-health", children: [!sections && _jsx("div", { className: "sm-empty", children: "Checking content\u2026" }), sections && brokenCount === 0 && sections.orphan.length === 0 && _jsx("div", { className: "sm-empty", children: "Everything matches the code." }), sections && brokenCount > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "sm-health__section", children: ["Blocks publishing \u00B7 ", brokenCount] }), sections.collections.map((c) => _jsx("p", { className: "sm-health__headline", children: c.headline }, c.name)), sections.broken.map((g) => _jsx(Group, { group: g }, g.key))] })), sections && sections.orphan.length > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "sm-health__section", children: ["Left over \u00B7 ", sections.orphan.length] }), sections.orphan.map((g) => _jsx(Group, { group: g }, g.key))] }))] }) })] }));
|
|
32
30
|
}
|
|
33
31
|
function Group({ group }) {
|
|
34
32
|
const adminHref = useAdminHref();
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { AdminGlobal } from "../serialize.ts";
|
|
2
2
|
import type { Group } from "../forms/tabs.ts";
|
|
3
|
-
import { type NodeValues } from "../editor/InspectorFields.tsx";
|
|
4
3
|
import type { RefOption } from "../forms/ref-picker.ts";
|
|
5
4
|
export type { RefOption };
|
|
6
|
-
/** The item's value map as the inspector reads it: the reserved keys pulled out into their namespaces. */
|
|
7
|
-
export declare function globalValues(fields: Record<string, unknown>): NodeValues;
|
|
8
5
|
export declare function GlobalFields({ item, groups, fields, errors, refOptions, multilingual, inlinePath, onEditHere, onChange }: {
|
|
9
6
|
item: AdminGlobal;
|
|
10
7
|
groups: Group[];
|
|
@@ -5,7 +5,7 @@ import { FieldWidget } from "../forms/FieldWidget.js";
|
|
|
5
5
|
import { InspectorFields, valueOf } from "../editor/InspectorFields.js";
|
|
6
6
|
import { UnlocalizedMark } from "../ui/primitives.js";
|
|
7
7
|
/** The item's value map as the inspector reads it: the reserved keys pulled out into their namespaces. */
|
|
8
|
-
|
|
8
|
+
function globalValues(fields) {
|
|
9
9
|
const ns = (key) => (fields[key] && typeof fields[key] === "object" ? fields[key] : {});
|
|
10
10
|
return { fields, settings: ns(SETTINGS_KEY), styles: ns(STYLES_KEY), css: typeof fields[CSS_KEY] === "string" ? fields[CSS_KEY] : undefined };
|
|
11
11
|
}
|