robodev 0.47.3 → 0.49.0

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 (175) hide show
  1. package/package.json +3 -3
  2. package/src/dev-inspect.test.ts +15 -0
  3. package/src/dev-inspect.ts +10 -3
  4. package/src/dev-server.ts +6 -0
  5. package/src/emit-starter.test.ts +6 -3
  6. package/templates/auth-chat/ROBODEV.md +7 -1
  7. package/templates/auth-chat/package.json +2 -2
  8. package/templates/backend/ROBODEV.md +7 -1
  9. package/templates/backend/package.json +2 -2
  10. package/templates/catalog.json +5 -0
  11. package/templates/empty/ROBODEV.md +7 -1
  12. package/templates/empty/package.json +2 -2
  13. package/templates/marketplace/ROBODEV.md +7 -1
  14. package/templates/marketplace/package.json +2 -2
  15. package/templates/povio/.config/local.spa.template.yml +27 -0
  16. package/templates/povio/.oxlint-base.json +29 -0
  17. package/templates/povio/.oxlintrc.json +78 -0
  18. package/templates/povio/.rulesync/rules/frontend-api-boundary.md +36 -0
  19. package/templates/povio/.rulesync/rules/frontend-component-structure.md +40 -0
  20. package/templates/povio/.rulesync/rules/frontend-forms.md +27 -0
  21. package/templates/povio/.rulesync/rules/frontend-notifications.md +24 -0
  22. package/templates/povio/.rulesync/rules/frontend-povio-components.md +42 -0
  23. package/templates/povio/.rulesync/rules/frontend-povio-ui.md +45 -0
  24. package/templates/povio/.rulesync/rules/frontend-query-autocomplete.md +24 -0
  25. package/templates/povio/.rulesync/rules/frontend-tables-and-lists.md +26 -0
  26. package/templates/povio/.rulesync/rules/frontend-translations.md +25 -0
  27. package/templates/povio/.rulesync/rules/project-overview.md +32 -0
  28. package/templates/povio/.rulesync/rules/robodev-api.md +30 -0
  29. package/templates/povio/.rulesync/rules/robodev-auth.md +40 -0
  30. package/templates/povio/.rulesync/rules/robodev-database.md +22 -0
  31. package/templates/povio/.rulesync/rules/role-based-app-structure.md +36 -0
  32. package/templates/povio/.rulesync/skills/media-feature/SKILL.md +21 -0
  33. package/templates/povio/.rulesync/skills/povio-ui-styling/SKILL.md +211 -0
  34. package/templates/povio/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  35. package/templates/povio/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  36. package/templates/povio/.rulesync/skills/robodev-table/SKILL.md +12 -0
  37. package/templates/povio/README.md +40 -0
  38. package/templates/povio/ROBODEV.md +84 -0
  39. package/templates/povio/apps/fe/index.html +24 -0
  40. package/templates/povio/apps/fe/openapi-codegen.config.ts +72 -0
  41. package/templates/povio/apps/fe/package.json +71 -0
  42. package/templates/povio/apps/fe/public/apple-touch-icon.png +0 -0
  43. package/templates/povio/apps/fe/public/favicon-96x96.png +0 -0
  44. package/templates/povio/apps/fe/public/favicon.ico +0 -0
  45. package/templates/povio/apps/fe/public/favicon.svg +3 -0
  46. package/templates/povio/apps/fe/public/site.webmanifest +21 -0
  47. package/templates/povio/apps/fe/public/web-app-manifest-192x192.png +0 -0
  48. package/templates/povio/apps/fe/public/web-app-manifest-512x512.png +0 -0
  49. package/templates/povio/apps/fe/src/assets/locales/en/translation.json +166 -0
  50. package/templates/povio/apps/fe/src/clients/app-rest-client.ts +15 -0
  51. package/templates/povio/apps/fe/src/clients/auth-token-store.ts +101 -0
  52. package/templates/povio/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  53. package/templates/povio/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  54. package/templates/povio/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +45 -0
  55. package/templates/povio/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  56. package/templates/povio/apps/fe/src/components/404.tsx +18 -0
  57. package/templates/povio/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  58. package/templates/povio/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  59. package/templates/povio/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  60. package/templates/povio/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  61. package/templates/povio/apps/fe/src/components/features/home/HomePage.tsx +21 -0
  62. package/templates/povio/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  63. package/templates/povio/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  64. package/templates/povio/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
  65. package/templates/povio/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
  66. package/templates/povio/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  67. package/templates/povio/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  68. package/templates/povio/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  69. package/templates/povio/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  70. package/templates/povio/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  71. package/templates/povio/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  72. package/templates/povio/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  73. package/templates/povio/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  74. package/templates/povio/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  75. package/templates/povio/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  76. package/templates/povio/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  77. package/templates/povio/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  78. package/templates/povio/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  79. package/templates/povio/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  80. package/templates/povio/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  81. package/templates/povio/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  82. package/templates/povio/apps/fe/src/config/app.config.ts +23 -0
  83. package/templates/povio/apps/fe/src/config/i18n.ts +37 -0
  84. package/templates/povio/apps/fe/src/config/inits/logger.ts +7 -0
  85. package/templates/povio/apps/fe/src/config/jwt.config.ts +2 -0
  86. package/templates/povio/apps/fe/src/config/query.config.ts +20 -0
  87. package/templates/povio/apps/fe/src/hooks/useAuth.ts +5 -0
  88. package/templates/povio/apps/fe/src/main.tsx +52 -0
  89. package/templates/povio/apps/fe/src/pages/(guest)/login.tsx +23 -0
  90. package/templates/povio/apps/fe/src/pages/(guest)/register.tsx +23 -0
  91. package/templates/povio/apps/fe/src/pages/(guest)/route.tsx +18 -0
  92. package/templates/povio/apps/fe/src/pages/(private)/index.tsx +23 -0
  93. package/templates/povio/apps/fe/src/pages/(private)/profile.tsx +23 -0
  94. package/templates/povio/apps/fe/src/pages/(private)/route.tsx +18 -0
  95. package/templates/povio/apps/fe/src/pages/(public)/auth.tsx +37 -0
  96. package/templates/povio/apps/fe/src/pages/(public)/route.tsx +9 -0
  97. package/templates/povio/apps/fe/src/pages/__root.tsx +133 -0
  98. package/templates/povio/apps/fe/src/providers/AppErrorBoundary.tsx +44 -0
  99. package/templates/povio/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  100. package/templates/povio/apps/fe/src/providers/index.tsx +44 -0
  101. package/templates/povio/apps/fe/src/providers/jwt.provider.tsx +95 -0
  102. package/templates/povio/apps/fe/src/routeTree.gen.ts +225 -0
  103. package/templates/povio/apps/fe/src/styles/base.css +84 -0
  104. package/templates/povio/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  105. package/templates/povio/apps/fe/src/styles/globals.css +26 -0
  106. package/templates/povio/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  107. package/templates/povio/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  108. package/templates/povio/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  109. package/templates/povio/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  110. package/templates/povio/apps/fe/src/styles/overrides/defaults/modal.override.ts +62 -0
  111. package/templates/povio/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  112. package/templates/povio/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  113. package/templates/povio/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  114. package/templates/povio/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  115. package/templates/povio/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  116. package/templates/povio/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  117. package/templates/povio/apps/fe/src/styles/theme.css +2177 -0
  118. package/templates/povio/apps/fe/src/types/i18next.d.ts +12 -0
  119. package/templates/povio/apps/fe/src/types/table.d.ts +17 -0
  120. package/templates/povio/apps/fe/src/types/ui.d.ts +26 -0
  121. package/templates/povio/apps/fe/src/types/vite-env.d.ts +13 -0
  122. package/templates/povio/apps/fe/src/utils/date.utils.ts +17 -0
  123. package/templates/povio/apps/fe/src/utils/image-fallback.ts +9 -0
  124. package/templates/povio/apps/fe/src/utils/number.utils.ts +12 -0
  125. package/templates/povio/apps/fe/src/utils/string.utils.ts +5 -0
  126. package/templates/povio/apps/fe/src/vite-env.d.ts +1 -0
  127. package/templates/povio/apps/fe/tsconfig.app.json +32 -0
  128. package/templates/povio/apps/fe/tsconfig.json +9 -0
  129. package/templates/povio/apps/fe/tsconfig.node.json +31 -0
  130. package/templates/povio/apps/fe/vite.config.ts +150 -0
  131. package/templates/povio/openapi.json +911 -0
  132. package/templates/povio/oxfmt.config.js +23 -0
  133. package/templates/povio/package.json +19 -0
  134. package/templates/povio/robodev/api/_lib.ts +99 -0
  135. package/templates/povio/robodev/api/health.ts +7 -0
  136. package/templates/povio/robodev/database.ts +28 -0
  137. package/templates/povio/robodev/package.json +4 -0
  138. package/templates/povio/rulesync.jsonc +18 -0
  139. package/templates/povio/tsconfig.json +11 -0
  140. package/templates/shadcn/.rulesync/rules/frontend-api-boundary.md +29 -0
  141. package/templates/shadcn/.rulesync/rules/frontend-component-structure.md +19 -0
  142. package/templates/shadcn/.rulesync/rules/frontend-hosted-ui.md +38 -0
  143. package/templates/shadcn/.rulesync/rules/project-overview.md +29 -0
  144. package/templates/shadcn/.rulesync/rules/robodev-api.md +22 -0
  145. package/templates/shadcn/.rulesync/rules/robodev-auth.md +47 -0
  146. package/templates/shadcn/.rulesync/rules/robodev-database.md +21 -0
  147. package/templates/shadcn/.rulesync/skills/robodev-crud-page/SKILL.md +181 -0
  148. package/templates/shadcn/README.md +9 -4
  149. package/templates/shadcn/ROBODEV.md +14 -6
  150. package/templates/shadcn/package.json +4 -6
  151. package/templates/shadcn/rulesync.jsonc +18 -0
  152. package/templates/shadcn/src/App.tsx +12 -3
  153. package/templates/shadcn/src/app.css +359 -0
  154. package/templates/shadcn/src/components/auth/AuthCard.tsx +29 -0
  155. package/templates/shadcn/src/components/ui/badge.tsx +17 -0
  156. package/templates/shadcn/src/components/ui/card.tsx +28 -0
  157. package/templates/shadcn/src/components/ui/dialog.tsx +38 -0
  158. package/templates/shadcn/src/components/ui/empty-state.tsx +17 -0
  159. package/templates/shadcn/src/components/ui/input.tsx +8 -0
  160. package/templates/shadcn/src/components/ui/label.tsx +8 -0
  161. package/templates/shadcn/src/components/ui/skeleton.tsx +6 -0
  162. package/templates/shadcn/src/components/ui/spinner.tsx +3 -0
  163. package/templates/shadcn/src/components/ui/table.tsx +30 -0
  164. package/templates/shadcn/src/components/ui/textarea.tsx +8 -0
  165. package/templates/shadcn/src/lib/auth.tsx +72 -0
  166. package/templates/shadcn/src/lib/client.ts +8 -0
  167. package/templates/shadcn/src/pages/Home.tsx +15 -0
  168. package/templates/shadcn/src/pages/Login.tsx +70 -0
  169. package/templates/shadcn/src/pages/NotFound.tsx +14 -0
  170. package/templates/shadcn/src/pages/Register.tsx +82 -0
  171. package/templates/space/ROBODEV.md +7 -1
  172. package/templates/space/package.json +2 -2
  173. package/templates/shadcn/components.json +0 -21
  174. package/templates/shadcn/postcss.config.js +0 -6
  175. package/templates/shadcn/tailwind.config.ts +0 -42
@@ -0,0 +1,84 @@
1
+ # Robodev
2
+
3
+ Use this file for Robodev and SDK facts. Do not web-search the SDK.
4
+
5
+ ## defineApi (copy-paste)
6
+
7
+ `db`, `email`, `storage`, and `env` are injected. Never `import { db } from "@robodev-ai/sdk"`.
8
+ The SDK is not installed in the runner. Do not `import("@robodev-ai/sdk")`. Do not search `/opt`, `node_modules`, or grok/gemini home for it.
9
+
10
+ ```ts
11
+ import { defineApi, z } from "@robodev-ai/sdk";
12
+
13
+ export const get = defineApi({
14
+ response: z.object({ ok: z.literal(true) }),
15
+ handler: async ({ db, email, storage, env }) => {
16
+ void db;
17
+ void email;
18
+ void storage;
19
+ void env;
20
+ return { ok: true as const };
21
+ },
22
+ });
23
+ ```
24
+
25
+ Method exports: one export per method — `get` / `post` / `put` / `patch` / `delete`. `delete` is a reserved word — write `export const del = ...` or `export { remove as delete }`; both serve DELETE.
26
+
27
+ `ctx.user.id` is a string like `usr_abc123`, not a UUID. Type user id columns as `text`, never `uuid`.
28
+
29
+ Throw an Error with an integer `statusCode` (400-599) to answer with that status: `throw Object.assign(new Error("Not found"), { statusCode: 404 })`. Any other throw is a generic 500.
30
+
31
+ ## Hard rules
32
+
33
+ - Do not run npm, pnpm, or yarn install.
34
+ - Do not start npm run dev, vite, or any local server.
35
+ - Do not run tsc (node_modules is not installed in this runner).
36
+ - Do not port-scan or curl/fetch localhost.
37
+ - Preview is the WebContainer. Live APIs exist only after `robodev-publish be`.
38
+ - Publish backend after a batch of file edits, not after every file.
39
+ - Do not publish the frontend.
40
+
41
+ ## Auth and product data
42
+
43
+ Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny and Shadcn: reuse seed login/register and AuthGuard. Empty and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
44
+
45
+ ## Backend
46
+
47
+ - Auth: robodev-auth get / set / users / users-delete. Do not invent control-plane HTTP.
48
+ - Secrets: robodev-secret list / set / delete. Use env.NAME in defineApi. List names only.
49
+ - SQL: psql or robodev-sql for DML, SELECT, and seed only. Never print DATABASE_URL. Never run DDL.
50
+ - Publish backend only with robodev-publish be. Do not publish the frontend.
51
+ - Email settings: robodev-email get / set / validate / logs.
52
+
53
+ ## Email templates
54
+
55
+ Hosted mail is the Email page. Changes apply immediately through these helpers — no Code save or deploy.
56
+
57
+ - `robodev-email templates list`
58
+ - `robodev-email templates get --slug <slug>`
59
+ - `robodev-email templates set --slug <slug> [--subject] [--html] [--text]`
60
+
61
+ `set` edits an existing slug. If the slug is missing and is a custom slug (`^[a-z][a-z0-9-]{0,62}$`, not `auth.*`), `set` creates it.
62
+
63
+ System slugs: `auth.welcome` (`{{email}}`, `{{name}}`), `auth.magic-link` (`{{link}}`, `{{email}}`, `{{name}}`, `{{appUrl}}`), `auth.forgot-password` (`{{link}}`, `{{code}}`, `{{email}}`, `{{name}}`, `{{appUrl}}`). Keep placeholders exact. Do not delete system templates. Do not invent `auth.*` slugs.
64
+
65
+ Send with `ctx.email.send({ template: "<slug>", to, vars })`.
66
+
67
+ Do not write `robodev/emails/*.html` or `robodev/emails/*.json` for hosted templates. Those files do not update the live project.
68
+
69
+ Never write `.env` or secrets. Never write `.robodev`.
70
+
71
+ ## Layout
72
+
73
+ CLI Tiny-family: `robodev/database.ts` + `robodev/api/**`, Povio UI in `apps/fe`. Keep that layout. This is a blank seed — no sample features.
74
+
75
+ - Schema: `robodev/database.ts` (`defineDatabase` from `@robodev-ai/sdk`; placeholder `items` table + `media`)
76
+ - HTTP handlers: `robodev/api/**/*.ts` (`GET /api/health` is seeded; shared helpers in `robodev/api/_lib.ts`)
77
+ - Optional jobs / sockets / hooks under `robodev/`
78
+ - Frontend: `apps/fe` (real `@povio/ui` kit, TanStack Router + Query, generated `@/openapi` client). Seeded: login/register (`(guest)`), Home and profile (`(private)`). Reuse the seeded auth — never rebuild token handling.
79
+ - After schema or API changes, update root `openapi.json` and regenerate the client (`cd apps/fe && bun openapi:gen`), then `robodev-publish be`.
80
+ - Add new pages under `apps/fe/src/pages/(private)/` and link them in `AppHeader.tsx` / `MobileNavigation.tsx`.
81
+ - Auth is reserved at `/api/user/*`. Do not add `robodev/api/user/**`.
82
+ - AI rules live in `.rulesync/` — read `.rulesync/rules/project-overview.md` first.
83
+
84
+ Allowed paths: `robodev/database.ts`, `robodev/api/**/*.ts`, `robodev/jobs/**/*.ts`, `robodev/sockets/**/*.ts`, `robodev/hooks/**/*.ts`, `robodev/package.json`, `apps/fe/**`, `.rulesync/**`, `openapi.json`, `package.json`, `README.md`, `ROBODEV.md`, `.gitignore`.
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link
6
+ rel="icon"
7
+ type="image/svg+xml"
8
+ href="/favicon.svg"
9
+ />
10
+ <meta
11
+ name="viewport"
12
+ content="width=device-width, initial-scale=1.0"
13
+ />
14
+ <title>Povio UI</title>
15
+ </head>
16
+
17
+ <body class="bg-elevation-fill-default-2 text-text-default-1">
18
+ <div id="app"></div>
19
+ <script
20
+ type="module"
21
+ src="/src/main.tsx"
22
+ ></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,72 @@
1
+ /* oxlint-disable import/no-nodejs-modules */
2
+ import type { OpenAPICodegenConfig } from "@povio/openapi-codegen-cli";
3
+ import { resolveConfigSync } from "@povio/resolve-config";
4
+ import { readFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+
7
+ const COMPACT_OPENAPI_INPUT = "../../openapi.json";
8
+ const repoRoot = resolve(process.cwd(), "../..");
9
+
10
+ function readDotEnvKey(root: string, key: string): string | undefined {
11
+ try {
12
+ const content = readFileSync(join(root, ".env"), "utf8");
13
+ const line = content.split(/\r?\n/).find((row) => row.startsWith(`${key}=`));
14
+ const value = line?.slice(key.length + 1).trim();
15
+ return value || undefined;
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ function loadResolvedSpaEnv(cwd: string): Record<string, string | undefined> {
22
+ try {
23
+ const resolved = resolveConfigSync({ cwd, module: "spa", target: "resolved" });
24
+ if (resolved && typeof resolved === "object") {
25
+ return resolved as Record<string, string | undefined>;
26
+ }
27
+ } catch {
28
+ // unnamed or missing spa manifest
29
+ }
30
+ return {};
31
+ }
32
+
33
+ const env = loadResolvedSpaEnv(repoRoot);
34
+
35
+ function fallbackApiUrl(): string {
36
+ return (
37
+ env.VITE_PUBLIC_API_URL ||
38
+ process.env.VITE_PUBLIC_API_URL ||
39
+ readDotEnvKey(repoRoot, "VITE_PUBLIC_API_URL") ||
40
+ "http://localhost:4000"
41
+ );
42
+ }
43
+
44
+ function resolveLiveOpenApiInput(apiUrl = fallbackApiUrl()) {
45
+ const usable = apiUrl.trim();
46
+ if (!usable) return COMPACT_OPENAPI_INPUT;
47
+ return `${usable.replace(/\/+$/, "")}/openapi.json`;
48
+ }
49
+
50
+ export function resolveOpenApiInput() {
51
+ if (process.env.OPENAPI_INPUT) return process.env.OPENAPI_INPUT;
52
+ if (process.env.OPENAPI_LIVE === "true") return resolveLiveOpenApiInput();
53
+ return COMPACT_OPENAPI_INPUT;
54
+ }
55
+
56
+ const config: OpenAPICodegenConfig = {
57
+ input: resolveOpenApiInput(),
58
+ output: "src/openapi",
59
+ tsPath: "@/openapi",
60
+ restClientImportPath: "@/clients/app-rest-client",
61
+ clearOutput: true,
62
+ excludeRedundantZodSchemas: false,
63
+ checkAcl: false,
64
+ acl: false,
65
+ replaceOptionalWithNullish: true,
66
+ infiniteQueries: true,
67
+ mutationDefaultOnError: true,
68
+ modelsInCommon: true,
69
+ axiosRequestConfig: true,
70
+ };
71
+
72
+ export default config;
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "fe",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "Povio UI starter frontend",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "vite --host",
9
+ "build": "bun run openapi:gen && bunx --bun vite build",
10
+ "clean": "rimraf dist src/openapi",
11
+ "lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
12
+ "lint:check": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --quiet",
13
+ "format": "oxfmt -c \"../../oxfmt.config.js\" . ../../robodev/api ../../robodev/database.ts",
14
+ "format:check": "oxfmt -c \"../../oxfmt.config.js\" --check . ../../robodev/api ../../robodev/database.ts",
15
+ "ts:check": "tsc -b --noEmit",
16
+ "test": "vitest run --passWithNoTests",
17
+ "check": "bun ts:check && bun lint:check && bun format:check",
18
+ "openapi:gen": "openapi-codegen generate && oxfmt -c \"../../oxfmt.config.js\" ./src/openapi",
19
+ "openapi:check": "openapi-codegen check"
20
+ },
21
+ "dependencies": {
22
+ "@casl/ability": "^6.8.1",
23
+ "@casl/react": "^5.0.1",
24
+ "@fontsource-variable/inter": "^5.2.8",
25
+ "@hookform/resolvers": "^5.2.2",
26
+ "@internationalized/date": "^3.11.0",
27
+ "@povio/openapi-codegen-cli": "3.1.0",
28
+ "@povio/resolve-config": "1.3.0",
29
+ "@povio/ui": "3.4.0-rc.12",
30
+ "@tanstack/react-form": "^1.33.2",
31
+ "@tanstack/react-query": "^5.90.21",
32
+ "@tanstack/react-router": "^1.136.8",
33
+ "@tanstack/react-table": "^8.21.3",
34
+ "@tanstack/router-plugin": "^1.136.8",
35
+ "axios": "^1.13.2",
36
+ "class-variance-authority": "^0.7.1",
37
+ "clsx": "^2.1.1",
38
+ "i18next": "^25.7.3",
39
+ "loglevel": "^1.9.2",
40
+ "lucide-react": "^1.28.0",
41
+ "luxon": "^3.7.2",
42
+ "react": "19.2.4",
43
+ "react-aria": "^3.46.0",
44
+ "react-aria-components": "^1.15.1",
45
+ "react-dom": "19.2.4",
46
+ "react-hook-form": "^7.65.0",
47
+ "react-i18next": "^16.5.1",
48
+ "zod": "^4.4.3"
49
+ },
50
+ "devDependencies": {
51
+ "@tailwindcss/vite": "^4.2.1",
52
+ "@types/luxon": "^3.7.1",
53
+ "@types/node": "^24.10.9",
54
+ "@types/react": "^19.2.7",
55
+ "@types/react-dom": "^19.2.3",
56
+ "@typescript/native": "npm:typescript@^7.0.2",
57
+ "@vitejs/plugin-react": "^5.1.2",
58
+ "babel-plugin-react-compiler": "1.0.0",
59
+ "oxfmt": "^0.45.0",
60
+ "oxlint": "1.60.0",
61
+ "oxlint-tsgolint": "0.21.0",
62
+ "react-icons-sprite": "^1.1.1",
63
+ "rimraf": "^6.1.2",
64
+ "tailwindcss": "^4.2.4",
65
+ "tailwindcss-react-aria-components": "^2.1.0",
66
+ "tsx": "^4.22.1",
67
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
68
+ "vite": "^8.0.0",
69
+ "vitest": "^3.2.4"
70
+ }
71
+ }
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="239.36" height="239.36"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="239.36" height="239.36"><svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 239.36 239.36"><defs><style>.cls-1{fill:#3c60ad;}.cls-2{fill:none;}.cls-3{clip-path:url(#clip-path);}.cls-4{mask:url(#mask);}.cls-5{fill:#fff;}</style><clipPath id="clip-path"><rect class="cls-2" width="239.36" height="239.36"></rect></clipPath><mask id="mask" x="0" y="0" width="239.36" height="239.36" maskUnits="userSpaceOnUse"><g id="mask0_21778_2258" data-name="mask0 21778 2258"><path class="cls-1" d="M0,29.92A29.93,29.93,0,0,1,29.92,0H209.44a29.93,29.93,0,0,1,29.92,29.92V209.44a29.93,29.93,0,0,1-29.92,29.92H29.92A29.93,29.93,0,0,1,0,209.44Z"></path></g></mask></defs><g class="cls-3"><g class="cls-4"><path class="cls-5" d="M0,29.92A29.93,29.93,0,0,1,29.92,0H209.44a29.93,29.93,0,0,1,29.92,29.92V209.44a29.93,29.93,0,0,1-29.92,29.92H29.92A29.93,29.93,0,0,1,0,209.44Z"></path><path d="M119.8,49.55c26.68,0,48.41,22.8,48.5,50.63,0,28.18-21.73,51.06-48.5,51.06A46.64,46.64,0,0,1,83.31,133.4v56.54h-12V100.27C71.29,72.35,93,49.55,119.8,49.55Zm0,12.29c-20.15,0-36.49,17.22-36.49,38.43,0,21.46,16.34,39,36.49,39s36.39-17.5,36.48-39C156.28,79.06,139.94,61.84,119.8,61.84Z"></path></g></g></svg></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
2
+ @media (prefers-color-scheme: dark) { :root { filter: none; } }
3
+ </style></svg>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "Povio",
3
+ "short_name": "Povio",
4
+ "icons": [
5
+ {
6
+ "src": "/web-app-manifest-192x192.png",
7
+ "sizes": "192x192",
8
+ "type": "image/png",
9
+ "purpose": "maskable"
10
+ },
11
+ {
12
+ "src": "/web-app-manifest-512x512.png",
13
+ "sizes": "512x512",
14
+ "type": "image/png",
15
+ "purpose": "maskable"
16
+ }
17
+ ],
18
+ "theme_color": "#ffffff",
19
+ "background_color": "#ffffff",
20
+ "display": "standalone"
21
+ }
@@ -0,0 +1,166 @@
1
+ {
2
+ "appName": "Tiny Template",
3
+ "appDescription": "A template for building web applications",
4
+ "shared": {
5
+ "notFound": {
6
+ "head": {
7
+ "title": "Page not found",
8
+ "description": "The page you are looking for doesn't exist."
9
+ },
10
+ "heading": "Page not found",
11
+ "subheading": "The page you are looking for doesn't exist.",
12
+ "homeBtn": "Go to home"
13
+ },
14
+ "errorFallback": {
15
+ "heading": "Something went wrong",
16
+ "subheading": "Error: {{error}}",
17
+ "retryBtn": "Try again"
18
+ },
19
+ "apiErrors": {
20
+ "requestFailed": "Request failed: {{error}}",
21
+ "unknown": "An unknown error occurred."
22
+ },
23
+ "seedData": {
24
+ "actionButton": "Reset seed data",
25
+ "confirmation": {
26
+ "heading": "Reset seed data?",
27
+ "description": "This will reset all local data to the initial seed state.",
28
+ "confirm": "Reset",
29
+ "cancel": "Cancel"
30
+ }
31
+ },
32
+ "actions": {
33
+ "back": "Back"
34
+ }
35
+ },
36
+ "auth": {
37
+ "wrapper": {
38
+ "title": "Administration portal",
39
+ "description": "Tiny",
40
+ "watermark": "© {{year}} Tiny • Developed and maintained by Povio"
41
+ },
42
+ "shared": {
43
+ "errors": {
44
+ "invalidCredentials": "Invalid email or password.",
45
+ "userAlreadyExists": "An account with this email already exists.",
46
+ "identityNotFound": "We could not find this account.",
47
+ "notConfirmed": "This account has not been confirmed yet."
48
+ }
49
+ },
50
+ "login": {
51
+ "head": {
52
+ "title": "Login",
53
+ "description": "Login to your account"
54
+ },
55
+ "form": {
56
+ "title": "Login",
57
+ "email": "Email",
58
+ "emailPlaceholder": "Enter your email",
59
+ "password": "Password",
60
+ "passwordPlaceholder": "Enter your password",
61
+ "submit": "Login",
62
+ "submitLoading": "Logging in...",
63
+ "demoLogin": "Login as demo user",
64
+ "noAccount": "Don't have an account?",
65
+ "registerLink": "Register",
66
+ "loginFailed": "Login failed",
67
+ "resetSeedData": "Reset seed data"
68
+ },
69
+ "confirmations": {
70
+ "resetSeed": {
71
+ "heading": "Reset seed data?",
72
+ "description": "This will reset all local data to the initial seed state.",
73
+ "confirm": "Reset",
74
+ "cancel": "Cancel"
75
+ }
76
+ }
77
+ },
78
+ "register": {
79
+ "head": {
80
+ "title": "Register",
81
+ "description": "Create a new account"
82
+ },
83
+ "form": {
84
+ "title": "Register",
85
+ "description": "Create a new account",
86
+ "email": "Email",
87
+ "emailPlaceholder": "Enter your email",
88
+ "name": "Name",
89
+ "namePlaceholder": "Enter your name",
90
+ "password": "Password",
91
+ "passwordPlaceholder": "Enter your password",
92
+ "confirmPassword": "Confirm password",
93
+ "confirmPasswordPlaceholder": "Confirm your password",
94
+ "confirmPasswordRequired": "Confirm password is required",
95
+ "passwordsDoNotMatch": "Passwords do not match",
96
+ "passwordRequirements": "Password must be at least 12 characters long.",
97
+ "submit": "Register",
98
+ "registrationFailed": "Registration failed",
99
+ "hasAccount": "Already have an account?",
100
+ "loginLink": "Login"
101
+ }
102
+ },
103
+ "setPassword": {
104
+ "form": {
105
+ "title": "Set your password",
106
+ "welcome": "Welcome, {{firstName}} {{lastName}}. Please set your password to complete your account.",
107
+ "tokenNote": "Enter the invitation token you received by email.",
108
+ "token": "Invitation token",
109
+ "tokenPlaceholder": "Enter your invitation token",
110
+ "tokenChecking": "Checking token...",
111
+ "tokenError": "{{error}}",
112
+ "invalidToken": "Invalid or expired token",
113
+ "email": "Email",
114
+ "password": "Password",
115
+ "passwordPlaceholder": "Enter your password",
116
+ "confirmPassword": "Confirm password",
117
+ "confirmPasswordPlaceholder": "Confirm your password",
118
+ "passwordRequirements": "Password must be at least 8 characters long.",
119
+ "submit": "Set password",
120
+ "setPasswordFailed": "Setting password failed",
121
+ "hasPassword": "Already have a password?",
122
+ "loginLink": "Login"
123
+ }
124
+ }
125
+ },
126
+ "layout": {
127
+ "header": {
128
+ "logo": "Tiny",
129
+ "openMenu": "Open menu",
130
+ "closeMenu": "Close menu",
131
+ "nav": {
132
+ "home": "Home"
133
+ },
134
+ "profile": "Profile",
135
+ "logout": "Logout"
136
+ }
137
+ },
138
+ "home": {
139
+ "head": {
140
+ "title": "Home",
141
+ "description": "Welcome to the app!"
142
+ },
143
+ "title": "Home",
144
+ "message": "Welcome to the app!"
145
+ },
146
+ "profile": {
147
+ "head": {
148
+ "title": "Profile",
149
+ "description": "Your profile"
150
+ },
151
+ "form": {
152
+ "title": "Profile",
153
+ "description": "Update your profile details",
154
+ "email": "Email",
155
+ "emailPlaceholder": "Enter your email",
156
+ "name": "Name",
157
+ "namePlaceholder": "Enter your name",
158
+ "submit": "Save",
159
+ "submitLoading": "Saving...",
160
+ "notifications": {
161
+ "success": "Profile updated",
162
+ "error": "Failed to update profile"
163
+ }
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,15 @@
1
+ import { RestClient } from "@povio/openapi-codegen-cli";
2
+
3
+ import { AuthorizationHeaderInterceptor } from "@/clients/rest/interceptors/authorization-header.interceptor";
4
+ import { RefreshTokenInterceptor } from "@/clients/rest/interceptors/refresh-token.interceptor";
5
+ import { ResponseInterceptor } from "@/clients/rest/interceptors/response.interceptor";
6
+ import { AppConfig } from "@/config/app.config";
7
+ import { AppErrorHandler } from "@/clients/rest/app-error-handler";
8
+
9
+ export const AppRestClient = new RestClient({
10
+ config: {
11
+ baseURL: AppConfig.api.url,
12
+ },
13
+ errorHandler: AppErrorHandler,
14
+ interceptors: [AuthorizationHeaderInterceptor, RefreshTokenInterceptor, ResponseInterceptor],
15
+ });
@@ -0,0 +1,101 @@
1
+ import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/config/jwt.config";
2
+
3
+ type TokenListener = () => void;
4
+
5
+ const listeners = new Set<TokenListener>();
6
+ let storageListenerCount = 0;
7
+
8
+ const isBrowser = () => typeof window !== "undefined";
9
+
10
+ const readToken = (key: string) => {
11
+ if (!isBrowser()) {
12
+ return null;
13
+ }
14
+
15
+ try {
16
+ return window.localStorage.getItem(key);
17
+ } catch {
18
+ return null;
19
+ }
20
+ };
21
+
22
+ const writeToken = (key: string, token: string | null) => {
23
+ if (!isBrowser()) {
24
+ return;
25
+ }
26
+
27
+ try {
28
+ if (token) {
29
+ window.localStorage.setItem(key, token);
30
+ } else {
31
+ window.localStorage.removeItem(key);
32
+ }
33
+ } catch {
34
+ // Keep in-memory auth subscribers in sync even if storage is unavailable.
35
+ }
36
+ };
37
+
38
+ const emitChange = () => {
39
+ for (const listener of listeners) {
40
+ listener();
41
+ }
42
+ };
43
+
44
+ const onStorageChange = (event: StorageEvent) => {
45
+ if (event.storageArea !== window.localStorage) {
46
+ return;
47
+ }
48
+
49
+ if (event.key === ACCESS_TOKEN_KEY || event.key === REFRESH_TOKEN_KEY || event.key === null) {
50
+ emitChange();
51
+ }
52
+ };
53
+
54
+ export const authTokenStore = {
55
+ getAccessToken: () => readToken(ACCESS_TOKEN_KEY),
56
+ getRefreshToken: () => readToken(REFRESH_TOKEN_KEY),
57
+ getServerSnapshot: () => null,
58
+ subscribe: (listener: TokenListener) => {
59
+ listeners.add(listener);
60
+
61
+ if (isBrowser()) {
62
+ if (storageListenerCount === 0) {
63
+ window.addEventListener("storage", onStorageChange);
64
+ }
65
+ storageListenerCount += 1;
66
+ }
67
+
68
+ return () => {
69
+ listeners.delete(listener);
70
+
71
+ if (isBrowser()) {
72
+ storageListenerCount -= 1;
73
+ if (storageListenerCount === 0) {
74
+ window.removeEventListener("storage", onStorageChange);
75
+ }
76
+ }
77
+ };
78
+ },
79
+ setAccessToken: (token: string | null) => {
80
+ writeToken(ACCESS_TOKEN_KEY, token);
81
+ emitChange();
82
+ },
83
+ setRefreshToken: (token: string | null) => {
84
+ writeToken(REFRESH_TOKEN_KEY, token);
85
+ emitChange();
86
+ },
87
+ setTokens: (accessToken: string | null, refreshToken?: string | null) => {
88
+ writeToken(ACCESS_TOKEN_KEY, accessToken);
89
+
90
+ if (refreshToken !== undefined) {
91
+ writeToken(REFRESH_TOKEN_KEY, refreshToken);
92
+ }
93
+
94
+ emitChange();
95
+ },
96
+ clear: () => {
97
+ writeToken(ACCESS_TOKEN_KEY, null);
98
+ writeToken(REFRESH_TOKEN_KEY, null);
99
+ emitChange();
100
+ },
101
+ };
@@ -0,0 +1,31 @@
1
+ import { type ErrorEntry, ErrorHandler } from "@povio/openapi-codegen-cli";
2
+
3
+ export enum AuthErrorCodes {
4
+ InvalidCredentials = "invalid-credentials",
5
+ LocalAuthnIdentityExists = "identity-already-exists",
6
+ IdentityNotFound = "identity-not-found",
7
+ NotConfirmed = "identity-not-confirmed",
8
+ }
9
+
10
+ const authErrors: ErrorEntry<AuthErrorCodes>[] = [
11
+ {
12
+ code: AuthErrorCodes.InvalidCredentials,
13
+ getMessage: (t) => t("auth.shared.errors.invalidCredentials"),
14
+ },
15
+ {
16
+ code: AuthErrorCodes.LocalAuthnIdentityExists,
17
+ getMessage: (t) => t("auth.shared.errors.userAlreadyExists"),
18
+ },
19
+ {
20
+ code: AuthErrorCodes.IdentityNotFound,
21
+ getMessage: (t) => t("auth.shared.errors.identityNotFound"),
22
+ },
23
+ {
24
+ code: AuthErrorCodes.NotConfirmed,
25
+ getMessage: (t) => t("auth.shared.errors.notConfirmed"),
26
+ },
27
+ ];
28
+
29
+ const entries = [...authErrors];
30
+
31
+ export const AppErrorHandler = new ErrorHandler({ entries });
@@ -0,0 +1,15 @@
1
+ import { RestInterceptor } from "@povio/openapi-codegen-cli";
2
+
3
+ import { authTokenStore } from "@/clients/auth-token-store";
4
+
5
+ export const AuthorizationHeaderInterceptor = new RestInterceptor((client) => {
6
+ return client.interceptors.request.use(async (config) => {
7
+ const accessToken = authTokenStore.getAccessToken();
8
+ if (accessToken != null) {
9
+ // oxlint-disable-next-line no-param-reassign
10
+ config.headers.Authorization = `Bearer ${accessToken}`;
11
+ }
12
+
13
+ return config;
14
+ });
15
+ });
@@ -0,0 +1,45 @@
1
+ import { RestInterceptor } from "@povio/openapi-codegen-cli";
2
+
3
+ import { AuthErrorCodes } from "@/clients/rest/app-error-handler";
4
+ import { UserAuthApi } from "@/openapi/userAuth/userAuth.api";
5
+ import { authTokenStore } from "@/clients/auth-token-store";
6
+
7
+ const REFRESH_PATH = "/api/user/auth/refresh";
8
+
9
+ export const RefreshTokenInterceptor = new RestInterceptor((client) => {
10
+ return client.interceptors.response.use(
11
+ (response) => response,
12
+ // oxlint-disable-next-line promise/prefer-await-to-callbacks
13
+ async (error) => {
14
+ const originalRequest = error.config;
15
+
16
+ if (error.response?.data?.code === AuthErrorCodes.InvalidCredentials) {
17
+ return Promise.reject(error);
18
+ }
19
+
20
+ // The refresh call itself must not trigger another refresh: a 401 from it would otherwise
21
+ // re-enter this interceptor and loop until the tab is closed.
22
+ const isRefreshCall = typeof originalRequest?.url === "string" && originalRequest.url.includes(REFRESH_PATH);
23
+
24
+ if (error.response?.status === 401 && !originalRequest._retry && !isRefreshCall) {
25
+ originalRequest._retry = true;
26
+ const refreshToken = authTokenStore.getRefreshToken();
27
+ if (!refreshToken) {
28
+ authTokenStore.clear();
29
+ return Promise.reject(error);
30
+ }
31
+ try {
32
+ const { accessToken, refreshToken: newRefreshToken } = await UserAuthApi.accessToken({ refreshToken });
33
+ authTokenStore.setTokens(accessToken, newRefreshToken || null);
34
+
35
+ originalRequest.headers.Authorization = `Bearer ${accessToken}`;
36
+
37
+ return client(originalRequest);
38
+ } catch {
39
+ authTokenStore.clear();
40
+ }
41
+ }
42
+ return Promise.reject(error);
43
+ },
44
+ );
45
+ });