ugly-app 0.1.873 → 0.1.875
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 +139 -107
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/components/DeviceSimulator.d.ts.map +1 -1
- package/dist/client/components/DeviceSimulator.js +1 -12
- package/dist/client/components/DeviceSimulator.js.map +1 -1
- package/dist/client/deviceProfiles.d.ts +47 -0
- package/dist/client/deviceProfiles.d.ts.map +1 -0
- package/dist/client/deviceProfiles.js +22 -0
- package/dist/client/deviceProfiles.js.map +1 -0
- package/dist/client/inspect/keyboard.js +28 -4
- package/dist/client/inspect/keyboard.js.map +1 -1
- package/dist/playwright/index.d.ts +2 -0
- package/dist/playwright/index.d.ts.map +1 -1
- package/dist/playwright/index.js +5 -0
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/mobile.d.ts +45 -0
- package/dist/playwright/mobile.d.ts.map +1 -0
- package/dist/playwright/mobile.js +114 -0
- package/dist/playwright/mobile.js.map +1 -0
- package/dist/server/sqlite/SqliteDoc.d.ts.map +1 -1
- package/dist/server/sqlite/SqliteDoc.js +37 -15
- package/dist/server/sqlite/SqliteDoc.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/client/components/DeviceSimulator.tsx +4 -15
- package/src/client/deviceProfiles.ts +32 -0
- package/src/client/inspect/keyboard.ts +28 -4
- package/src/playwright/index.ts +14 -0
- package/src/playwright/mobile.ts +148 -0
- package/src/server/sqlite/SqliteDoc.test.ts +51 -0
- package/src/server/sqlite/SqliteDoc.ts +47 -22
- package/templates/.claude/skills/eval-swarm/SKILL.md +155 -14
package/README.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
# ugly-app
|
|
2
2
|
|
|
3
|
-
A full-stack TypeScript framework for shipping production web apps with one CLI. Scaffold with `npx ugly-app init my-app` and get an opinionated Express + React + PostgreSQL stack with built-in auth, type-safe RPC over WebSocket and HTTP, real-time document tracking, AI generation, storage, and a CLI for every workflow.
|
|
3
|
+
A full-stack TypeScript framework for shipping production web apps with one CLI. Scaffold with `npx ugly-app init my-app` and get an opinionated Express + React + PostgreSQL stack with built-in auth, type-safe RPC over WebSocket and HTTP, real-time document tracking, AI generation, storage, cron/workers, and a CLI for every workflow.
|
|
4
4
|
|
|
5
|
-
ugly-app is designed to be deployed and operated through [ugly.bot](https://ugly.bot) — the platform
|
|
5
|
+
ugly-app is designed to be deployed and operated through [ugly.bot](https://ugly.bot) — the platform provides auth, infra (PostgreSQL, Qdrant, NATS, S3-compatible object storage), AI provider keys, and deployment. Your app talks to all of this through the project's dev tunnel and the per-app `UGLY_BOT_TOKEN`.
|
|
6
6
|
|
|
7
7
|
## What's included
|
|
8
8
|
|
|
9
9
|
- **Server**: Express + WebSocket with type-safe RPC and Zod validation
|
|
10
|
-
- **Client**: React + Vite with typed routing, lazy pages, animated transitions, popup
|
|
11
|
-
- **Database**: PostgreSQL (JSONB) via the data proxy, with full-text search
|
|
12
|
-
- **Auth**: HttpOnly cookies + JWT
|
|
13
|
-
- **AI**: Text, image, embeddings, web search — all proxied through ugly.bot (no per-provider keys in your app)
|
|
10
|
+
- **Client**: React + Vite with typed routing, lazy pages, animated transitions, popup layer
|
|
11
|
+
- **Database**: PostgreSQL (JSONB) via the data proxy, with full-text search and vector search (Qdrant)
|
|
12
|
+
- **Auth**: HttpOnly cookies + JWT; ugly.bot OAuth out of the box, magic-link / Google via `mode: 'self'`
|
|
13
|
+
- **AI**: Text, JSON, image, embeddings, web search — all proxied through ugly.bot (no per-provider keys in your app)
|
|
14
14
|
- **Realtime**: NATS pub/sub and document change subscriptions (`trackDoc` / `trackDocs`)
|
|
15
15
|
- **Storage**: S3-compatible buckets with presigned uploads
|
|
16
16
|
- **Workers & cron**: `setWorkers()` registers named async tasks with optional Zod input schemas and cron schedules
|
|
17
17
|
- **Localization**: Strings tables with critical-string SSR injection
|
|
18
18
|
- **Experiments**: Deterministic A/B bucketing tied to event logging
|
|
19
|
+
- **Two-adapter deploy**: Same source runs on Node (Express) or Cloudflare Workers (Hono + Durable Objects)
|
|
19
20
|
- **CLI**: `ugly-app` commands for dev, build, deploy, migrations, logs, AI, and auth
|
|
20
21
|
|
|
21
22
|
## Quick start
|
|
@@ -26,7 +27,7 @@ cd my-app
|
|
|
26
27
|
npm run dev
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
The scaffold gives you a working app at `http://localhost:4321` with todo CRUD, AI chat, file upload, auth demo, collab editing, and
|
|
30
|
+
The scaffold gives you a working app at `http://localhost:4321` with todo CRUD, AI chat, file upload, auth demo, collab editing, and other test pages wired up.
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
@@ -40,10 +41,11 @@ The single server entry point. Returns an `App` that owns Express, the WebSocket
|
|
|
40
41
|
import {
|
|
41
42
|
createApp,
|
|
42
43
|
type AppConfigurator,
|
|
44
|
+
type InboundEmail,
|
|
43
45
|
type RequestHandlers,
|
|
44
46
|
} from 'ugly-app';
|
|
45
|
-
import { dbDefaults } from 'ugly-app/shared';
|
|
46
47
|
import { nanoid } from 'nanoid';
|
|
48
|
+
import { dbDefaults } from 'ugly-app/shared';
|
|
47
49
|
import { requests, messages } from '../shared/api';
|
|
48
50
|
import { collections } from '../shared/collections';
|
|
49
51
|
import { pages } from '../shared/pages';
|
|
@@ -53,7 +55,9 @@ const app = createApp(
|
|
|
53
55
|
{
|
|
54
56
|
createTodo: async (userId, { text }) => {
|
|
55
57
|
const _id = nanoid();
|
|
56
|
-
await app.db.setDoc(collections.todo, {
|
|
58
|
+
await app.db.setDoc(collections.todo, {
|
|
59
|
+
_id, userId, text, done: false, ...dbDefaults(),
|
|
60
|
+
});
|
|
57
61
|
return { id: _id };
|
|
58
62
|
},
|
|
59
63
|
} satisfies RequestHandlers<typeof requests>,
|
|
@@ -69,37 +73,43 @@ await app.start(parseInt(process.env['PORT'] ?? '4321'));
|
|
|
69
73
|
**Signature:**
|
|
70
74
|
|
|
71
75
|
```typescript
|
|
72
|
-
function createApp<
|
|
76
|
+
function createApp<
|
|
77
|
+
R extends AppRegistryBase,
|
|
78
|
+
Defs extends CollectionDefRegistry,
|
|
79
|
+
>(
|
|
73
80
|
registry: R, // { requests, messages }
|
|
74
81
|
requests: Partial<RequestHandlers<R['requests']>>, // handler implementations
|
|
75
82
|
appDefs: Defs, // collections from defineCollections()
|
|
76
83
|
configure?: (c: AppConfigurator) => void,
|
|
77
84
|
deleteHandlers?: DeleteHandlers<Defs>, // per-collection onDelete hooks
|
|
78
|
-
): App<CollectionMap<typeof BUILTIN_DEFS & Defs>>;
|
|
85
|
+
): App<CollectionMap<typeof BUILTIN_DEFS & Defs>, RegistryPages<R>>;
|
|
79
86
|
```
|
|
80
87
|
|
|
81
|
-
The returned `App` object
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
The returned `App` object exposes:
|
|
89
|
+
|
|
90
|
+
| Field | Description |
|
|
91
|
+
|-------|-------------|
|
|
92
|
+
| `start(port?)` | Start the server (default port 3000; templates use 4321). |
|
|
93
|
+
| `db` | The `TypedDB<Map>` instance (map is inferred from your collections + framework built-ins). |
|
|
94
|
+
| `httpServer` | The underlying Node `http.Server`. |
|
|
95
|
+
| `wss` | The main `WebSocketServer` (path set by `setWsPath`, default `/rpc`). |
|
|
96
|
+
| `dispatch(name, input, userId)` | Invoke an RPC handler programmatically (inherited from `AppRouter`). |
|
|
97
|
+
| `registerRoutes(fn)` | Mount more Express routes after creation. |
|
|
98
|
+
| `pushSend(input)` | Send a push notification whose click-through target is a route from this app's `pages` table. `page` is type-checked against route keys (when `pages` is provided on the registry), `query` against that route's params. The framework builds the absolute URL and stamps project identity so the client can open the dock app. |
|
|
89
99
|
|
|
90
|
-
Framework-managed background services start automatically
|
|
100
|
+
Framework-managed background services start automatically inside `app.start()`: schema drift check, NATS connection + KV buckets, data-proxy connection, event-counter flush, TTL cleanup for log tables, console/error capture, and ugly.bot log forwarding. Individual subsystems (Postgres, NATS, storage, AI providers) are loaded lazily so a host without `DATABASE_URL` / `NATS_URL` / `R2_BUCKET` doesn't pay their startup cost.
|
|
91
101
|
|
|
92
102
|
### `AppConfigurator`
|
|
93
103
|
|
|
94
|
-
Passed to the optional
|
|
104
|
+
Passed to the optional 4th argument of `createApp`. Every method is optional (except `setPages` if you want the SPA served).
|
|
95
105
|
|
|
96
106
|
| Method | Description |
|
|
97
107
|
|--------|-------------|
|
|
98
|
-
| `setPages({ pages, renderPage?, clientDistPath? })` | Mount the SPA. In dev, runs Vite in middleware mode; in prod, serves `dist/client`. Provide `renderPage
|
|
99
|
-
| `setUserHelper(helper)` | Customize how the framework reads
|
|
100
|
-
| `setOnUserCreate(handler)` | Called on first login with `(userId, { email?, phone? }, db)` —
|
|
101
|
-
| `setAuth(provider)` | Replace the default
|
|
102
|
-
| `setOnSocketMessage(handler)` | Single raw-WebSocket message handler.
|
|
108
|
+
| `setPages({ pages, renderPage?, clientDistPath? })` | Mount the SPA. In dev, runs Vite in middleware mode; in prod, serves `dist/client`. Provide `renderPage(routeName, params) => Promise<string>` for SSR on pages marked `ssr: true`. |
|
|
109
|
+
| `setUserHelper(helper)` | Customize how the framework reads/writes the `user` collection during WebSocket auth (default looks up by id in a generic `user` collection). |
|
|
110
|
+
| `setOnUserCreate(handler)` | Called on first login with `(userId, { email?, phone? }, db)` — the app's chance to create the user record. |
|
|
111
|
+
| `setAuth(provider)` | Replace the default auth provider. Must implement `verify(code)`, `authUrl(origin)`, optional `registerRoutes(router, { db, onUserCreate })`. |
|
|
112
|
+
| `setOnSocketMessage(handler)` | Single raw-WebSocket message handler. `(ws, userId, msg) => boolean` — return `true` to consume, `false` to fall through. |
|
|
103
113
|
| `addSocketMessageHandler(handler)` | Append to the handler chain; first to return `true` wins. |
|
|
104
114
|
| `setWsPath(path)` | Override the WebSocket path (default `/rpc`). |
|
|
105
115
|
| `setOnWsAuth(handler)` | `(ws, userId, req) => void` — fires after a socket session authenticates. |
|
|
@@ -108,16 +118,16 @@ Passed to the optional fourth argument of `createApp`. Every method is optional.
|
|
|
108
118
|
| `setHealthHandler(fn)` | Override the default `GET /health` response. |
|
|
109
119
|
| `setExperiments(experiments)` | Register `Experiment` definitions for `initSession` / `captureEvent` bucketing. |
|
|
110
120
|
| `setIsAdmin(fn)` | `(userId, db) => boolean \| Promise<boolean>` — gate for admin-only framework requests (`adminCreateTestUser` / `list` / `delete`). Defaults to matching `MAINTAIN_BOT_USER_ID`. |
|
|
111
|
-
| `setOnEmail(handler)` | Handle inbound emails routed to `{domain}@ugly.bot` (
|
|
121
|
+
| `setOnEmail(handler)` | Handle inbound emails routed to `{domain}@ugly.bot` (delivered via internal HTTP). |
|
|
112
122
|
| `setCronTasks(tasks, handlers)` | Legacy cron-only registry. Prefer `setWorkers()`. |
|
|
113
|
-
| `setWorkers(workers, handlers)` | Register named async tasks with optional Zod input schema and cron schedule. Powers
|
|
123
|
+
| `setWorkers(workers, handlers)` | Register named async tasks with optional Zod input schema and cron schedule. Powers `GET /_workers/manifest`, `POST /_workers/run`, and the cron orchestrator. |
|
|
114
124
|
| `setStrings(config)` | Localization config — framework injects language + critical strings into SSR HTML and exposes `resolveLanguage` / `getCriticalStrings`. |
|
|
115
125
|
| `registerRoutes(fn)` | Mount custom Express routes. |
|
|
116
126
|
| `setWorkerQueue(queue)` | Register a `WorkerQueue` with `start()` / `stop()` for app lifecycle management. |
|
|
117
127
|
|
|
118
128
|
### Handler signatures
|
|
119
129
|
|
|
120
|
-
Handlers are plain async functions — no context object
|
|
130
|
+
Handlers are plain async functions — no context object. Access state via captured imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, etc.).
|
|
121
131
|
|
|
122
132
|
```typescript
|
|
123
133
|
// req() — public, userId may be null
|
|
@@ -127,11 +137,9 @@ getPublicData: async (userId: string | null, input) => { ... }
|
|
|
127
137
|
getMe: async (userId: string, input) => { ... }
|
|
128
138
|
```
|
|
129
139
|
|
|
130
|
-
Inside a handler, access state via captured imports — `app.db`, `storage`, `pgQuery`, `uglyBotRequest`, etc. There is no injected context.
|
|
131
|
-
|
|
132
140
|
### Built-in framework requests
|
|
133
141
|
|
|
134
|
-
`createApp` automatically registers several framework handlers, accessible from any client via the normal RPC pipeline
|
|
142
|
+
`createApp` automatically registers several framework handlers, accessible from any client via the normal RPC pipeline. App-provided handlers with the same name override the framework's defaults.
|
|
135
143
|
|
|
136
144
|
| Name | Purpose |
|
|
137
145
|
|------|---------|
|
|
@@ -142,12 +150,10 @@ Inside a handler, access state via captured imports — `app.db`, `storage`, `pg
|
|
|
142
150
|
| `kagiSearch` / `kagiSummarize` / `kagiEnrichWeb` / `kagiEnrichNews` | Web search via ugly.bot. |
|
|
143
151
|
| `uploadUrl` | Issues a presigned PUT for the `temp` bucket. |
|
|
144
152
|
| `shareLink` | Mint a `https://ugly.bot/l/<code>` short link with OG metadata (see the "sharing links" rule in `CLAUDE.md`). |
|
|
145
|
-
| `feedbackReportCreateNoAuth` / `errorLogCaptureNoAuth` | Same-origin, public endpoints used by the built-in browser telemetry
|
|
153
|
+
| `feedbackReportCreateNoAuth` / `errorLogCaptureNoAuth` | Same-origin, public endpoints used by the built-in browser telemetry to write into the project's own Postgres. |
|
|
146
154
|
| `submitFeedbackBot` | Forwards `db.captureFeedback` writes for the maintain-bot persona. |
|
|
147
|
-
| `feedbackReportResolve` | Admin-only — resolve or decline a feedback report
|
|
148
|
-
| `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only
|
|
149
|
-
|
|
150
|
-
App-provided handlers with the same name override the framework's defaults.
|
|
155
|
+
| `feedbackReportResolve` | Admin-only — resolve or decline a feedback report. |
|
|
156
|
+
| `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only synthetic-user management. Gated by `setIsAdmin()` (or `MAINTAIN_BOT_USER_ID`). |
|
|
151
157
|
|
|
152
158
|
---
|
|
153
159
|
|
|
@@ -214,7 +220,7 @@ export const collections = defineCollections({
|
|
|
214
220
|
- `search?: { fields, language? }` — PostgreSQL full-text search columns.
|
|
215
221
|
- `vector?: { dimensions, source }` — Qdrant vector index over the named JSONB path.
|
|
216
222
|
|
|
217
|
-
All documents extend `DBObject`: `{ _id, version, created, updated }`. Use `dbDefaults()` to stamp
|
|
223
|
+
All documents extend `DBObject`: `{ _id, version, created, updated }`. Use `dbDefaults()` to stamp `version`/`created`/`updated` on inserts. **Always generate `_id` with `nanoid()`** — never `crypto.randomUUID()`, `Date.now()`, or `Math.random()` (see `CLAUDE.md`).
|
|
218
224
|
|
|
219
225
|
After schema changes, run `npm run db:schema-gen` and then `npm run db:migrate`. The app refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true` only as a last resort).
|
|
220
226
|
|
|
@@ -226,18 +232,22 @@ import { definePage, definePages } from 'ugly-app/shared';
|
|
|
226
232
|
export const pages = definePages({
|
|
227
233
|
'': definePage<{}>({ auth: false }), // /
|
|
228
234
|
'user/:userId': definePage<{ userId: string }>(), // /user/abc
|
|
229
|
-
'search': definePage<{ q?: string }>({ auth: false }),
|
|
235
|
+
'search': definePage<{ q?: string }>({ auth: false, cacheQuery: ['q'] }),
|
|
230
236
|
'blog/*slug': definePage<{ slug: string }>({ ssr: true }), // /blog/any/path
|
|
231
237
|
});
|
|
232
238
|
export type AppPages = typeof pages;
|
|
233
239
|
```
|
|
234
240
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
- `auth`
|
|
238
|
-
- `
|
|
239
|
-
- `
|
|
240
|
-
-
|
|
241
|
+
`definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime object carrying `PageMeta` plus a phantom `_params` used only for TypeScript inference. Options:
|
|
242
|
+
|
|
243
|
+
- `auth` (default `true`) — protected route; router surfaces the framework's `<AuthRoot>` on the client and the server injects `window.__AUTH_TOKEN__` only for a verified cookie.
|
|
244
|
+
- `ssr` (default `false`) — server-render the page for SEO. `{ auth: true, ssr: true }` is dropped to `ssr: false` with a warning: the SSR document is served from a shared edge cache and must not depend on the viewer.
|
|
245
|
+
- `cacheQuery?: string[]` — query params that participate in the SSR edge-cache key. Anything NOT listed bypasses the cache instead of poisoning it (e.g. `cacheQuery: ['q']` on a search page).
|
|
246
|
+
- `ssrCacheTimeout?: number` — edge cache lifetime in seconds. Default is effectively 1 year (`DEFAULT_SSR_CACHE_TIMEOUT`) because the buildId is part of the cache key. Override only for pages that drift between deploys.
|
|
247
|
+
|
|
248
|
+
Path syntax: `:param` matches a single path segment; `*param` is greedy (captures slashes). Query-string params are declared in `Params` but never appear in the path template.
|
|
249
|
+
|
|
250
|
+
`definePages<T>(p)` is a pass-through identity function that widens no types — use it to give the registry a name.
|
|
241
251
|
|
|
242
252
|
---
|
|
243
253
|
|
|
@@ -268,20 +278,22 @@ bootstrapApp({
|
|
|
268
278
|
|-------|-------------|
|
|
269
279
|
| `requests` | Your `RequestRegistry` (merged with framework requests internally). |
|
|
270
280
|
| `messages?` | Your `MessageRegistry` (merged with framework messages). |
|
|
271
|
-
| `RouterProvider` | The `RouterProvider` returned from `createRouter()`. |
|
|
281
|
+
| `RouterProvider` | The `RouterProvider` component returned from `createRouter()`. |
|
|
272
282
|
| `render` | Callback returning the app's UI tree (typically `<RouterView />`). |
|
|
273
283
|
| `root?` | Root element / selector (default `'#root'`). |
|
|
274
284
|
| `fallback?` | UI for unmatched routes (default: tiny "404"). |
|
|
275
285
|
| `socketUrl?` | Override the WebSocket path (default `/rpc`). |
|
|
276
286
|
| `strings?` | Localization config — when present, wraps the tree with `<StringsProvider>`. |
|
|
277
287
|
| `keyboard?: false` | Disable the framework `<KeyboardProvider>` wrapper. |
|
|
278
|
-
| `silentSso?` | Apex-domain apps (ugly.chat, ugly.press, …) that use ugly.bot as their auth authority but don't share its cookie. When true
|
|
288
|
+
| `silentSso?` | Apex-domain apps (ugly.chat, ugly.press, …) that use ugly.bot as their auth authority but don't share its cookie. When `true`, a logged-out boot attempts a top-level SSO redirect (once per tab, guarded) to adopt an existing ugly.bot session. Leave unset for `*.ugly.bot` subdomains and Mode B (self-auth) apps. |
|
|
289
|
+
|
|
290
|
+
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after cookie verification). If absent, it renders unauthenticated immediately — the router's per-route auth guard surfaces the framework's `<AuthRoot>` (Mode A → `<LoginPopup>`; Mode B → magic-link form + optional Google button) only when the user opens a protected route. If the token is present, it connects the socket, mounts `<AppProvider>`, and renders.
|
|
279
291
|
|
|
280
|
-
|
|
292
|
+
After render (both logged-in and logged-out), a hidden iframe silently calls `${UGLY_BOT_URL}/oauth/silent`: if it returns a fresh code, the cookie is refreshed via `POST /auth/verify`; if the returned account differs from the current session, the page reloads onto the live account (guarded once per from→to pair to prevent loops). Any `?ugly_oauth_code=…` on the URL (redirect-fallback OAuth code from ugly.bot) is redeemed at the very top of bootstrap before anything renders.
|
|
281
293
|
|
|
282
|
-
|
|
294
|
+
If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it renders `<MagicLinkCallback>` instead of the regular app shell — that path is the fallback target when the server route is shadowed by a static-assets handler.
|
|
283
295
|
|
|
284
|
-
|
|
296
|
+
The previous first-paint-blocking `<AutoLoginGate>` iframe was removed: logged-out landing pages now paint immediately.
|
|
285
297
|
|
|
286
298
|
### Routing — `createRouter()`
|
|
287
299
|
|
|
@@ -291,21 +303,31 @@ import { createRouter } from 'ugly-app/client';
|
|
|
291
303
|
import { pages } from '../shared/pages';
|
|
292
304
|
import { allPages } from './allPages';
|
|
293
305
|
|
|
294
|
-
export const {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
306
|
+
export const {
|
|
307
|
+
RouterProvider,
|
|
308
|
+
RouterView,
|
|
309
|
+
useRouter,
|
|
310
|
+
Link,
|
|
311
|
+
setAllPages,
|
|
312
|
+
} = createRouter({ pages, allPages });
|
|
298
313
|
```
|
|
299
314
|
|
|
300
|
-
Config
|
|
315
|
+
**Config:**
|
|
316
|
+
- `pages` (required) — the `PageRegistry` from `shared/pages.ts`.
|
|
317
|
+
- `allPages?` — the lazy route → element map (`PageMap<Pages>`). Prefer registering via `setAllPages()` from the browser entry when the app also server-renders — the Worker bundle has no code splitting, so a static import inlines every page (and its deps) into `worker.js`.
|
|
318
|
+
- `ssrPages?` — `Partial<{ [K]: ComponentType<Params> }>` of statically-imported components for `ssr: true` routes. Used by both the Worker's `renderToString` and the client's first hydration render so the initial paint resolves synchronously and never mismatches the server output.
|
|
301
319
|
|
|
302
|
-
|
|
320
|
+
**`createRouter` returns:**
|
|
303
321
|
|
|
304
|
-
- **`RouterProvider`** — props: `children`, `fallback?`, `isAuthenticated?`, `initialUrl?` (`{ pathname, search }
|
|
305
|
-
- **`RouterView`** — renders the active page with animated transitions. Props: `durationMs?`, `easing?`, `transitionComponent?` (replaces `ViewFlipper`), `renderPage
|
|
306
|
-
- **`useRouter()`** — returns the
|
|
322
|
+
- **`RouterProvider`** — props: `children`, `fallback?`, `isAuthenticated?`, `initialUrl?` (`{ pathname, search }`, required for SSR + hydration). Manages route state, browser history, and the popup layer. When `isAuthenticated()` returns false on a route with `auth: true`, the framework's `<AuthRoot>` renders synchronously in place of the page — apps cannot override this fallback (the framework owns the login UX so an app can't accidentally ship an auth-required route with no working login button).
|
|
323
|
+
- **`RouterView`** — renders the active page with animated transitions. Props: `durationMs?`, `easing?`, `transitionComponent?` (replaces the default `ViewFlipper`), `renderPage?(state) => ReactElement` (sync alternative to `allPages` loaders, called with `RouterStateRaw`).
|
|
324
|
+
- **`useRouter()`** — returns the `RouterContextValue<Pages>` (typed navigation + popup API). Throws if used outside `<RouterProvider>`.
|
|
307
325
|
- **`Link`** — typed SPA link bound to this router's pages. Renders a real `<a>` (right-click / cmd-click / SEO keep working) but intercepts a plain left-click and routes through `push` / `replace`. Props: `to`, `params` (both type-checked against `pages`), `replace?`, `children`, plus the usual anchor attrs.
|
|
308
|
-
- **`setAllPages(map)`** — register the lazy `PageMap<Pages>` after construction (typically from the browser entry).
|
|
326
|
+
- **`setAllPages(map)`** — register the lazy `PageMap<Pages>` after construction (typically from the browser entry). Keeps the Worker's import graph free of every lazy page while the client still gets code-split routes.
|
|
327
|
+
|
|
328
|
+
A standalone `Link` is also exported from `ugly-app/client` for code that can't easily reach the typed one; it accepts an optional `router` prop and falls back to `<RouterProvider>` context.
|
|
329
|
+
|
|
330
|
+
**Never use a bare `<a href="/route">`** for internal navigation — it triggers a full document reload (white flash + repaint). See the "client navigation" rule in `CLAUDE.md`.
|
|
309
331
|
|
|
310
332
|
### Page map — `lazyPage` / `lazyPageLoader`
|
|
311
333
|
|
|
@@ -325,6 +347,8 @@ export const allPages = {
|
|
|
325
347
|
- **`lazyPage(factory)`** — lazy-imports a default-exported `React.ComponentType<Params>`. The page receives route params as props.
|
|
326
348
|
- **`lazyPageLoader(factory)`** — lazy-imports an async loader `(params) => Promise<ReactElement>`. Use when a route needs data fetching before render. The loader file is the chunk boundary, so it can statically import its page component.
|
|
327
349
|
|
|
350
|
+
Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch dynamically imported module"): on the first failure they trigger a single `window.location.reload()` (guarded by `sessionStorage` so a genuinely broken chunk can't loop) to fetch the fresh `index.html` with new chunk hashes.
|
|
351
|
+
|
|
328
352
|
Example loader:
|
|
329
353
|
|
|
330
354
|
```typescript
|
|
@@ -340,7 +364,7 @@ export default async function PageLoader({ id }: { id: string }) {
|
|
|
340
364
|
|
|
341
365
|
```typescript
|
|
342
366
|
const {
|
|
343
|
-
current, // { routeName, params }
|
|
367
|
+
current, // { routeName, params } — typed union over all pages
|
|
344
368
|
transitionType, // 'PUSH' | 'REPLACE' | 'POP' | 'NONE' — how we got here
|
|
345
369
|
push,
|
|
346
370
|
replace,
|
|
@@ -353,16 +377,13 @@ const {
|
|
|
353
377
|
push('user/:userId', { userId: '123' }); // → /user/123
|
|
354
378
|
replace('search', { q: 'hello' }); // → /search?q=hello
|
|
355
379
|
back(); // browser history back
|
|
356
|
-
|
|
357
|
-
current.routeName; // typed union of all route keys
|
|
358
|
-
current.params; // typed params for the current route
|
|
359
380
|
```
|
|
360
381
|
|
|
361
|
-
|
|
382
|
+
Route names and params are fully typed against `pages`. `push` / `replace` no-op with a `console.error` when `buildUrl()` produces a URL that doesn't match a registered route.
|
|
362
383
|
|
|
363
384
|
### Popups — `openPopup()`
|
|
364
385
|
|
|
365
|
-
|
|
386
|
+
Use `useRouter().openPopup()` for modals, sheets, and menus. The router owns the popup layer, drives the spring animation, and stacks popups z-index-correctly.
|
|
366
387
|
|
|
367
388
|
```tsx
|
|
368
389
|
const { openPopup } = useRouter();
|
|
@@ -380,12 +401,13 @@ const handle = openPopup(<MyContent />, {
|
|
|
380
401
|
handle.hide(); // dismiss programmatically
|
|
381
402
|
```
|
|
382
403
|
|
|
383
|
-
Modes:
|
|
404
|
+
`openPopup` returns a `PopupHandle = { id, hide }`. Modes:
|
|
405
|
+
|
|
384
406
|
- **`block`** (default) — 40% opacity backdrop, **does not** dismiss on backdrop click.
|
|
385
407
|
- **`transient`** — 20% opacity backdrop, **dismisses** on backdrop click.
|
|
386
408
|
- **`contextMenu`** — same as transient, intended for menus and pickers.
|
|
387
409
|
|
|
388
|
-
`renderLayer` receives `{ content, spring, hide }
|
|
410
|
+
`renderLayer` receives `{ content, spring, hide }`: `spring` is a `createAnimatedValue()` result driving 0 → 1, `hide` closes the popup.
|
|
389
411
|
|
|
390
412
|
### `AppProvider` & `useApp()`
|
|
391
413
|
|
|
@@ -393,34 +415,20 @@ Modes:
|
|
|
393
415
|
|
|
394
416
|
```typescript
|
|
395
417
|
const {
|
|
396
|
-
userId, // current user id
|
|
418
|
+
userId, // current user id (string)
|
|
397
419
|
user, // UserBase doc
|
|
398
420
|
socket, // AppSocket — typed RPC client
|
|
399
|
-
uglyBotSocket, //
|
|
421
|
+
uglyBotSocket, // UglyBotSocket | null — for direct platform calls (STT/TTS, etc.)
|
|
400
422
|
showPopup, // legacy popup API (prefer useRouter().openPopup)
|
|
401
423
|
hidePopup,
|
|
402
424
|
hideAllPopups,
|
|
403
|
-
runAsync, //
|
|
404
|
-
splashDone, // mark a splash-screen step complete
|
|
405
|
-
localizer, // (key, params?) => string — alias for useLocalizer
|
|
425
|
+
runAsync, // (label, async () => {...}, options?) — shows loading overlay while pending
|
|
426
|
+
splashDone, // (step: string) — mark a splash-screen step complete
|
|
427
|
+
localizer, // (key, params?) => string — alias for useLocalizer()
|
|
406
428
|
} = useApp();
|
|
407
429
|
```
|
|
408
430
|
|
|
409
|
-
`useApp` is generic in
|
|
410
|
-
|
|
411
|
-
### `Link` component
|
|
412
|
-
|
|
413
|
-
Prefer the typed `Link` returned by `createRouter()` — no `router` prop, `to` / `params` are checked against your pages, and it uses the enclosing `<RouterProvider>` context automatically.
|
|
414
|
-
|
|
415
|
-
```tsx
|
|
416
|
-
// Typed Link — from your `client/router.ts`
|
|
417
|
-
const { Link } = createRouter({ pages, allPages });
|
|
418
|
-
<Link to="user/:userId" params={{ userId: '123' }}>View profile</Link>
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
A standalone `Link` is also exported from `ugly-app/client` for code that can't easily reach the typed one; it accepts an optional `router` prop and falls back to `<RouterProvider>` context.
|
|
422
|
-
|
|
423
|
-
Never use a bare `<a href="/route">` for internal navigation — it triggers a full document reload (white flash + repaint). `Link` intercepts plain left-clicks and lets `ctrl/cmd+click` open in a new tab as expected.
|
|
431
|
+
`useApp<TAsyncOptions>()` is generic in the async-options type so apps can pass a custom option through to a custom `loadingOverlay` element (accepted as an `AppProvider` prop). `useAppOptional()` returns `null` outside the provider; `useLocalizer()` returns a localizer that prefers `<StringsProvider>` data, falls back to the `AppProvider` `localizer` prop, and finally to identity.
|
|
424
432
|
|
|
425
433
|
### Direct socket access
|
|
426
434
|
|
|
@@ -431,8 +439,8 @@ Never use a bare `<a href="/route">` for internal navigation — it triggers a f
|
|
|
431
439
|
| `request(name, input)` | Invoke a typed RPC handler. |
|
|
432
440
|
| `getDoc(collection, id)` | Server-mediated doc fetch. |
|
|
433
441
|
| `getDocs(collection, filter?, opts?)` | Filtered query. |
|
|
434
|
-
| `trackDoc(collection, id, cb)` | Live subscription — returns unsubscribe. |
|
|
435
|
-
| `trackDocs(collection, params, cb)` | Live filtered subscription. |
|
|
442
|
+
| `trackDoc(collection, id, cb)` | Live subscription (with initial snapshot) — returns unsubscribe. |
|
|
443
|
+
| `trackDocs(collection, params, cb)` | Live filtered subscription (with initial snapshot). |
|
|
436
444
|
| `uploadFile(file, key)` | Presigned upload to the `temp` bucket. |
|
|
437
445
|
| `connectionState` | `'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'idle-disconnected'`. |
|
|
438
446
|
| `disconnect()` | Close the connection. |
|
|
@@ -450,17 +458,17 @@ Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
|
450
458
|
- **Mode A — `mode: 'uglybot'`** (default when no `auth` block is present). Auth is delegated to ugly.bot OAuth. AI / email / push proxies bill the end user's ugly.bot credits.
|
|
451
459
|
- **Mode B — `mode: 'self'`**. The app issues its own sessions via magic-link email and/or Google OAuth. AI / email / push proxies bill the developer's ugly.bot account using the project's `AI_PROXY_TOKEN`.
|
|
452
460
|
|
|
453
|
-
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code (incl. `<AuthRoot>`
|
|
461
|
+
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code (incl. `<AuthRoot>`) can branch correctly.
|
|
454
462
|
|
|
455
463
|
### Mode A — ugly.bot OAuth (default)
|
|
456
464
|
|
|
457
465
|
1. Every page request: the server checks the `auth_token` cookie. If it verifies through `${UGLY_BOT_URL}/verify`, the server injects `window.__AUTH_TOKEN__` into the HTML.
|
|
458
466
|
2. Client boot: if `__AUTH_TOKEN__` is present, `bootstrapApp` opens the WebSocket with the token. Otherwise it renders logged-out immediately (no first-paint block).
|
|
459
|
-
3. Background account-sync (always): `bootstrapApp` opens a hidden iframe to `${UGLY_BOT_URL}/oauth/silent` on every load. If it returns a one-time code, the client POSTs `/auth/verify` to refresh the cookie. If the refreshed account's `userId` differs from the current session, the client calls `/auth/logout` and reloads onto the live account (guarded once per from→to pair
|
|
460
|
-
4. Apex apps (`silentSso: true`
|
|
467
|
+
3. Background account-sync (always): `bootstrapApp` opens a hidden iframe to `${UGLY_BOT_URL}/oauth/silent` on every load. If it returns a one-time code, the client POSTs `/auth/verify` to refresh the cookie. If the refreshed account's `userId` differs from the current session, the client calls `/auth/logout` and reloads onto the live account (guarded once per from→to pair).
|
|
468
|
+
4. Apex apps (`silentSso: true`): if the hidden iframe can't read ugly.bot's cookie (Safari / 3p-cookie blocking), a **top-level** redirect to `/oauth/silent` (first-party, bypasses 3p blocking) runs instead — one-shot, marked done in `localStorage` so it can't loop. Skipped for accounts already confirmed live.
|
|
461
469
|
5. Manual login: on protected routes, the per-route auth guard surfaces `<AuthRoot>` → `<LoginPopup>`, which opens `${UGLY_BOT_URL}/oauth` in a popup. On completion, the platform redirects back with either a `postMessage` code or `?ugly_oauth_code=…`; `bootstrapApp` redeems either at the top of boot.
|
|
462
470
|
|
|
463
|
-
### Mode B —
|
|
471
|
+
### Mode B — self-issued sessions
|
|
464
472
|
|
|
465
473
|
When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link provider as primary; if `providers.google.clientId` is configured it adds Google as an extra provider on the same router.
|
|
466
474
|
|
|
@@ -498,7 +506,7 @@ configurator.setAuth({
|
|
|
498
506
|
});
|
|
499
507
|
```
|
|
500
508
|
|
|
501
|
-
### Server-side helpers
|
|
509
|
+
### Server-side helpers
|
|
502
510
|
|
|
503
511
|
- `verifyToken(token)` — verifies a token against ugly.bot and returns the `userId`.
|
|
504
512
|
- `getRequestUser(req)` — synchronous decode of the per-project `UGLY_PROJECT_TOKEN` cookie set by ugly.bot's wake-on-traffic gate. Returns `{ userId } | null` without a network round-trip; safe to use as the primary auth check in deployed-app handlers.
|
|
@@ -507,7 +515,7 @@ configurator.setAuth({
|
|
|
507
515
|
|
|
508
516
|
## Database — `TypedDB`
|
|
509
517
|
|
|
510
|
-
Access via `app.db
|
|
518
|
+
Access via `app.db`. All methods accept a `CollectionDef` (from `defineCollections()`) or a plain collection name string.
|
|
511
519
|
|
|
512
520
|
### Writing
|
|
513
521
|
|
|
@@ -523,7 +531,7 @@ await db.setDocOp(collections.note, id, { $inc: { views: 1 } }); // Mong
|
|
|
523
531
|
await db.setDocOpOrIgnore(collections.note, id, { $inc: { views: 1 } });
|
|
524
532
|
```
|
|
525
533
|
|
|
526
|
-
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`. All keys are dot-notation, fully typed against the collection's schema.
|
|
534
|
+
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`. All keys are dot-notation, fully typed against the collection's schema. Partial updates go through optimistic concurrency to prevent lost updates on concurrent writers.
|
|
527
535
|
|
|
528
536
|
### Reading
|
|
529
537
|
|
|
@@ -532,8 +540,8 @@ const doc = await db.getDoc(collections.note, id);
|
|
|
532
540
|
const docs = await db.getDocs(collections.note, { userId }, { sort: { created: -1 }, limit: 20 });
|
|
533
541
|
|
|
534
542
|
// Typed SQL-native query API (preferred for new code)
|
|
535
|
-
const notes
|
|
536
|
-
const count
|
|
543
|
+
const notes = await db.find(collections.note, { userId, done: { $ne: true } }, { sort: { created: -1 }, limit: 20 });
|
|
544
|
+
const count = await db.findCount(collections.note, { userId });
|
|
537
545
|
const sample = await db.findRandom(collections.note, { userId }, 5);
|
|
538
546
|
|
|
539
547
|
// Aggregation pipelines (legacy / advanced)
|
|
@@ -578,9 +586,10 @@ const k = db.cacheKey('prefix', id);
|
|
|
578
586
|
|
|
579
587
|
```typescript
|
|
580
588
|
import { createUserHelper, dbDefaults } from 'ugly-app';
|
|
589
|
+
import { nanoid } from 'nanoid';
|
|
581
590
|
|
|
582
|
-
const newDoc = { _id:
|
|
583
|
-
//
|
|
591
|
+
const newDoc = { _id: nanoid(), ...dbDefaults(), title: 'Hi' };
|
|
592
|
+
// ^^^^^^^^^^^^^^^ { version: 1, created, updated }
|
|
584
593
|
|
|
585
594
|
const userHelper = createUserHelper<User>(collections.user);
|
|
586
595
|
const user = await userHelper.get(db, userId);
|
|
@@ -600,7 +609,7 @@ Imports available from `ugly-app`:
|
|
|
600
609
|
|
|
601
610
|
## AI
|
|
602
611
|
|
|
603
|
-
AI calls are proxied through ugly.bot — your app never holds
|
|
612
|
+
AI calls are proxied through ugly.bot — your app never holds a provider key. Pass `UGLY_BOT_TOKEN` in the environment and the framework handles routing, balance tracking, retries, and per-user billing.
|
|
604
613
|
|
|
605
614
|
### Server-side text generation
|
|
606
615
|
|
|
@@ -704,6 +713,8 @@ Client-side, use `socket.uploadFile(file, key)` — it requests a presigned URL
|
|
|
704
713
|
|
|
705
714
|
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment isolation.
|
|
706
715
|
|
|
716
|
+
On Cloudflare Workers, storage uses the R2 binding directly; presigned PUTs are not exposed (browser uploads must go through a Worker endpoint).
|
|
717
|
+
|
|
707
718
|
---
|
|
708
719
|
|
|
709
720
|
## Workers & cron
|
|
@@ -736,6 +747,8 @@ configurator.setWorkers(cronTasks, cronHandlers);
|
|
|
736
747
|
|
|
737
748
|
Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`, `description`. Workers without a schedule are still invocable via `POST /_workers/run` (auth: localhost in dev, `Authorization: Bearer $CRON_SECRET` in prod). Scheduled workers also appear in `/_cron/manifest` for the deploy orchestrator.
|
|
738
749
|
|
|
750
|
+
On the Workers adapter, scheduled workers dispatch through Cloudflare Cron Triggers and durable queueing goes through Cloudflare Queues. On the Node adapter, `enqueueWorker` runs the handler inline (in-process queues only).
|
|
751
|
+
|
|
739
752
|
---
|
|
740
753
|
|
|
741
754
|
## Localization
|
|
@@ -805,17 +818,34 @@ Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for un
|
|
|
805
818
|
| `ugly-app` | Server: `createApp`, `TypedDB`, auth, AI clients, NATS, storage, email, push, workers. |
|
|
806
819
|
| `ugly-app/shared` | Cross-tier: `defineRequests`, `defineCollections`, `definePage`, `defineWorkers`, Zod, experiments, time constants. |
|
|
807
820
|
| `ugly-app/client` | React: `bootstrapApp`, `createRouter`, `lazyPage`, `AppProvider`, components, animations, audio, AI helpers. |
|
|
808
|
-
| `ugly-app/
|
|
821
|
+
| `ugly-app/server/adapter/workers` | Cloudflare Workers entry (Hono router + Durable Objects + Neon HTTP driver + R2). |
|
|
822
|
+
| `ugly-app/conversation/{shared,server,client,engine}` | AI chat sessions with persisted history. |
|
|
809
823
|
| `ugly-app/collab/{server,client}` | Yjs-based collaborative editing. |
|
|
810
824
|
| `ugly-app/markdown/{shared,client}` | Markdown rendering + editor. |
|
|
825
|
+
| `ugly-app/search/{shared,server}` | Search primitives. |
|
|
826
|
+
| `ugly-app/agent/{shared,server,client}` | Agent-mode SSE + tool orchestration. |
|
|
811
827
|
| `ugly-app/webrtc`, `ugly-app/webrtc/server` | WebRTC video rooms. |
|
|
812
828
|
| `ugly-app/three/{server,client}` | Three.js scene helpers. |
|
|
829
|
+
| `ugly-app/native`, `ugly-app/native/server` | Native task host + conformance harness. |
|
|
830
|
+
| `ugly-app/inspect`, `ugly-app/inspect/agent`, `ugly-app/inspect/ui` | UX inspection agent + host bundle. |
|
|
813
831
|
| `ugly-app/worker` | Worker queue runtime. |
|
|
814
|
-
| `ugly-app/playwright` |
|
|
832
|
+
| `ugly-app/playwright` | E2E test utilities (`inspectWindow`, `expectClean`, `setDevice`, …). |
|
|
833
|
+
| `ugly-app/testing` | Test-mode helpers. |
|
|
815
834
|
| `ugly-app/vite`, `ugly-app/eslint` | Build-tool plugins. |
|
|
816
835
|
|
|
817
836
|
---
|
|
818
837
|
|
|
838
|
+
## Two-adapter architecture
|
|
839
|
+
|
|
840
|
+
The same developer source compiles for two runtimes via `src/server/adapter/`:
|
|
841
|
+
|
|
842
|
+
- **Adapter A — Node + TCP** (`src/server/adapter/node/`): default for `npm run dev` and any non-Workers deploy. Wraps `pg.Pool`, `nats.js`, AWS S3 SDK.
|
|
843
|
+
- **Adapter B — Cloudflare Workers** (`src/server/adapter/workers/`): used when the Studio publish flow deploys to Cloudflare. Hono router + Durable Objects (one `CollectionDO` per project-collection, plus a `SessionDO` per user WS) + `@neondatabase/serverless` HTTP driver + R2 binding + Cloudflare Cron Triggers + Cloudflare Queues.
|
|
844
|
+
|
|
845
|
+
Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`, `setCronTasks`, `setWorkers`) don't change between adapters. Local Workers dev boots via `npm run dev:workers`, which starts a small Node HTTP proxy speaking Neon's wire format so the Worker can talk to a real Postgres.
|
|
846
|
+
|
|
847
|
+
---
|
|
848
|
+
|
|
819
849
|
## Environment variables
|
|
820
850
|
|
|
821
851
|
| Variable | Description |
|
|
@@ -831,13 +861,15 @@ Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for un
|
|
|
831
861
|
| `NATS_PREFIX` / `COMPOSE_PROJECT_NAME` | NATS subject prefix for per-env isolation. |
|
|
832
862
|
| `CLOCK_ENABLED` | `true` to enable `setOnMinuteTick` / `setOnHourlyTick`. |
|
|
833
863
|
| `CRON_SECRET` | Bearer secret for `POST /api/_cron/:taskName` and prod `POST /_workers/run`. |
|
|
834
|
-
| `MAINTAIN_BOT_USER_ID` | User id allowed to access admin-only handlers. |
|
|
864
|
+
| `MAINTAIN_BOT_USER_ID` | User id allowed to access admin-only handlers when `setIsAdmin()` is unset. |
|
|
835
865
|
| `INTERNAL_EMAIL_SECRET` | Shared secret for `/internal/email-callback`. |
|
|
836
866
|
| `JWT_SECRET` | Required when using `getRequestUser()` for the per-project session cookie. |
|
|
837
867
|
| `APP_DOMAIN` | App domain; combined with `NATS_PREFIX` for `getRequestUser()` validation. |
|
|
838
868
|
| `LOG_CAPTURE_URL` | Studio override for client log capture (empty → ugly.bot default). |
|
|
839
869
|
| `UGLY_APP_HMR` | Set to `false` to disable Vite HMR in dev. |
|
|
840
870
|
| `SCHEMA_CHECK_SKIP` | `true` to start despite schema drift (unsafe). |
|
|
871
|
+
| `NEON_PROXY_URL` | Set by `dev:workers` — Neon-wire endpoint the local Neon HTTP driver dials. |
|
|
872
|
+
| `CLOUDFLARE_WORKERS` | Set to `1` inside the Workers adapter for runtime detection. |
|
|
841
873
|
|
|
842
874
|
Browser-visible variables must be prefixed `VITE_` and consumed via `import.meta.env.VITE_*`.
|
|
843
875
|
|
|
@@ -864,7 +896,7 @@ Browser-visible variables must be prefixed `VITE_` and consumed via `import.meta
|
|
|
864
896
|
| `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
|
|
865
897
|
| `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
|
|
866
898
|
|
|
867
|
-
Inside a scaffolded project, the same commands are available via `npm run …` scripts — see `templates/CLAUDE.md
|
|
899
|
+
Inside a scaffolded project, the same commands are available via `npm run …` scripts — see `templates/CLAUDE.md`.
|
|
868
900
|
|
|
869
901
|
---
|
|
870
902
|
|
|
@@ -883,4 +915,4 @@ The framework refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=tr
|
|
|
883
915
|
|
|
884
916
|
## Tech stack
|
|
885
917
|
|
|
886
|
-
Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) · Qdrant · NATS · S3-compatible storage · Zod · JWT (jose) · ugly.bot platform
|
|
918
|
+
Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) · Qdrant · NATS · S3-compatible storage · Zod · JWT (jose) · Cloudflare Workers + Durable Objects (Adapter B) · ugly.bot platform
|
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.875";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeviceSimulator.d.ts","sourceRoot":"","sources":["../../../src/client/components/DeviceSimulator.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"DeviceSimulator.d.ts","sourceRoot":"","sources":["../../../src/client/components/DeviceSimulator.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AA6ChF,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,KAAK,CAAC,SAAS,CA0KlB"}
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
|
|
4
|
-
ios: {
|
|
5
|
-
safeArea: { top: 59, right: 0, bottom: 34, left: 0 },
|
|
6
|
-
keyboardHeight: 320,
|
|
7
|
-
viewport: { width: 390, height: 844 },
|
|
8
|
-
},
|
|
9
|
-
android: {
|
|
10
|
-
safeArea: { top: 25, right: 0, bottom: 48, left: 0 },
|
|
11
|
-
keyboardHeight: 300,
|
|
12
|
-
viewport: { width: 360, height: 780 },
|
|
13
|
-
},
|
|
14
|
-
};
|
|
3
|
+
import { SIMULATOR_PROFILES, } from '../deviceProfiles.js';
|
|
15
4
|
function isKeyboardInput(el) {
|
|
16
5
|
if (!el)
|
|
17
6
|
return false;
|