@notis_ai/cli 0.2.0-beta.20.1 → 0.2.0-beta.32.1
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 +36 -9
- package/package.json +2 -1
- package/src/command-specs/apps.js +780 -42
- package/src/command-specs/helpers.js +28 -3
- package/src/command-specs/index.js +1 -1
- package/src/command-specs/tools.js +33 -6
- package/src/runtime/agent-browser.js +192 -0
- package/src/runtime/app-boundary-validator.js +132 -0
- package/src/runtime/app-dev-server.js +577 -0
- package/src/runtime/app-dev-sessions.js +87 -0
- package/src/runtime/app-platform.js +352 -17
- package/src/runtime/cli-mode.generated.js +4 -0
- package/src/runtime/cli-mode.js +29 -0
- package/src/runtime/output.js +2 -2
- package/src/runtime/ports.js +15 -0
- package/src/runtime/profiles.js +34 -3
- package/src/runtime/transport.js +129 -4
- package/template/.harness/index.html.tmpl +260 -0
- package/template/app/layout.tsx +1 -2
- package/template/notis.config.ts +16 -1
- package/template/package.json +1 -2
- package/template/packages/notis-sdk/src/components/MultiSelectActionBar.tsx +272 -0
- package/template/packages/notis-sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/template/packages/notis-sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/template/packages/notis-sdk/src/config.ts +24 -1
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +2 -1
- package/template/packages/notis-sdk/src/hooks/useMultiSelect.ts +502 -0
- package/template/packages/notis-sdk/src/hooks/useNotis.ts +5 -3
- package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +1 -1
- package/template/packages/notis-sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/template/packages/notis-sdk/src/index.ts +20 -0
- package/template/packages/notis-sdk/src/provider.tsx +23 -24
- package/template/packages/notis-sdk/src/runtime.ts +43 -26
- package/template/packages/notis-sdk/src/styles.css +6 -91
- package/src/runtime/app-preview-server.js +0 -336
package/src/runtime/transport.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_PROFILE,
|
|
5
|
+
ensureProfile,
|
|
6
|
+
getProfile,
|
|
7
|
+
loadConfig,
|
|
8
|
+
saveConfig,
|
|
9
|
+
} from './profiles.js';
|
|
3
10
|
|
|
4
11
|
const META_TOOL_NAMES = new Set([
|
|
5
12
|
'notis_find_toolkits',
|
|
@@ -97,6 +104,94 @@ function normalizeBackendError(status, payload) {
|
|
|
97
104
|
});
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
function shouldAttemptDevPortalRefresh(runtime, { force = false } = {}) {
|
|
108
|
+
if (runtime.authMode !== 'dev_portal' || !runtime.refreshToken || !runtime.apiBase) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (force) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (typeof runtime.accessExpiresAt !== 'number' || runtime.accessExpiresAt <= 0) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const thresholdSeconds = 60;
|
|
121
|
+
return runtime.accessExpiresAt - Math.floor(Date.now() / 1000) <= thresholdSeconds;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function persistDevPortalRuntimeAuth(runtime, session) {
|
|
125
|
+
const config = ensureProfile(loadConfig(), runtime.profileName || DEFAULT_PROFILE);
|
|
126
|
+
const profileName = runtime.profileName || DEFAULT_PROFILE;
|
|
127
|
+
config.current_profile = profileName;
|
|
128
|
+
config.profiles[profileName] = {
|
|
129
|
+
...config.profiles[profileName],
|
|
130
|
+
jwt: session.access_token,
|
|
131
|
+
api_base: runtime.apiBase,
|
|
132
|
+
auth_mode: 'dev_portal',
|
|
133
|
+
refresh_token: session.refresh_token,
|
|
134
|
+
access_expires_at: session.access_expires_at,
|
|
135
|
+
refresh_expires_at: session.refresh_expires_at,
|
|
136
|
+
};
|
|
137
|
+
saveConfig(config);
|
|
138
|
+
|
|
139
|
+
runtime.jwt = session.access_token;
|
|
140
|
+
runtime.authMode = 'dev_portal';
|
|
141
|
+
runtime.refreshToken = session.refresh_token;
|
|
142
|
+
runtime.accessExpiresAt = session.access_expires_at;
|
|
143
|
+
runtime.refreshExpiresAt = session.refresh_expires_at;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function reloadJwtFromConfig(runtime) {
|
|
147
|
+
const profileName = runtime.profileName || DEFAULT_PROFILE;
|
|
148
|
+
let profile;
|
|
149
|
+
try {
|
|
150
|
+
profile = getProfile(loadConfig(), profileName);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const nextJwt = typeof profile.jwt === 'string' && profile.jwt ? profile.jwt : null;
|
|
155
|
+
if (!nextJwt || nextJwt === runtime.jwt) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
runtime.jwt = nextJwt;
|
|
159
|
+
runtime.authMode = profile.auth_mode;
|
|
160
|
+
runtime.refreshToken = profile.refresh_token;
|
|
161
|
+
runtime.accessExpiresAt = profile.access_expires_at;
|
|
162
|
+
runtime.refreshExpiresAt = profile.refresh_expires_at;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function maybeRefreshDevPortalAuth(runtime, { force = false } = {}) {
|
|
167
|
+
if (!shouldAttemptDevPortalRefresh(runtime, { force })) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const response = await fetch(`${runtime.apiBase}/portal_auth/dev-refresh`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
'X-Notis-CLI-Version': runtime.cliVersion,
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({ refresh_token: runtime.refreshToken }),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
let payload = null;
|
|
181
|
+
try {
|
|
182
|
+
payload = await response.json();
|
|
183
|
+
} catch {
|
|
184
|
+
payload = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!response.ok || !payload?.session?.access_token || !payload?.session?.refresh_token) {
|
|
188
|
+
throw normalizeBackendError(response.status, payload);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
persistDevPortalRuntimeAuth(runtime, payload.session);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
100
195
|
export async function httpRequest({
|
|
101
196
|
runtime,
|
|
102
197
|
method = 'POST',
|
|
@@ -104,9 +199,16 @@ export async function httpRequest({
|
|
|
104
199
|
body,
|
|
105
200
|
requireAuth = true,
|
|
106
201
|
}) {
|
|
107
|
-
|
|
108
|
-
|
|
202
|
+
await maybeRefreshDevPortalAuth(runtime);
|
|
203
|
+
|
|
204
|
+
let controller = new AbortController();
|
|
205
|
+
let timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
109
206
|
const requestId = `req_${randomUUID().replace(/-/g, '')}`;
|
|
207
|
+
const resetTimeout = () => {
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
controller = new AbortController();
|
|
210
|
+
timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
211
|
+
};
|
|
110
212
|
|
|
111
213
|
const headers = {
|
|
112
214
|
'Content-Type': 'application/json',
|
|
@@ -119,13 +221,12 @@ export async function httpRequest({
|
|
|
119
221
|
}
|
|
120
222
|
|
|
121
223
|
try {
|
|
122
|
-
|
|
224
|
+
let response = await fetch(`${runtime.apiBase}${path}`, {
|
|
123
225
|
method,
|
|
124
226
|
headers,
|
|
125
227
|
body: body ? JSON.stringify(body) : undefined,
|
|
126
228
|
signal: controller.signal,
|
|
127
229
|
});
|
|
128
|
-
clearTimeout(timeout);
|
|
129
230
|
|
|
130
231
|
let payload = null;
|
|
131
232
|
try {
|
|
@@ -134,6 +235,30 @@ export async function httpRequest({
|
|
|
134
235
|
payload = null;
|
|
135
236
|
}
|
|
136
237
|
|
|
238
|
+
if (response.status === 401) {
|
|
239
|
+
const refreshed = (await maybeRefreshDevPortalAuth(runtime, { force: true }))
|
|
240
|
+
|| reloadJwtFromConfig(runtime);
|
|
241
|
+
if (refreshed) {
|
|
242
|
+
if (requireAuth && runtime.jwt) {
|
|
243
|
+
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
244
|
+
}
|
|
245
|
+
resetTimeout();
|
|
246
|
+
response = await fetch(`${runtime.apiBase}${path}`, {
|
|
247
|
+
method,
|
|
248
|
+
headers,
|
|
249
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
250
|
+
signal: controller.signal,
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
payload = await response.json();
|
|
254
|
+
} catch {
|
|
255
|
+
payload = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
clearTimeout(timeout);
|
|
261
|
+
|
|
137
262
|
if (!response.ok) {
|
|
138
263
|
throw normalizeBackendError(response.status, payload);
|
|
139
264
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Notis App Harness</title>
|
|
6
|
+
<link rel="stylesheet" href="./bundle/app.css">
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{
|
|
9
|
+
"imports": {
|
|
10
|
+
"react": "https://esm.sh/react@{{REACT_VERSION}}",
|
|
11
|
+
"react-dom": "https://esm.sh/react-dom@{{REACT_VERSION}}",
|
|
12
|
+
"react/jsx-runtime": "https://esm.sh/react@{{REACT_VERSION}}/jsx-runtime",
|
|
13
|
+
"react-dom/client": "https://esm.sh/react-dom@{{REACT_VERSION}}/client"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
<style>
|
|
18
|
+
body { margin: 0; font-family: system-ui, sans-serif; }
|
|
19
|
+
#harness-status { position: fixed; top: 0; left: 0; right: 0; padding: 4px 8px; font-size: 11px; background: #111; color: #0f0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; z-index: 9999; }
|
|
20
|
+
#root { padding-top: 24px; min-height: 100vh; }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<div id="harness-status">harness: booting</div>
|
|
25
|
+
<div id="root"></div>
|
|
26
|
+
<script type="module">
|
|
27
|
+
const routeExport = {{ROUTE_EXPORT}};
|
|
28
|
+
const descriptor = {{RUNTIME_DESCRIPTOR}};
|
|
29
|
+
const mode = {{MODE}};
|
|
30
|
+
const apiBase = {{API_BASE}};
|
|
31
|
+
const jwt = {{JWT}};
|
|
32
|
+
const status = document.getElementById('harness-status');
|
|
33
|
+
const setStatus = (msg, color = '#0f0') => {
|
|
34
|
+
status.textContent = `harness: ${msg}`;
|
|
35
|
+
status.style.color = color;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
window.__harness = {
|
|
39
|
+
errors: [],
|
|
40
|
+
runtimeCalls: [],
|
|
41
|
+
renderStarted: false,
|
|
42
|
+
mounted: false,
|
|
43
|
+
mode,
|
|
44
|
+
route: descriptor.route && descriptor.route.slug,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
window.addEventListener('error', (event) => {
|
|
48
|
+
window.__harness.errors.push({
|
|
49
|
+
type: 'window.error',
|
|
50
|
+
message: event.message,
|
|
51
|
+
stack: event.error && event.error.stack,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
55
|
+
window.__harness.errors.push({
|
|
56
|
+
type: 'unhandledrejection',
|
|
57
|
+
reason: String((event.reason && event.reason.stack) || event.reason),
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function record(op, args) {
|
|
62
|
+
window.__harness.runtimeCalls.push({ op, args });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readJsonOrSnippet(response) {
|
|
66
|
+
const text = await response.text().catch(() => '');
|
|
67
|
+
if (!text) return { payload: null, snippet: null };
|
|
68
|
+
try {
|
|
69
|
+
return { payload: JSON.parse(text), snippet: null };
|
|
70
|
+
} catch {
|
|
71
|
+
const trimmed = text.trim();
|
|
72
|
+
return { payload: null, snippet: trimmed.length > 120 ? `${trimmed.slice(0, 120)}...` : trimmed };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runtimeQuery(body) {
|
|
77
|
+
if (!apiBase || !jwt || !descriptor.app || !descriptor.app.id) {
|
|
78
|
+
throw new Error('live harness mode requires apiBase, jwt, and app id');
|
|
79
|
+
}
|
|
80
|
+
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}/portal_views/runtime_query`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
Authorization: `Bearer ${jwt}`,
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
app_id: descriptor.app.id,
|
|
88
|
+
route_slug: descriptor.route && descriptor.route.slug,
|
|
89
|
+
...body,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
const { payload, snippet } = await readJsonOrSnippet(response);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const message = (payload && (payload.error || payload.message)) || snippet || `status ${response.status}`;
|
|
95
|
+
throw new Error(`Runtime request failed: ${message}`);
|
|
96
|
+
}
|
|
97
|
+
if (!payload) {
|
|
98
|
+
throw new Error(snippet ? `Runtime response was not JSON: ${snippet}` : 'Runtime response was empty');
|
|
99
|
+
}
|
|
100
|
+
if (payload.status === 'error' || payload.error) {
|
|
101
|
+
throw new Error(payload.message || payload.error || 'Runtime request failed');
|
|
102
|
+
}
|
|
103
|
+
return payload;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stubRuntime() {
|
|
107
|
+
return {
|
|
108
|
+
app: descriptor.app,
|
|
109
|
+
route: descriptor.route,
|
|
110
|
+
databases: descriptor.databases || [],
|
|
111
|
+
context: descriptor.context || { collectionItem: null },
|
|
112
|
+
navigate: (args) => record('navigate', args),
|
|
113
|
+
registerTopBarSearch: () => {},
|
|
114
|
+
setTopBarSearchValue: () => {},
|
|
115
|
+
setTopBarSearchLoading: () => {},
|
|
116
|
+
listTools: async () => {
|
|
117
|
+
record('listTools', {});
|
|
118
|
+
return descriptor.tools || [];
|
|
119
|
+
},
|
|
120
|
+
callTool: async (name, args) => {
|
|
121
|
+
record('callTool', { name, arguments: args || {} });
|
|
122
|
+
return { ok: true, result: null };
|
|
123
|
+
},
|
|
124
|
+
queryDatabase: async (args) => {
|
|
125
|
+
record('queryDatabase', args || {});
|
|
126
|
+
return { documents: [], next_offset: null };
|
|
127
|
+
},
|
|
128
|
+
getDocument: async (args) => {
|
|
129
|
+
record('getDocument', args || {});
|
|
130
|
+
return null;
|
|
131
|
+
},
|
|
132
|
+
upsertDocument: async (args) => {
|
|
133
|
+
record('upsertDocument', args || {});
|
|
134
|
+
return {
|
|
135
|
+
status: 'ok',
|
|
136
|
+
document: {
|
|
137
|
+
id: (args && args.documentId) || 'mock-doc',
|
|
138
|
+
title: (args && args.title) || '',
|
|
139
|
+
properties: (args && args.properties) || {},
|
|
140
|
+
databaseSlug: args && args.databaseSlug,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
listCollectionItems: async (args) => {
|
|
145
|
+
record('listCollectionItems', args || {});
|
|
146
|
+
return { items: [] };
|
|
147
|
+
},
|
|
148
|
+
listCollectionTree: async (args) => {
|
|
149
|
+
record('listCollectionTree', args || {});
|
|
150
|
+
return { items: [] };
|
|
151
|
+
},
|
|
152
|
+
request: async (path, options) => {
|
|
153
|
+
record('request', { path, options });
|
|
154
|
+
return null;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function liveRuntime() {
|
|
160
|
+
return {
|
|
161
|
+
...stubRuntime(),
|
|
162
|
+
listTools: async () => {
|
|
163
|
+
record('listTools', {});
|
|
164
|
+
const result = await runtimeQuery({ method: 'tools/list' });
|
|
165
|
+
return result.tools || [];
|
|
166
|
+
},
|
|
167
|
+
callTool: async (name, args) => {
|
|
168
|
+
record('callTool', { name, arguments: args || {} });
|
|
169
|
+
return runtimeQuery({ method: 'tools/call', name, arguments: args || {} });
|
|
170
|
+
},
|
|
171
|
+
queryDatabase: async (args) => {
|
|
172
|
+
record('queryDatabase', args || {});
|
|
173
|
+
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_query_database', arguments: args || {} });
|
|
174
|
+
return result.result || result;
|
|
175
|
+
},
|
|
176
|
+
getDocument: async (args) => {
|
|
177
|
+
record('getDocument', args || {});
|
|
178
|
+
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_get_document', arguments: args || {} });
|
|
179
|
+
return result.result || result;
|
|
180
|
+
},
|
|
181
|
+
upsertDocument: async (args) => {
|
|
182
|
+
record('upsertDocument', args || {});
|
|
183
|
+
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_upsert_document', arguments: args || {} });
|
|
184
|
+
return result.result || result;
|
|
185
|
+
},
|
|
186
|
+
listCollectionItems: async (args) => {
|
|
187
|
+
record('listCollectionItems', args || {});
|
|
188
|
+
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_list_collection_items', arguments: args || {} });
|
|
189
|
+
return result.result || result;
|
|
190
|
+
},
|
|
191
|
+
request: async (path, options) => {
|
|
192
|
+
record('request', { path, options });
|
|
193
|
+
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}${path}`, {
|
|
194
|
+
method: (options && options.method) || 'GET',
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${jwt}`,
|
|
197
|
+
...((options && options.body) ? { 'Content-Type': 'application/json' } : {}),
|
|
198
|
+
...((options && options.headers) || {}),
|
|
199
|
+
},
|
|
200
|
+
body: options && options.body ? JSON.stringify(options.body) : undefined,
|
|
201
|
+
});
|
|
202
|
+
const { payload, snippet } = await readJsonOrSnippet(response);
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
throw new Error((payload && (payload.error || payload.message)) || snippet || `Request failed with status ${response.status}`);
|
|
205
|
+
}
|
|
206
|
+
return payload;
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
setStatus('loading react');
|
|
213
|
+
const React = await import('react');
|
|
214
|
+
const { createRoot } = await import('react-dom/client');
|
|
215
|
+
|
|
216
|
+
setStatus('loading bundle');
|
|
217
|
+
const bundle = await import('./bundle/app.js');
|
|
218
|
+
|
|
219
|
+
setStatus('binding runtime context');
|
|
220
|
+
const RuntimeContext = globalThis[Symbol.for('notis.sdk.runtime_context')];
|
|
221
|
+
if (!RuntimeContext) {
|
|
222
|
+
throw new Error('runtime context Symbol not initialized by bundle');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const Page = bundle[routeExport];
|
|
226
|
+
if (!Page) {
|
|
227
|
+
throw new Error(`bundle has no ${routeExport} export`);
|
|
228
|
+
}
|
|
229
|
+
const Shell = bundle.__AppShell;
|
|
230
|
+
const runtime = mode === 'live' ? liveRuntime() : stubRuntime();
|
|
231
|
+
const Root = () =>
|
|
232
|
+
React.createElement(
|
|
233
|
+
RuntimeContext.Provider,
|
|
234
|
+
{ value: runtime },
|
|
235
|
+
Shell
|
|
236
|
+
? React.createElement(Shell, null, React.createElement(Page, null))
|
|
237
|
+
: React.createElement(Page, null),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
setStatus('mounting');
|
|
241
|
+
window.__harness.renderStarted = true;
|
|
242
|
+
createRoot(document.getElementById('root')).render(React.createElement(Root));
|
|
243
|
+
|
|
244
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
245
|
+
window.__harness.mounted = true;
|
|
246
|
+
const errors = window.__harness.errors.length;
|
|
247
|
+
const calls = window.__harness.runtimeCalls.length;
|
|
248
|
+
setStatus(`mounted (errors=${errors}, runtimeCalls=${calls})`, errors ? '#f55' : '#0f0');
|
|
249
|
+
}));
|
|
250
|
+
} catch (error) {
|
|
251
|
+
window.__harness.errors.push({
|
|
252
|
+
type: 'boot',
|
|
253
|
+
message: error && error.message ? error.message : String(error),
|
|
254
|
+
stack: error && error.stack,
|
|
255
|
+
});
|
|
256
|
+
setStatus(`boot failed: ${error && error.message ? error.message : String(error)}`, '#f55');
|
|
257
|
+
}
|
|
258
|
+
</script>
|
|
259
|
+
</body>
|
|
260
|
+
</html>
|
package/template/app/layout.tsx
CHANGED
package/template/notis.config.ts
CHANGED
|
@@ -8,7 +8,22 @@ export default defineNotisApp({
|
|
|
8
8
|
databases: ['items'],
|
|
9
9
|
|
|
10
10
|
routes: [
|
|
11
|
-
{ path: '/', name: 'Home', icon: 'lucide:home', default: true },
|
|
11
|
+
{ path: '/', slug: 'home', name: 'Home', icon: 'lucide:home', default: true },
|
|
12
|
+
// Example collection tree route:
|
|
13
|
+
// {
|
|
14
|
+
// path: '/notes',
|
|
15
|
+
// slug: 'notes',
|
|
16
|
+
// name: 'Notes',
|
|
17
|
+
// collection: {
|
|
18
|
+
// database: 'notes',
|
|
19
|
+
// titleProperty: 'Name',
|
|
20
|
+
// parentProperty: 'Parent',
|
|
21
|
+
// sidebar: {
|
|
22
|
+
// mode: 'tree',
|
|
23
|
+
// allowCreate: true,
|
|
24
|
+
// },
|
|
25
|
+
// },
|
|
26
|
+
// },
|
|
12
27
|
],
|
|
13
28
|
|
|
14
29
|
tools: [
|