rastack 0.0.63 → 0.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +19 -24
  3. package/admin/AdminApp.tsx +56 -3
  4. package/admin/httpEngine.ts +5 -6
  5. package/admin/index.tsx +11 -85
  6. package/admin/ui/icons.tsx +9 -0
  7. package/admin/ui/styles.ts +3 -0
  8. package/admin/useAdmin.ts +41 -4
  9. package/dist/admin.js +17 -22
  10. package/dist/compile/analyze.js +11 -1
  11. package/dist/compile/model.d.ts +1 -1
  12. package/dist/compile/run.d.ts +2 -2
  13. package/dist/compile/run.js +2 -2
  14. package/dist/define/index.d.ts +10 -4
  15. package/dist/define/index.js +2 -2
  16. package/dist/define/manifest.js +7 -1
  17. package/dist/deploy/io.d.ts +50 -0
  18. package/dist/deploy/io.js +124 -0
  19. package/dist/deploy/workflows.js +1 -1
  20. package/dist/dev/harness.d.ts +19 -27
  21. package/dist/dev/harness.js +31 -40
  22. package/dist/dev/native-server.d.ts +29 -0
  23. package/dist/dev/native-server.js +174 -0
  24. package/dist/external/envelope.d.ts +1 -1
  25. package/dist/external/envelope.js +1 -1
  26. package/dist/external/service.d.ts +2 -2
  27. package/dist/external/service.js +2 -2
  28. package/dist/external/types.d.ts +1 -1
  29. package/dist/external/types.js +1 -1
  30. package/dist/rastack-admin.d.ts +6 -14
  31. package/dist/rastack-admin.js +60 -88
  32. package/dist/rastack-bootstrap.js +18 -47
  33. package/dist/rastack-design.d.ts +1 -2
  34. package/dist/rastack-design.js +1 -2
  35. package/dist/rastack-dev.d.ts +7 -10
  36. package/dist/rastack-dev.js +68 -80
  37. package/dist/rastack-init.d.ts +6 -3
  38. package/dist/rastack-init.js +43 -30
  39. package/dist/rastack.d.ts +1 -1
  40. package/dist/rastack.js +5 -6
  41. package/dist/validate/transitions.d.ts +1 -1
  42. package/dist/validate/transitions.js +1 -1
  43. package/hooks/query/api.ts +4 -6
  44. package/package.json +2 -2
  45. package/provider/index.ts +0 -1
  46. package/provider/provider.tsx +31 -134
  47. package/provider/types.ts +30 -69
  48. package/src/compile/analyze.ts +11 -1
  49. package/src/compile/model.ts +1 -1
  50. package/src/compile/run.ts +2 -2
  51. package/src/define/index.ts +7 -4
  52. package/src/define/manifest.ts +7 -1
  53. package/src/deploy/io.ts +162 -1
  54. package/src/deploy/workflows.ts +1 -1
  55. package/src/dev/harness.ts +30 -43
  56. package/src/dev/native-server.ts +164 -0
  57. package/src/external/envelope.ts +1 -1
  58. package/src/external/service.ts +2 -2
  59. package/src/external/types.ts +1 -1
  60. package/src/rastack-admin.ts +66 -107
  61. package/src/rastack-bootstrap.ts +24 -69
  62. package/src/rastack-design.ts +1 -2
  63. package/src/rastack-dev.ts +79 -91
  64. package/src/rastack-init.ts +50 -41
  65. package/src/rastack.ts +5 -6
  66. package/src/validate/transitions.ts +1 -1
  67. package/dist/rastack-wasm-build.d.ts +0 -19
  68. package/dist/rastack-wasm-build.js +0 -94
  69. package/dist/wasm/package.json +0 -4
  70. package/dist/wasm/rastack_wasm.d.ts +0 -100
  71. package/dist/wasm/rastack_wasm.js +0 -745
  72. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  73. package/dist/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
  74. package/provider/wasm.ts +0 -126
  75. package/scripts/copy-wasm.js +0 -35
  76. package/src/rastack-wasm-build.ts +0 -113
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.0.69](https://github.com/theserverkid/reactapistack/compare/v0.0.64...v0.0.69) (2026-10-03)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * indent hybrid build rules in openspec/config.yaml ([6fa81ae](https://github.com/theserverkid/reactapistack/commit/6fa81ae1a1c8724a4f4bb54f483619a36940e316))
11
+
12
+ ### [0.0.68](https://github.com/theserverkid/reactapistack/compare/v0.0.64...v0.0.68) (2026-09-26)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * indent hybrid build rules in openspec/config.yaml ([6fa81ae](https://github.com/theserverkid/reactapistack/commit/6fa81ae1a1c8724a4f4bb54f483619a36940e316))
18
+
19
+ ### [0.0.67](https://github.com/theserverkid/reactapistack/compare/v0.0.64...v0.0.67) (2026-09-26)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * indent hybrid build rules in openspec/config.yaml ([6fa81ae](https://github.com/theserverkid/reactapistack/commit/6fa81ae1a1c8724a4f4bb54f483619a36940e316))
25
+
26
+ ### [0.0.66](https://github.com/theserverkid/reactapistack/compare/v0.0.64...v0.0.66) (2026-09-26)
27
+
28
+ ### [0.0.65](https://github.com/theserverkid/reactapistack/compare/v0.0.64...v0.0.65) (2026-09-02)
29
+
30
+ ### [0.0.64](https://github.com/theserverkid/reactapistack/compare/v0.0.63...v0.0.64) (2026-07-20)
31
+
32
+
33
+ ### Features
34
+
35
+ * **define,provider:** nullable FK relations + bundled-warehouse seed ([276f8c6](https://github.com/theserverkid/reactapistack/commit/276f8c6d6555a6dbcb8965926009f0ddd076ad96))
36
+
5
37
  ### [0.0.63](https://github.com/theserverkid/reactapistack/compare/v0.0.62...v0.0.63) (2026-07-20)
6
38
 
7
39
 
package/README.md CHANGED
@@ -82,8 +82,8 @@ export const Terminal = resource("airports", "terminal", {
82
82
 
83
83
  ```bash
84
84
  # No compile step to run — it happens in-process. Author your resources, then:
85
- rastack dev # compiles + serves your app in-browser over the WASM engine
86
- rastack admin # Django-admin-style DB browser over the same engine
85
+ rastack dev # compiles, starts native rastack-server, proxies /api
86
+ rastack admin # Django-admin-style console served by that same binary
87
87
  ```
88
88
 
89
89
  ```tsx
@@ -101,8 +101,8 @@ straight from the field types — see
101
101
  resolve your types against the compiled manifest at runtime — see
102
102
  [docs/typed-hooks.md](docs/typed-hooks.md). Resources can declare **state
103
103
  transitions** (`scheduled → boarding → departed`), compiled into the manifest
104
- and enforced on every write surface — an illegal jump is a 400 from the form,
105
- the importer, the server, and the in-browser WASM engine alike.
104
+ and enforced on every write by the native handler — an illegal jump is a 400
105
+ from the form, the importer, and `rastack-server` (laptop and Lambda).
106
106
 
107
107
  ## Ship it — one command per stage
108
108
 
@@ -127,32 +127,27 @@ is fixed and CI only ships code: `rastack ci deploy` builds and runs
127
127
  is just `run: rastack ci deploy` (`rastack ci --plan deploy` prints every step).
128
128
  Full guide: **[docs/serverless-deploy.md](docs/serverless-deploy.md)**.
129
129
 
130
- ## Run the whole API in the browser
130
+ ## The UI is in the browser. The API is native Rust.
131
131
 
132
- The Rust API is stateless compute over object storage, so it also compiles to
133
- **WebAssembly and runs entirely in the browser** — no server, no network. One
134
- setting on the React provider picks where the API runs; the hooks never
135
- change:
132
+ A browser cannot execute `rastack-server`. `rastack dev` starts that binary
133
+ and proxies `/api` to it. The page renders your app. The request logic
134
+ (`rastack-api-core`), the Iceberg warehouse, and access control run in the
135
+ native process — the same binary Lambda runs (`provided.al2023`,
136
+ `cargo lambda build --arm64 --features lambda`). There is no in-browser engine:
137
+ no `RastackApi`, no warehouse seeded into the page, no IndexedDB copy of the
138
+ database. The warehouse is the server's directory under `data/warehouse`.
136
139
 
137
140
  ```tsx
138
- // Local-first dev: the entire API in the browser, over the data/ Iceberg files.
139
- <RAStackProvider mode="local" manifest={schema} warehouseBaseUrl="/data/warehouse/">
141
+ // Local: this origin. rastack dev proxies /api to native rastack-server.
142
+ <RAStackProvider mode="local" manifest={schema}>
140
143
  <App />
141
144
  </RAStackProvider>
142
145
 
143
- // Production: the same code on AWS Lambda + S3.
146
+ // Production: the same hooks, over HTTP, to Lambda.
144
147
  <RAStackProvider mode="remote" baseURL="https://api.example.com">…</RAStackProvider>
145
-
146
- // Client-only, reading and writing S3 directly.
147
- <RAStackProvider mode="s3" manifest={schema} warehouseBaseUrl="https://bucket.s3.amazonaws.com/warehouse/">…</RAStackProvider>
148
148
  ```
149
149
 
150
- `local` and `s3` run the same `rastack-api-core` request logic (CRUD, foreign-key
151
- validation, DRF-shaped lists) that Lambda runs — and, because Arrow/Parquet
152
- compile to WebAssembly, the browser reads and writes the **same Parquet Iceberg
153
- files** the server does. The local database is just a committed directory of
154
- those files under `data/`. Full guide:
155
- **[docs/local-wasm-dev.md](docs/local-wasm-dev.md)**.
150
+ Full guide: **[docs/local-wasm-dev.md](docs/local-wasm-dev.md)**.
156
151
 
157
152
  ## Why "on top of object storage"?
158
153
 
@@ -177,9 +172,9 @@ That gives you:
177
172
  `useData` / `useForm`, and state transitions as typesafe backend functions.
178
173
  - **[docs/architecture.md](docs/architecture.md)** — the full serverless,
179
174
  object-storage architecture and how the three surfaces compose.
180
- - **[docs/local-wasm-dev.md](docs/local-wasm-dev.md)** — running the whole API in
181
- the browser via WebAssembly, and the `RAStackProvider` mode switch.
182
- - **[engine/](engine/)** — the Rust engine: `core/` (manifest, Iceberg storage, request brain) and `serve/` (axum API, server binary, wasm).
175
+ - **[docs/local-wasm-dev.md](docs/local-wasm-dev.md)** — local dev: native
176
+ `rastack-server`, UI in the browser.
177
+ - **[engine/](engine/)** — the Rust engine: `core/` (manifest, Iceberg storage, request brain) and `serve/` (axum API, native server binary).
183
178
  - **[examples/serverless/](examples/serverless/)** — Lambda + S3 + IaC.
184
179
  - **[CLAUDE.md](CLAUDE.md)** — codebase guide for contributors.
185
180
  - **Building with an LLM?** The documentation site is mirrored as plain Markdown
@@ -10,7 +10,7 @@
10
10
  * Layout: models on the left (`<Sidebar>`), the changelist table in the main
11
11
  * area, a Linear-style filter bar on top (`<FilterBar>`), and add/change forms.
12
12
  */
13
- import React, { useEffect, useMemo, useState } from "react";
13
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
14
14
  import {
15
15
  AutoForm,
16
16
  Button,
@@ -28,9 +28,11 @@ import {
28
28
  mergeDiscovered,
29
29
  } from "./adapters";
30
30
  import {
31
+ RefreshContext,
31
32
  useDashboardStats,
32
33
  useDeleteRecord,
33
34
  useFilterModel,
35
+ useRefresh,
34
36
  useResourceForm,
35
37
  useResourceTable,
36
38
  } from "./useAdmin";
@@ -56,6 +58,7 @@ import {
56
58
  IconLayers,
57
59
  IconLink,
58
60
  IconPlus,
61
+ IconRefresh,
59
62
  IconTable,
60
63
  IconTrash,
61
64
  } from "./ui/icons";
@@ -100,7 +103,43 @@ interface Crumb {
100
103
  hash?: string;
101
104
  }
102
105
 
106
+ /** Compact relative age, e.g. "just now", "3m ago", "2h ago". */
107
+ function formatAgo(ts: number, now: number): string {
108
+ if (!ts) return "—";
109
+ const secs = Math.max(0, Math.round((now - ts) / 1000));
110
+ if (secs < 5) return "just now";
111
+ if (secs < 60) return `${secs}s ago`;
112
+ const mins = Math.round(secs / 60);
113
+ if (mins < 60) return `${mins}m ago`;
114
+ const hrs = Math.round(mins / 60);
115
+ if (hrs < 24) return `${hrs}h ago`;
116
+ return `${Math.round(hrs / 24)}d ago`;
117
+ }
118
+
119
+ /** The "Updated …" label — ticks over time and snaps fresh when data lands. */
120
+ function LastUpdated(): React.JSX.Element {
121
+ const { lastUpdated } = useRefresh();
122
+ const [now, setNow] = useState(() => Date.now());
123
+ useEffect(() => {
124
+ const id = window.setInterval(() => setNow(Date.now()), 30_000);
125
+ return () => window.clearInterval(id);
126
+ }, []);
127
+ useEffect(() => setNow(Date.now()), [lastUpdated]);
128
+ return (
129
+ <span className="ras-updated" style={{ marginLeft: "auto" }}>
130
+ Updated {formatAgo(lastUpdated, now)}
131
+ </span>
132
+ );
133
+ }
134
+
103
135
  function Topbar({ crumbs }: { crumbs: Crumb[] }): React.JSX.Element {
136
+ const { refresh } = useRefresh();
137
+ const [spinning, setSpinning] = useState(false);
138
+ const onReload = () => {
139
+ setSpinning(true);
140
+ refresh();
141
+ window.setTimeout(() => setSpinning(false), 500);
142
+ };
104
143
  return (
105
144
  <header className="ras-topbar">
106
145
  <nav className="ras-crumbs">
@@ -115,6 +154,16 @@ function Topbar({ crumbs }: { crumbs: Crumb[] }): React.JSX.Element {
115
154
  </React.Fragment>
116
155
  ))}
117
156
  </nav>
157
+ <LastUpdated />
158
+ <button
159
+ type="button"
160
+ className="ras-icon-btn"
161
+ onClick={onReload}
162
+ title="Reload data"
163
+ aria-label="Reload data"
164
+ >
165
+ <IconRefresh size={16} className={spinning ? "ras-spin" : undefined} />
166
+ </button>
118
167
  </header>
119
168
  );
120
169
  }
@@ -695,6 +744,10 @@ export function AdminApp(props: {
695
744
  const { engine } = props;
696
745
  const resources = useDiscoveredResources(engine, props.resources);
697
746
  const [collapsed, toggleCollapsed] = useSidebarCollapsed();
747
+ const [nonce, setNonce] = useState(0);
748
+ const [lastUpdated, setLastUpdated] = useState(0);
749
+ const refresh = useCallback(() => setNonce((n) => n + 1), []);
750
+ const markUpdated = useCallback(() => setLastUpdated(Date.now()), []);
698
751
 
699
752
  const activeKey =
700
753
  route.name === "index" ? undefined : `${route.app}.${route.model}`;
@@ -737,7 +790,7 @@ export function AdminApp(props: {
737
790
  }
738
791
 
739
792
  return (
740
- <>
793
+ <RefreshContext.Provider value={{ nonce, refresh, lastUpdated, markUpdated }}>
741
794
  <AdminStyles />
742
795
  <div className={"ras-root" + (collapsed ? " is-collapsed" : "")}>
743
796
  <Sidebar
@@ -748,6 +801,6 @@ export function AdminApp(props: {
748
801
  />
749
802
  <main className="ras-main">{screen}</main>
750
803
  </div>
751
- </>
804
+ </RefreshContext.Provider>
752
805
  );
753
806
  }
@@ -1,10 +1,7 @@
1
1
  /**
2
2
  * An HTTP-backed `RastackEngine` — the same `handle(method, path, query, body)`
3
- * seam the WASM engine exposes, implemented over `fetch` against a live
4
- * `rastack-server`. This is what lets the *identical* admin UI run two ways:
5
- * over WebAssembly in `rastack dev` (`client.engine`), or over the real Rust
6
- * API + Iceberg store in `rastack admin` (this adapter) — the components,
7
- * hooks and adapters above it never learn which.
3
+ * seam, implemented over `fetch` against the native `rastack-server`. The
4
+ * browser renders the console. The engine is that process.
8
5
  */
9
6
 
10
7
  import type { RastackEngine } from "../provider/types";
@@ -44,7 +41,9 @@ export function createHttpEngine(baseUrl = ""): RastackEngine {
44
41
  }
45
42
 
46
43
  const unavailable = (op: string) => async (): Promise<never> => {
47
- throw new Error(`${op} is only available over the local WASM engine.`);
44
+ throw new Error(
45
+ `${op} is not available in the browser. The native rastack-server process owns the warehouse.`,
46
+ );
48
47
  };
49
48
 
50
49
  return {
package/admin/index.tsx CHANGED
@@ -1,22 +1,14 @@
1
1
  /**
2
- * Entry point for the admin bundle (`dist/admin.js`). It runs two ways from the
3
- * *same* build, chosen by a `window.__RASTACK_ADMIN__` config the host page
4
- * injects (defaulting to local):
2
+ * Entry point for the admin bundle (`dist/admin.js`).
5
3
  *
6
- * • `rastack dev` → `mode: "local"` — boots the WASM engine via
7
- * `<RAStackProvider mode="local">`, seeding from the served `/data/warehouse/`.
8
- * • `rastack admin` → `mode: "remote"` — talks to the live `rastack-server`
9
- * over HTTP through an `httpEngine`, against the real Iceberg warehouse.
10
- *
11
- * Either way it fetches the compiled schema, themes everything with
12
- * `<ThemeProvider>` and the design tokens, and renders the manifest-driven
13
- * `<AdminApp>` — which never learns which engine it got.
4
+ * The native `rastack-server admin` process serves this bundle and injects
5
+ * `window.__RASTACK_ADMIN__` with `mode: "remote"`. The console calls `/api`
6
+ * on that same origin through `createHttpEngine`. The engine is the native
7
+ * process. This bundle does not load a module into the page.
14
8
  */
15
9
 
16
10
  import React, { useEffect, useMemo, useState } from "react";
17
11
  import { createRoot } from "react-dom/client";
18
- import { RAStackProvider, useRastackClient } from "../provider";
19
- import type { RastackWasmModule } from "../provider/types";
20
12
  import { ThemeProvider } from "../theme";
21
13
  import { defineTheme } from "../tokens";
22
14
  import { AdminApp } from "./AdminApp";
@@ -26,62 +18,23 @@ import { BootScreen } from "./ui/BootScreen";
26
18
 
27
19
  interface AdminConfig {
28
20
  mode: "local" | "remote";
29
- /** Remote: base URL the `/api/...` paths are joined onto (default same origin). */
21
+ /** Base URL the `/api/...` paths are joined onto (default same origin). */
30
22
  apiBase?: string;
31
23
  /** Where to fetch the compiled `schema.rastack.json`. */
32
24
  schemaUrl?: string;
33
- /** Local: base URL of the committed Iceberg warehouse to seed from. */
34
- warehouseBaseUrl?: string;
35
25
  }
36
26
 
37
27
  const cfg: AdminConfig =
38
28
  (typeof window !== "undefined" && (window as any).__RASTACK_ADMIN__) || {
39
- mode: "local",
29
+ mode: "remote",
40
30
  schemaUrl: "/schema.rastack.json",
41
- warehouseBaseUrl: "/data/warehouse/",
42
31
  };
43
32
 
44
33
  // The framework's own design system paints the whole console.
45
34
  const theme = defineTheme({ brand: "#4F6BFF", radius: "rounded" });
46
35
 
47
- /** Load the WASM engine from the URL `rastack dev` serves it at (`/wasm/…`). */
48
- const loadEngine = async (): Promise<RastackWasmModule> => {
49
- const runtimeImport = new Function("s", "return import(s)") as (
50
- s: string,
51
- ) => Promise<any>;
52
- const mod: RastackWasmModule = await runtimeImport("/wasm/rastack_wasm.js");
53
- const init = mod.default || mod.init;
54
- if (typeof init === "function") await init();
55
- return mod;
56
- };
57
-
58
- /** Local (WASM) admin: wait for the provider's engine, then render. */
59
- function LocalAdmin({ resources }: { resources: AdminResource[] }): React.JSX.Element {
60
- const client = useRastackClient();
61
- if (client.status === "error")
62
- return (
63
- <BootScreen
64
- error
65
- label="The WASM engine failed to start — check the console."
66
- />
67
- );
68
- if (!client.engine || client.status !== "ready")
69
- return (
70
- <BootScreen
71
- label={
72
- client.status === "seeding"
73
- ? "Seeding the local warehouse…"
74
- : "Starting the WASM engine…"
75
- }
76
- />
77
- );
78
- return <AdminApp engine={client.engine} resources={resources} />;
79
- }
80
-
81
36
  function Boot(): React.JSX.Element {
82
37
  const [resources, setResources] = useState<AdminResource[] | null>(null);
83
- const [manifest, setManifest] = useState<object | null>(null);
84
- const [openapi, setOpenapi] = useState<object | undefined>();
85
38
  const [error, setError] = useState<string>();
86
39
  const httpEngine = useMemo(() => createHttpEngine(cfg.apiBase ?? ""), []);
87
40
 
@@ -91,16 +44,7 @@ function Boot(): React.JSX.Element {
91
44
  const res = await fetch(schemaUrl);
92
45
  if (!res.ok) throw new Error(`${schemaUrl} (${res.status})`);
93
46
  const doc = await res.json();
94
- setManifest(doc);
95
47
  setResources(parseManifest(doc));
96
- if (cfg.mode === "local") {
97
- try {
98
- const o = await fetch("/openapi.json");
99
- if (o.ok) setOpenapi(await o.json());
100
- } catch {
101
- /* openapi is optional */
102
- }
103
- }
104
48
  })().catch((e) => setError(String(e)));
105
49
  }, []);
106
50
 
@@ -110,35 +54,17 @@ function Boot(): React.JSX.Element {
110
54
  <BootScreen error label={`Failed to load the schema: ${error}`} />
111
55
  </ThemeProvider>
112
56
  );
113
- if (!resources || !manifest)
57
+ if (!resources)
114
58
  return (
115
59
  <ThemeProvider tokens={theme}>
116
60
  <BootScreen label="Loading the schema…" />
117
61
  </ThemeProvider>
118
62
  );
119
63
 
120
- if (cfg.mode === "remote") {
121
- return (
122
- <ThemeProvider tokens={theme}>
123
- <AdminApp engine={httpEngine} resources={resources} />
124
- </ThemeProvider>
125
- );
126
- }
127
-
128
64
  return (
129
- <RAStackProvider
130
- mode="local"
131
- manifest={manifest}
132
- openapi={openapi}
133
- loadEngine={loadEngine}
134
- warehouseBaseUrl={cfg.warehouseBaseUrl ?? "/data/warehouse/"}
135
- persist={false}
136
- cache={false}
137
- >
138
- <ThemeProvider tokens={theme}>
139
- <LocalAdmin resources={resources} />
140
- </ThemeProvider>
141
- </RAStackProvider>
65
+ <ThemeProvider tokens={theme}>
66
+ <AdminApp engine={httpEngine} resources={resources} />
67
+ </ThemeProvider>
142
68
  );
143
69
  }
144
70
 
@@ -152,6 +152,15 @@ export const IconArrowUpRight = (p: SvgProps) => (
152
152
  </Svg>
153
153
  );
154
154
 
155
+ export const IconRefresh = (p: SvgProps) => (
156
+ <Svg {...p}>
157
+ <path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
158
+ <path d="M21 3v5h-5" />
159
+ <path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
160
+ <path d="M3 21v-5h5" />
161
+ </Svg>
162
+ );
163
+
155
164
  export const IconPanelLeft = (p: SvgProps) => (
156
165
  <Svg {...p}>
157
166
  <rect x="3.5" y="4.5" width="17" height="15" rx="2" />
@@ -192,6 +192,8 @@ a { color: inherit; text-decoration: none; }
192
192
  transition: background 0.14s, color 0.14s, border-color 0.14s;
193
193
  }
194
194
  .ras-icon-btn:hover { color: var(--color-text); border-color: color-mix(in srgb, var(--color-text) 22%, transparent); }
195
+ @keyframes ras-spin { to { transform: rotate(360deg); } }
196
+ .ras-spin { animation: ras-spin 0.5s linear; }
195
197
 
196
198
  /* ---- top bar ------------------------------------------------------------- */
197
199
  .ras-topbar {
@@ -212,6 +214,7 @@ a { color: inherit; text-decoration: none; }
212
214
  .ras-crumbs a:hover { color: var(--color-text); }
213
215
  .ras-crumb-sep { color: color-mix(in srgb, var(--color-text-muted) 55%, transparent); }
214
216
  .ras-crumb-current { color: var(--color-text); font-weight: 600; }
217
+ .ras-updated { font-size: 12.5px; color: var(--color-text-muted); white-space: nowrap; }
215
218
 
216
219
  .ras-content { padding: 26px 28px 64px; max-width: 1240px; width: 100%; }
217
220
 
package/admin/useAdmin.ts CHANGED
@@ -7,7 +7,14 @@
7
7
  * one admin serves any app.
8
8
  */
9
9
 
10
- import { useCallback, useEffect, useMemo, useState } from "react";
10
+ import {
11
+ createContext,
12
+ useCallback,
13
+ useContext,
14
+ useEffect,
15
+ useMemo,
16
+ useState,
17
+ } from "react";
11
18
  import type { RastackEngine } from "../provider/types";
12
19
  import type { AutoFormField, AutoFormLike, TableQuery } from "../components/types";
13
20
  import type {
@@ -45,6 +52,30 @@ interface EngineResult {
45
52
  }
46
53
  const ok = (r: EngineResult) => r.status >= 200 && r.status < 300;
47
54
 
55
+ /**
56
+ * A global "reload" signal for the admin. The engine holds the live data (the
57
+ * WASM warehouse under `rastack dev`, the real `rastack-server` under
58
+ * `rastack admin`), so a manual reload just re-runs the read effects against it.
59
+ * `nonce` bumps on every {@link RefreshApi.refresh}; the data hooks include it in
60
+ * their fetch deps so they refetch **without** resetting filters/paging. The
61
+ * default value keeps the hooks usable with no provider (tests, embedding).
62
+ */
63
+ export interface RefreshApi {
64
+ nonce: number;
65
+ refresh: () => void;
66
+ /** Epoch ms of the most recent successful load (0 before the first). */
67
+ lastUpdated: number;
68
+ /** Called by the data hooks when a fetch resolves, to stamp {@link lastUpdated}. */
69
+ markUpdated: () => void;
70
+ }
71
+ export const RefreshContext = createContext<RefreshApi>({
72
+ nonce: 0,
73
+ refresh: () => {},
74
+ lastUpdated: 0,
75
+ markUpdated: () => {},
76
+ });
77
+ export const useRefresh = (): RefreshApi => useContext(RefreshContext);
78
+
48
79
  /** What `useResourceTable` returns: a `TableQuery` plus changelist controls. */
49
80
  export interface ResourceTable {
50
81
  query: TableQuery;
@@ -75,6 +106,7 @@ export function useResourceTable(
75
106
  const [isLoading, setLoading] = useState(true);
76
107
  const [error, setError] = useState<unknown>(undefined);
77
108
  const [tick, setTick] = useState(0);
109
+ const { nonce, markUpdated } = useRefresh();
78
110
 
79
111
  // Reset paging/search/filters when the resource changes.
80
112
  useEffect(() => {
@@ -111,6 +143,7 @@ export function useResourceTable(
111
143
  setData([]);
112
144
  }
113
145
  setLoading(false);
146
+ markUpdated();
114
147
  })
115
148
  .catch((e: unknown) => {
116
149
  if (cancelled) return;
@@ -121,7 +154,7 @@ export function useResourceTable(
121
154
  return () => {
122
155
  cancelled = true;
123
156
  };
124
- }, [engine, resource.key, resource.app, resource.model, page, search, filterKey, tick]);
157
+ }, [engine, resource.key, resource.app, resource.model, page, search, filterKey, tick, nonce]);
125
158
 
126
159
  const setSearch = (v: string | undefined) => {
127
160
  setSearchState(v && v.length ? v : undefined);
@@ -447,6 +480,7 @@ export function useDashboardStats(
447
480
  ): DashboardStats {
448
481
  const models = useMemo(() => adminResources(resources), [resources]);
449
482
  const [counts, setCounts] = useState<Record<string, number> | null>(null);
483
+ const { nonce, markUpdated } = useRefresh();
450
484
 
451
485
  useEffect(() => {
452
486
  let cancelled = false;
@@ -467,12 +501,15 @@ export function useDashboardStats(
467
501
  }
468
502
  }),
469
503
  );
470
- if (!cancelled) setCounts(Object.fromEntries(entries));
504
+ if (!cancelled) {
505
+ setCounts(Object.fromEntries(entries));
506
+ markUpdated();
507
+ }
471
508
  })();
472
509
  return () => {
473
510
  cancelled = true;
474
511
  };
475
- }, [engine, models]);
512
+ }, [engine, models, nonce]);
476
513
 
477
514
  return useMemo((): DashboardStats => {
478
515
  const loading = counts === null;