@aurostack/stacks 0.1.1 → 0.2.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.
Files changed (112) hide show
  1. package/README.md +59 -0
  2. package/cli/src/generate.mjs +13 -1
  3. package/cli/stack.mjs +1 -1
  4. package/package.json +1 -1
  5. package/templates/nest-api/files/.bin/secrets.ts +15 -6
  6. package/templates/nest-api/files/.env.example +29 -2
  7. package/templates/nest-api/files/.env.test.example +1 -1
  8. package/templates/nest-api/files/_gitignore +1 -0
  9. package/templates/nest-api/files/compose.yml +16 -0
  10. package/templates/nest-api/files/package.json +18 -3
  11. package/templates/nest-api/files/src/app.module.ts +9 -1
  12. package/templates/nest-api/files/src/app.setup.ts +8 -7
  13. package/templates/nest-api/files/src/common/__tests__/temporal.service.spec.ts +66 -0
  14. package/templates/nest-api/files/src/common/common.module.ts +4 -3
  15. package/templates/nest-api/files/src/common/constants/pino.ts +14 -0
  16. package/templates/nest-api/files/src/common/controllers/index.ts +0 -1
  17. package/templates/nest-api/files/src/common/modules/index.ts +0 -1
  18. package/templates/nest-api/files/src/common/modules/logger.module.ts +5 -8
  19. package/templates/nest-api/files/src/common/services/config.service.ts +43 -5
  20. package/templates/nest-api/files/src/common/services/index.ts +1 -0
  21. package/templates/nest-api/files/src/common/services/temporal.service.ts +66 -0
  22. package/templates/nest-api/files/src/instrumentation.ts +117 -0
  23. package/templates/nest-api/files/src/main.ts +3 -1
  24. package/templates/nest-api/files/src/notifications/services/notifications.service.ts +4 -2
  25. package/templates/nest-api/files/src/telemetry/__tests__/config.spec.ts +67 -0
  26. package/templates/nest-api/files/src/telemetry/config.ts +62 -0
  27. package/templates/nest-api/template.json +54 -14
  28. package/templates/node-worker/files/.bin/secrets.ts +15 -6
  29. package/templates/node-worker/files/.env.example +27 -0
  30. package/templates/node-worker/files/Dockerfile +25 -2
  31. package/templates/node-worker/files/Dockerfile.dev +25 -2
  32. package/templates/node-worker/files/_gitignore +1 -0
  33. package/templates/node-worker/files/ecosystem.config.cjs +2 -0
  34. package/templates/node-worker/files/package.json +24 -4
  35. package/templates/node-worker/files/src/config.ts +23 -0
  36. package/templates/node-worker/files/src/index.ts +40 -4
  37. package/templates/node-worker/files/src/instrumentation.ts +110 -0
  38. package/templates/node-worker/files/src/logger.ts +14 -1
  39. package/templates/node-worker/files/src/telemetry/config.ts +61 -0
  40. package/templates/node-worker/files/src/temporal/activities/index.ts +14 -0
  41. package/templates/node-worker/files/src/temporal/worker.ts +84 -0
  42. package/templates/node-worker/files/src/temporal/workflows/index.ts +23 -0
  43. package/templates/node-worker/files/src/utils/worker.ts +10 -2
  44. package/templates/node-worker/template.json +43 -1
  45. package/templates/py-worker/files/.env.example +28 -0
  46. package/templates/py-worker/files/config.py +32 -1
  47. package/templates/py-worker/files/requirements.txt +16 -0
  48. package/templates/py-worker/files/temporal/__init__.py +0 -0
  49. package/templates/py-worker/files/temporal/activities.py +13 -0
  50. package/templates/py-worker/files/temporal/workflows.py +38 -0
  51. package/templates/py-worker/files/utils/db.py +20 -27
  52. package/templates/py-worker/files/utils/logger.py +9 -2
  53. package/templates/py-worker/files/utils/telemetry.py +154 -0
  54. package/templates/py-worker/files/utils/temporal.py +55 -0
  55. package/templates/py-worker/files/utils/worker.py +14 -3
  56. package/templates/py-worker/files/worker.py +16 -0
  57. package/templates/py-worker/template.json +36 -0
  58. package/templates/react-app/derive.sh +2 -1
  59. package/templates/react-app/files/.bin/secrets.js +8 -2
  60. package/templates/react-app/files/.env.example +11 -0
  61. package/templates/react-app/files/.forgejo/workflows/build-and-push.yml +3 -0
  62. package/templates/react-app/files/Dockerfile +10 -0
  63. package/templates/react-app/files/_gitignore +1 -0
  64. package/templates/react-app/files/build.sh +3 -1
  65. package/templates/react-app/files/package.json +2 -0
  66. package/templates/react-app/files/src/lib/env.ts +10 -0
  67. package/templates/react-app/files/src/main.tsx +4 -0
  68. package/templates/react-app/files/src/shared/auth/provider.tsx +2 -0
  69. package/templates/react-app/files/src/shared/layouts/error-boundary.tsx +2 -0
  70. package/templates/react-app/files/src/shared/telemetry/index.ts +80 -0
  71. package/templates/react-app/template.json +16 -2
  72. package/templates/react-monorepo/files/.forgejo/workflows/build-and-push.yml +15 -11
  73. package/templates/react-monorepo/files/Dockerfile +21 -7
  74. package/templates/react-monorepo/files/_gitignore +1 -0
  75. package/templates/react-monorepo/files/apps/admin/.bin/secrets.js +8 -3
  76. package/templates/react-monorepo/files/apps/admin/.env.example +11 -0
  77. package/templates/react-monorepo/files/apps/admin/package.json +1 -0
  78. package/templates/react-monorepo/files/apps/admin/src/app/router.tsx +11 -7
  79. package/templates/react-monorepo/files/apps/admin/src/lib/env.ts +10 -0
  80. package/templates/react-monorepo/files/apps/admin/src/main.tsx +4 -0
  81. package/templates/react-monorepo/files/apps/auth/.bin/secrets.js +8 -3
  82. package/templates/react-monorepo/files/apps/auth/.env.example +11 -0
  83. package/templates/react-monorepo/files/apps/auth/package.json +1 -0
  84. package/templates/react-monorepo/files/apps/auth/src/lib/env.ts +10 -0
  85. package/templates/react-monorepo/files/apps/auth/src/main.tsx +4 -0
  86. package/templates/react-monorepo/files/apps/client/.bin/secrets.js +8 -3
  87. package/templates/react-monorepo/files/apps/client/.env.example +11 -0
  88. package/templates/react-monorepo/files/apps/client/package.json +1 -0
  89. package/templates/react-monorepo/files/apps/client/src/lib/env.ts +10 -0
  90. package/templates/react-monorepo/files/apps/client/src/main.tsx +4 -0
  91. package/templates/react-monorepo/files/apps/client/src/vite-env.d.ts +1 -1
  92. package/templates/react-monorepo/files/apps/landing/.bin/secrets.js +8 -3
  93. package/templates/react-monorepo/files/apps/landing/.env.example +11 -0
  94. package/templates/react-monorepo/files/apps/landing/package.json +1 -0
  95. package/templates/react-monorepo/files/apps/landing/src/lib/env.ts +10 -0
  96. package/templates/react-monorepo/files/apps/landing/src/main.tsx +4 -0
  97. package/templates/react-monorepo/files/build.sh +3 -1
  98. package/templates/react-monorepo/files/packages/auth/package.json +1 -0
  99. package/templates/react-monorepo/files/packages/auth/src/provider.tsx +2 -0
  100. package/templates/react-monorepo/files/packages/layouts/package.json +1 -0
  101. package/templates/react-monorepo/files/packages/layouts/src/error-boundary.tsx +2 -0
  102. package/templates/react-monorepo/files/packages/telemetry/eslint.config.mjs +3 -0
  103. package/templates/react-monorepo/files/packages/telemetry/package.json +20 -0
  104. package/templates/react-monorepo/files/packages/telemetry/src/index.ts +80 -0
  105. package/templates/react-monorepo/files/packages/telemetry/tsconfig.json +4 -0
  106. package/templates/react-monorepo/files/packages/types/.bin/secrets.js +8 -3
  107. package/templates/react-monorepo/files/turbo.json +1 -1
  108. package/templates/react-monorepo/template.json +87 -9
  109. package/templates/nest-api/files/src/common/controllers/metrics.controller.ts +0 -21
  110. package/templates/nest-api/files/src/common/interceptors/index.ts +0 -1
  111. package/templates/nest-api/files/src/common/interceptors/metrics.interceptor.ts +0 -37
  112. package/templates/nest-api/files/src/common/modules/metrics.module.ts +0 -28
@@ -1,5 +1,7 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
+ import { initTelemetry } from '@/shared/telemetry'; // @feature telemetry
4
+ import { API_URL, TELEMETRY } from '@/lib/env'; // @feature telemetry
3
5
  import { RouterProvider } from 'react-router';
4
6
  import '@/shared/ui/styles/globals.css';
5
7
  import { Providers } from './app/providers';
@@ -10,6 +12,8 @@ import { router } from './app/router';
10
12
  // evaluating and `providers` has already called `configureApi` at module scope,
11
13
  // so the fetch races the session check instead of queueing behind it.
12
14
 
15
+ initTelemetry({ service: 'acme-app', ...TELEMETRY, apiUrls: [API_URL] }); // @feature telemetry
16
+
13
17
  createRoot(document.getElementById('root')!).render(
14
18
  <StrictMode>
15
19
  <Providers>
@@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux';
3
3
  import { getAuthClient } from './client';
4
4
  import { sessionCleared, sessionLoaded, sessionLoading, type AuthUser } from './slice';
5
5
  import { AUTH_BROADCAST_CHANNEL } from './logout';
6
+ import { setTelemetryUser } from '@/shared/telemetry'; // @feature telemetry
6
7
 
7
8
  function toAuthUser(user: Record<string, unknown> | null | undefined): AuthUser | null {
8
9
  if (!user) return null;
@@ -33,6 +34,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
33
34
  }
34
35
  const user = (session.data?.user as Record<string, unknown> | undefined) ?? null;
35
36
  dispatch(sessionLoaded(toAuthUser(user)));
37
+ setTelemetryUser(user ? { id: String(user.id) } : null); // @feature telemetry
36
38
  }, [session.isPending, session.data, dispatch]);
37
39
 
38
40
  useEffect(() => {
@@ -1,6 +1,7 @@
1
1
  import { Component, type ErrorInfo, type ReactNode } from 'react';
2
2
  import { Button } from '@/shared/ui';
3
3
  import { StateScreen } from './states/state-screen';
4
+ import { reportError } from '@/shared/telemetry'; // @feature telemetry
4
5
 
5
6
  type ErrorBoundaryProps = {
6
7
  children: ReactNode;
@@ -21,6 +22,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
21
22
 
22
23
  componentDidCatch(error: Error, info: ErrorInfo): void {
23
24
  console.error('ErrorBoundary caught an error', error, info);
25
+ reportError(error, { componentStack: info.componentStack }); // @feature telemetry
24
26
  }
25
27
 
26
28
  render(): ReactNode {
@@ -0,0 +1,80 @@
1
+ // @/shared/telemetry — browser monitoring (RUM) and logs, sent to OpenObserve.
2
+ //
3
+ // Page views, errors, slow resources and user actions, plus console errors as
4
+ // logs. Requests to the app's own API carry a W3C `traceparent` header, so a
5
+ // click links to the backend trace it caused. The SDK finishes starting a
6
+ // moment after init (its session store is async), so the first request or two
7
+ // of a page load (typically the session check) go out without the header.
8
+ // Off until connection settings are passed; everything here is then a no-op.
9
+ import { DefaultPrivacyLevel, openobserveRum } from '@openobserve/browser-rum';
10
+ import { openobserveLogs } from '@openobserve/browser-logs';
11
+
12
+ export interface TelemetryOptions {
13
+ /** Service name, e.g. `acme-client` — one per app. */
14
+ service: string;
15
+ /** OpenObserve base URL, e.g. `https://o2.example.com`. */
16
+ url?: string;
17
+ /** The project's organization identifier. */
18
+ org?: string;
19
+ /**
20
+ * A RUM client token (Ingestion → RUM). Write-only and built for shipping
21
+ * in a bundle — never the backend's ingestion token.
22
+ */
23
+ clientToken?: string;
24
+ /** `development`, `production`, … — Vite's `import.meta.env.MODE`. */
25
+ environment?: string;
26
+ version?: string;
27
+ /** Backend origin(s) whose requests should carry trace headers. */
28
+ apiUrls?: string[];
29
+ /** Share of sessions to record for replay, 0–100. Off by default. */
30
+ sessionReplaySampleRate?: number;
31
+ }
32
+
33
+ let started = false;
34
+
35
+ export function initTelemetry(options: TelemetryOptions): boolean {
36
+ const { url, org, clientToken } = options;
37
+ if (started || !url || !org || !clientToken) return false;
38
+
39
+ // The SDK takes a bare host (no scheme) plus an explicit plain-HTTP flag.
40
+ const { host, protocol } = new URL(url);
41
+ const common = {
42
+ clientToken,
43
+ site: host,
44
+ organizationIdentifier: org,
45
+ insecureHTTP: protocol === 'http:',
46
+ service: options.service,
47
+ env: options.environment,
48
+ version: options.version
49
+ };
50
+
51
+ openobserveRum.init({
52
+ ...common,
53
+ applicationId: options.service,
54
+ sessionSampleRate: 100,
55
+ sessionReplaySampleRate: options.sessionReplaySampleRate ?? 0,
56
+ defaultPrivacyLevel: DefaultPrivacyLevel.MASK_USER_INPUT,
57
+ trackResources: true,
58
+ trackLongTasks: true,
59
+ trackUserInteractions: true,
60
+ allowedTracingUrls: (options.apiUrls ?? []).filter(Boolean).map((match) => ({
61
+ match,
62
+ propagatorTypes: ['tracecontext' as const]
63
+ }))
64
+ });
65
+ openobserveLogs.init({ ...common, forwardErrorsToLogs: true });
66
+ started = true;
67
+ return true;
68
+ }
69
+
70
+ /** Report a handled error — e.g. from an error boundary. */
71
+ export function reportError(error: unknown, context?: object): void {
72
+ if (started) openobserveRum.addError(error, context);
73
+ }
74
+
75
+ /** Tie the session to a signed-in user (id only), or clear it on logout. */
76
+ export function setTelemetryUser(user: { id: string } | null): void {
77
+ if (!started) return;
78
+ if (user) openobserveRum.setUser({ id: user.id });
79
+ else openobserveRum.clearUser();
80
+ }
@@ -127,7 +127,7 @@
127
127
  },
128
128
  "pwa": {
129
129
  "title": "Installable PWA",
130
- "description": "vite-plugin-pwa: app-shell offline caching and an update prompt rather than a silent takeover.",
130
+ "description": "vite-plugin-pwa: app-shell offline caching, and registerType 'prompt' so a new build waits for the user instead of swapping assets under a running app. The update prompt and install button are yours to build with the plugin's useRegisterSW.",
131
131
  "packageJson": {
132
132
  "devDependencies": [
133
133
  "vite-plugin-pwa"
@@ -136,7 +136,7 @@
136
136
  },
137
137
  "charts": {
138
138
  "title": "Charts",
139
- "description": "ApexCharts via react-apexcharts, theme-aware through the shared chart palette.",
139
+ "description": "ApexCharts and react-apexcharts, with a theme-aware chart palette (the --chart-* tokens in the UI kit). No chart components ship.",
140
140
  "packageJson": {
141
141
  "dependencies": [
142
142
  "apexcharts",
@@ -189,6 +189,20 @@
189
189
  "secrets"
190
190
  ]
191
191
  }
192
+ },
193
+ "telemetry": {
194
+ "title": "Browser monitoring → OpenObserve",
195
+ "description": "OpenObserve RUM and logs: page views, errors (including the error boundary's), slow resources, user actions, the signed-in user id, and traceparent headers on API calls so frontend and backend traces link up. Session replay is off unless a project turns it on. Off at runtime until VITE_OPENOBSERVE_* are set.",
196
+ "default": true,
197
+ "files": [
198
+ "src/shared/telemetry/**"
199
+ ],
200
+ "packageJson": {
201
+ "dependencies": [
202
+ "@openobserve/browser-logs",
203
+ "@openobserve/browser-rum"
204
+ ]
205
+ }
192
206
  }
193
207
  },
194
208
  "hooks": [
@@ -1,12 +1,11 @@
1
- # Builds and pushes all four frontend apps from one Dockerfile.
1
+ # Builds and pushes every app's image from one Dockerfile.
2
2
  #
3
- # Unlike web-backend / web-coming-soon this repo produces FOUR images, so the
4
- # tag is generated once in a separate job and shared by every matrix leg —
3
+ # The tag is generated once in a separate job and shared by every matrix leg;
5
4
  # otherwise each app would get its own random tag and there would be no way to
6
5
  # deploy a matching set.
7
6
  #
8
7
  # Vite compiles the VITE_* URLs into each bundle at build time, so these images
9
- # are specific to one environment. See frontend/docker/README.md.
8
+ # are specific to one environment.
10
9
 
11
10
  name: Build and Push
12
11
 
@@ -33,12 +32,10 @@ jobs:
33
32
  DATE=$(date +'%y_%m_%d')
34
33
  RAND=$(printf '%05d' "$((RANDOM % 100000))")
35
34
  echo "TAG=${DATE}_${RAND}" >> "$GITHUB_OUTPUT"
36
- # Derived from `github.repository` (which web-backend's workflow already
37
- # relies on) rather than `github.repository_owner`, so this doesn't
38
- # depend on a context variable Forgejo may not populate. This repo is
39
- # `acme-corp/web-frontend`, so OWNER=acme-corp and the
40
- # images land as <registry>/acme-corp/web-<app>, matching
41
- # stack/frontend.compose.yml.
35
+ # Derived from `github.repository` rather than
36
+ # `github.repository_owner`, so this doesn't depend on a context
37
+ # variable Forgejo may not populate. Images land as
38
+ # <registry>/<owner>/web-<app>.
42
39
  echo "OWNER=$(echo '${{ github.repository }}' | cut -d/ -f1)" >> "$GITHUB_OUTPUT"
43
40
 
44
41
  build-and-push:
@@ -49,7 +46,11 @@ jobs:
49
46
  # failure. Note this means a partial set can be pushed — re-run the job.
50
47
  fail-fast: false
51
48
  matrix:
52
- app: [landing, auth, client, admin]
49
+ app:
50
+ - landing # @feature app-landing
51
+ - auth # @feature app-auth
52
+ - client # @feature app-client
53
+ - admin # @feature app-admin
53
54
 
54
55
  steps:
55
56
  - name: Checkout repository
@@ -83,6 +84,9 @@ jobs:
83
84
  VITE_APP_LANDING=${{ secrets.VITE_APP_LANDING }}
84
85
  VITE_APP_SUPPORT_EMAIL=${{ secrets.VITE_APP_SUPPORT_EMAIL }}
85
86
  VITE_APP_SITE_URL=${{ secrets.VITE_APP_SITE_URL }}
87
+ VITE_OPENOBSERVE_URL=${{ secrets.VITE_OPENOBSERVE_URL }} # @feature telemetry
88
+ VITE_OPENOBSERVE_ORG=${{ secrets.VITE_OPENOBSERVE_ORG }} # @feature telemetry
89
+ VITE_OPENOBSERVE_CLIENT_TOKEN=${{ secrets.VITE_OPENOBSERVE_CLIENT_TOKEN }} # @feature telemetry
86
90
  tags: |
87
91
  ${{ secrets.REGISTRY }}/${{ needs.meta.outputs.owner }}/web-${{ matrix.app }}:latest
88
92
  ${{ secrets.REGISTRY }}/${{ needs.meta.outputs.owner }}/web-${{ matrix.app }}:${{ needs.meta.outputs.tag }}
@@ -9,24 +9,28 @@
9
9
  # IMPORTANT: Vite inlines `VITE_*` values into the JS bundle at BUILD time, so a
10
10
  # built image is pinned to one environment. The URLs below are build args, not
11
11
  # runtime env — setting them in compose does nothing. Build one image per
12
- # environment, or move to runtime config (see docker/README.md).
12
+ # environment.
13
13
 
14
14
  # ---------- deps ----------
15
15
  # Manifests only, so `yarn install` re-runs solely when a dependency changes.
16
16
  FROM aurostack.dev/wesnetech/nodejs:24 AS deps
17
17
  WORKDIR /repo
18
18
 
19
+ # One line per workspace: every workspace in the project must be here (yarn
20
+ # install --immutable fails on a lockfile naming a missing one), and nothing
21
+ # else (COPY of a missing file fails the build).
19
22
  COPY package.json yarn.lock .yarnrc.yml turbo.json ./
20
- COPY apps/admin/package.json apps/admin/
21
- COPY apps/auth/package.json apps/auth/
22
- COPY apps/client/package.json apps/client/
23
- COPY apps/landing/package.json apps/landing/
23
+ COPY apps/admin/package.json apps/admin/ # @feature app-admin
24
+ COPY apps/auth/package.json apps/auth/ # @feature app-auth
25
+ COPY apps/client/package.json apps/client/ # @feature app-client
26
+ COPY apps/landing/package.json apps/landing/ # @feature app-landing
24
27
  COPY packages/api/package.json packages/api/
25
28
  COPY packages/auth/package.json packages/auth/
26
29
  COPY packages/config/package.json packages/config/
27
- COPY packages/hooks/package.json packages/hooks/
30
+ COPY packages/hooks/package.json packages/hooks/ # @feature hooks
28
31
  COPY packages/layouts/package.json packages/layouts/
29
- COPY packages/types/package.json packages/types/
32
+ COPY packages/telemetry/package.json packages/telemetry/ # @feature telemetry
33
+ COPY packages/types/package.json packages/types/ # @feature types
30
34
  COPY packages/ui/package.json packages/ui/
31
35
 
32
36
  RUN corepack enable && yarn install --immutable
@@ -56,6 +60,16 @@ ARG VITE_APP_SUPPORT_EMAIL
56
60
  # generated sitemap.xml. Must be a build arg — .dockerignore excludes .env, so a
57
61
  # local value never reaches the image.
58
62
  ARG VITE_APP_SITE_URL
63
+ # @feature:start telemetry
64
+ # Browser monitoring. Leave unset and the bundle ships with telemetry off. The
65
+ # client token is a RUM token, write-only and meant to be public.
66
+ ARG VITE_OPENOBSERVE_URL
67
+ ARG VITE_OPENOBSERVE_ORG
68
+ ARG VITE_OPENOBSERVE_CLIENT_TOKEN
69
+ ENV VITE_OPENOBSERVE_URL=$VITE_OPENOBSERVE_URL \
70
+ VITE_OPENOBSERVE_ORG=$VITE_OPENOBSERVE_ORG \
71
+ VITE_OPENOBSERVE_CLIENT_TOKEN=$VITE_OPENOBSERVE_CLIENT_TOKEN
72
+ # @feature:end
59
73
  ENV VITE_APP_API_URL=$VITE_APP_API_URL \
60
74
  VITE_APP_AUTH_HOST=$VITE_APP_AUTH_HOST \
61
75
  VITE_APP_CLIENT_HOST=$VITE_APP_CLIENT_HOST \
@@ -22,6 +22,7 @@ dist/
22
22
  .env.*.local
23
23
  .env.test
24
24
  .env.production
25
+ .env.staging
25
26
 
26
27
  # editor / os
27
28
  .DS_Store
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
52
52
  const envFile = ENV_FILES[env];
53
53
 
54
54
  console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
55
- execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env} > ${envFile}`, {
56
- stdio: 'inherit'
55
+ // Export first and write only on success: a shell `>` redirect would empty
56
+ // the existing file before infisical had the chance to fail. execSync throws
57
+ // (and the process exits non-zero) when infisical fails.
58
+ const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
59
+ stdio: ['inherit', 'pipe', 'inherit']
57
60
  });
61
+ writeFileSync(envFile, output);
62
+ console.log(`Wrote ${envFile}`);
@@ -14,3 +14,14 @@ VITE_APP_ADMIN_HOST='http://localhost:3003'
14
14
 
15
15
  # This app's own dev port.
16
16
  PORT='3003'
17
+
18
+ # @feature:start telemetry
19
+ # OpenObserve browser monitoring (RUM): page views, errors, slow requests, and
20
+ # trace headers on API calls. Leave empty and it stays off. The client token is
21
+ # a RUM token (OpenObserve → Ingestion → RUM): write-only and meant to ship in
22
+ # the bundle — never the backend's ingestion token.
23
+ # The instance must list this app's origin in ZO_CORS_ALLOWED_ORIGINS.
24
+ VITE_OPENOBSERVE_URL='https://o2.aurostack.co'
25
+ VITE_OPENOBSERVE_ORG=''
26
+ VITE_OPENOBSERVE_CLIENT_TOKEN=''
27
+ # @feature:end
@@ -15,6 +15,7 @@
15
15
  "@acme/api": "workspace:*",
16
16
  "@acme/auth": "workspace:*",
17
17
  "@acme/layouts": "workspace:*",
18
+ "@acme/telemetry": "workspace:*",
18
19
  "@acme/ui": "workspace:*",
19
20
  "@reduxjs/toolkit": "^2.12.0",
20
21
  "lucide-react": "^1.17.0",
@@ -1,7 +1,7 @@
1
1
  import { Suspense, lazy, type ReactNode } from 'react';
2
2
  import { createBrowserRouter } from 'react-router';
3
- import { ProtectedRoute, signOut, useAuth } from '@acme/auth';
4
- import { AppShell, AppShellSkeleton, ErrorBoundary, NotFound, RouteFallback } from '@acme/layouts';
3
+ import { RoleRoute, signOut, useAuth } from '@acme/auth';
4
+ import { AppShell, AppShellSkeleton, ErrorBoundary, Forbidden, NotFound, RouteFallback } from '@acme/layouts';
5
5
  import { NAV_ITEMS } from './nav';
6
6
  import { HomeRoute } from '../routes/home';
7
7
 
@@ -15,8 +15,8 @@ const lazyRoute = (element: ReactNode) => <Suspense fallback={<RouteFallback />}
15
15
 
16
16
  /**
17
17
  * The shell reads the session itself rather than taking it as a prop —
18
- * `ProtectedRoute` has already established the user is authenticated by the
19
- * time this renders.
18
+ * `RoleRoute` has already established the user is signed in with an admin role
19
+ * by the time this renders.
20
20
  *
21
21
  * `ErrorBoundary` wraps the shell rather than being passed as the route's
22
22
  * `errorElement`: it is a plain React error boundary taking `children`, so it
@@ -35,11 +35,15 @@ export const router = createBrowserRouter([
35
35
  {
36
36
  // `fallback` is required and must not be blank: the session round trip is
37
37
  // cross-origin and takes most of a second, and rendering nothing for that
38
- // long reads as a broken app.
38
+ // long reads as a broken app. Signed-out visitors go to the auth app;
39
+ // signed-in users without an admin role see Forbidden.
40
+ //
41
+ // The gate is UI, not security: every endpoint this console calls must
42
+ // enforce the same role on the API.
39
43
  element: (
40
- <ProtectedRoute fallback={<AppShellSkeleton />}>
44
+ <RoleRoute allow={['admin', 'superuser']} fallback={<AppShellSkeleton />} forbidden={<Forbidden />}>
41
45
  <Shell />
42
- </ProtectedRoute>
46
+ </RoleRoute>
43
47
  ),
44
48
  children: [
45
49
  { index: true, element: <HomeRoute /> },
@@ -10,3 +10,13 @@ export const ADMIN_URL = env.VITE_APP_ADMIN_HOST as string;
10
10
  export const CLIENT_URL = env.VITE_APP_CLIENT_HOST as string;
11
11
  /** The public marketing site (logout destination). */
12
12
  export const LANDING_URL = env.VITE_APP_LANDING as string;
13
+
14
+ // @feature:start telemetry
15
+ /** OpenObserve browser monitoring (see @acme/telemetry); unset keeps it off. */
16
+ export const TELEMETRY = {
17
+ url: env.VITE_OPENOBSERVE_URL as string | undefined,
18
+ org: env.VITE_OPENOBSERVE_ORG as string | undefined,
19
+ clientToken: env.VITE_OPENOBSERVE_CLIENT_TOKEN as string | undefined,
20
+ environment: env.MODE
21
+ };
22
+ // @feature:end
@@ -1,10 +1,14 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
+ import { initTelemetry } from '@acme/telemetry'; // @feature telemetry
4
+ import { API_URL, TELEMETRY } from './lib/env'; // @feature telemetry
3
5
  import { RouterProvider } from 'react-router';
4
6
  import '@acme/ui/globals.css';
5
7
  import { Providers } from './app/providers';
6
8
  import { router } from './app/router';
7
9
 
10
+ initTelemetry({ service: 'acme-admin', ...TELEMETRY, apiUrls: [API_URL] }); // @feature telemetry
11
+
8
12
  createRoot(document.getElementById('root')!).render(
9
13
  <StrictMode>
10
14
  <Providers>
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
52
52
  const envFile = ENV_FILES[env];
53
53
 
54
54
  console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
55
- execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env} > ${envFile}`, {
56
- stdio: 'inherit'
55
+ // Export first and write only on success: a shell `>` redirect would empty
56
+ // the existing file before infisical had the chance to fail. execSync throws
57
+ // (and the process exits non-zero) when infisical fails.
58
+ const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
59
+ stdio: ['inherit', 'pipe', 'inherit']
57
60
  });
61
+ writeFileSync(envFile, output);
62
+ console.log(`Wrote ${envFile}`);
@@ -14,3 +14,14 @@ VITE_APP_ADMIN_HOST='http://localhost:3003'
14
14
 
15
15
  # This app's own dev port.
16
16
  PORT='3001'
17
+
18
+ # @feature:start telemetry
19
+ # OpenObserve browser monitoring (RUM): page views, errors, slow requests, and
20
+ # trace headers on API calls. Leave empty and it stays off. The client token is
21
+ # a RUM token (OpenObserve → Ingestion → RUM): write-only and meant to ship in
22
+ # the bundle — never the backend's ingestion token.
23
+ # The instance must list this app's origin in ZO_CORS_ALLOWED_ORIGINS.
24
+ VITE_OPENOBSERVE_URL='https://o2.aurostack.co'
25
+ VITE_OPENOBSERVE_ORG=''
26
+ VITE_OPENOBSERVE_CLIENT_TOKEN=''
27
+ # @feature:end
@@ -15,6 +15,7 @@
15
15
  "@acme/api": "workspace:*",
16
16
  "@acme/auth": "workspace:*",
17
17
  "@acme/layouts": "workspace:*",
18
+ "@acme/telemetry": "workspace:*",
18
19
  "@acme/ui": "workspace:*",
19
20
  "@reduxjs/toolkit": "^2.12.0",
20
21
  "formik": "^2.4.9",
@@ -6,3 +6,13 @@ export const API_URL = env.VITE_APP_API_URL;
6
6
  /** Terms and Privacy live on the marketing site, which is a different origin. */
7
7
  export const LANDING_URL = env.VITE_APP_LANDING;
8
8
  export const AUTH_URL = window.location.origin;
9
+
10
+ // @feature:start telemetry
11
+ /** OpenObserve browser monitoring (see @acme/telemetry); unset keeps it off. */
12
+ export const TELEMETRY = {
13
+ url: env.VITE_OPENOBSERVE_URL as string | undefined,
14
+ org: env.VITE_OPENOBSERVE_ORG as string | undefined,
15
+ clientToken: env.VITE_OPENOBSERVE_CLIENT_TOKEN as string | undefined,
16
+ environment: env.MODE
17
+ };
18
+ // @feature:end
@@ -1,10 +1,14 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
+ import { initTelemetry } from '@acme/telemetry'; // @feature telemetry
4
+ import { API_URL, TELEMETRY } from './lib/env'; // @feature telemetry
3
5
  import { RouterProvider } from 'react-router';
4
6
  import '@acme/ui/globals.css';
5
7
  import { Providers } from './app/providers';
6
8
  import { router } from './app/router';
7
9
 
10
+ initTelemetry({ service: 'acme-auth', ...TELEMETRY, apiUrls: [API_URL] }); // @feature telemetry
11
+
8
12
  createRoot(document.getElementById('root')!).render(
9
13
  <StrictMode>
10
14
  <Providers>
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
52
52
  const envFile = ENV_FILES[env];
53
53
 
54
54
  console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
55
- execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env} > ${envFile}`, {
56
- stdio: 'inherit'
55
+ // Export first and write only on success: a shell `>` redirect would empty
56
+ // the existing file before infisical had the chance to fail. execSync throws
57
+ // (and the process exits non-zero) when infisical fails.
58
+ const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
59
+ stdio: ['inherit', 'pipe', 'inherit']
57
60
  });
61
+ writeFileSync(envFile, output);
62
+ console.log(`Wrote ${envFile}`);
@@ -14,3 +14,14 @@ VITE_APP_ADMIN_HOST='http://localhost:3003'
14
14
 
15
15
  # This app's own dev port.
16
16
  PORT='3002'
17
+
18
+ # @feature:start telemetry
19
+ # OpenObserve browser monitoring (RUM): page views, errors, slow requests, and
20
+ # trace headers on API calls. Leave empty and it stays off. The client token is
21
+ # a RUM token (OpenObserve → Ingestion → RUM): write-only and meant to ship in
22
+ # the bundle — never the backend's ingestion token.
23
+ # The instance must list this app's origin in ZO_CORS_ALLOWED_ORIGINS.
24
+ VITE_OPENOBSERVE_URL='https://o2.aurostack.co'
25
+ VITE_OPENOBSERVE_ORG=''
26
+ VITE_OPENOBSERVE_CLIENT_TOKEN=''
27
+ # @feature:end
@@ -15,6 +15,7 @@
15
15
  "@acme/api": "workspace:*",
16
16
  "@acme/auth": "workspace:*",
17
17
  "@acme/layouts": "workspace:*",
18
+ "@acme/telemetry": "workspace:*",
18
19
  "@acme/ui": "workspace:*",
19
20
  "@reduxjs/toolkit": "^2.12.0",
20
21
  "apexcharts": "^6.5.0",
@@ -10,3 +10,13 @@ export const CLIENT_URL = env.VITE_APP_CLIENT_HOST as string;
10
10
  export const ADMIN_URL = env.VITE_APP_ADMIN_HOST as string;
11
11
  /** The public marketing site (logout destination). */
12
12
  export const LANDING_URL = env.VITE_APP_LANDING as string;
13
+
14
+ // @feature:start telemetry
15
+ /** OpenObserve browser monitoring (see @acme/telemetry); unset keeps it off. */
16
+ export const TELEMETRY = {
17
+ url: env.VITE_OPENOBSERVE_URL as string | undefined,
18
+ org: env.VITE_OPENOBSERVE_ORG as string | undefined,
19
+ clientToken: env.VITE_OPENOBSERVE_CLIENT_TOKEN as string | undefined,
20
+ environment: env.MODE
21
+ };
22
+ // @feature:end
@@ -1,5 +1,7 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
+ import { initTelemetry } from '@acme/telemetry'; // @feature telemetry
4
+ import { API_URL, TELEMETRY } from './lib/env'; // @feature telemetry
3
5
  import { RouterProvider } from 'react-router';
4
6
  import '@acme/ui/globals.css';
5
7
  import { Providers } from './app/providers';
@@ -10,6 +12,8 @@ import { router } from './app/router';
10
12
  // evaluating and `providers` has already called `configureApi` at module scope,
11
13
  // so the fetch races the session check instead of queueing behind it.
12
14
 
15
+ initTelemetry({ service: 'acme-client', ...TELEMETRY, apiUrls: [API_URL] }); // @feature telemetry
16
+
13
17
  createRoot(document.getElementById('root')!).render(
14
18
  <StrictMode>
15
19
  <Providers>
@@ -1,6 +1,6 @@
1
1
  /// <reference types="vite/client" />
2
2
  /// <reference types="vite-plugin-svgr/client" />
3
- /// <reference types="vite-plugin-pwa/react" />
3
+ /// <reference types="vite-plugin-pwa/react" /> // @feature pwa
4
4
 
5
5
  interface ImportMetaEnv {
6
6
  VITE_APP_API_URL: string;
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
52
52
  const envFile = ENV_FILES[env];
53
53
 
54
54
  console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
55
- execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env} > ${envFile}`, {
56
- stdio: 'inherit'
55
+ // Export first and write only on success: a shell `>` redirect would empty
56
+ // the existing file before infisical had the chance to fail. execSync throws
57
+ // (and the process exits non-zero) when infisical fails.
58
+ const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
59
+ stdio: ['inherit', 'pipe', 'inherit']
57
60
  });
61
+ writeFileSync(envFile, output);
62
+ console.log(`Wrote ${envFile}`);
@@ -18,3 +18,14 @@ PORT='3000'
18
18
  # Canonical public URL, used for sitemap/robots/OG tags at build time.
19
19
  VITE_APP_SITE_URL='http://localhost:3000'
20
20
  VITE_APP_SUPPORT_EMAIL='support@example.com'
21
+
22
+ # @feature:start telemetry
23
+ # OpenObserve browser monitoring (RUM): page views, errors, slow requests, and
24
+ # trace headers on API calls. Leave empty and it stays off. The client token is
25
+ # a RUM token (OpenObserve → Ingestion → RUM): write-only and meant to ship in
26
+ # the bundle — never the backend's ingestion token.
27
+ # The instance must list this app's origin in ZO_CORS_ALLOWED_ORIGINS.
28
+ VITE_OPENOBSERVE_URL='https://o2.aurostack.co'
29
+ VITE_OPENOBSERVE_ORG=''
30
+ VITE_OPENOBSERVE_CLIENT_TOKEN=''
31
+ # @feature:end
@@ -15,6 +15,7 @@
15
15
  "@acme/api": "workspace:*",
16
16
  "@acme/auth": "workspace:*",
17
17
  "@acme/layouts": "workspace:*",
18
+ "@acme/telemetry": "workspace:*",
18
19
  "@acme/types": "workspace:*",
19
20
  "@acme/ui": "workspace:*",
20
21
  "@reduxjs/toolkit": "^2.12.0",
@@ -8,3 +8,13 @@ export const CLIENT_HOST = env.VITE_APP_CLIENT_HOST;
8
8
  * actually posts. Falsy when the build arg is missing; the UI hides the block
9
9
  * rather than rendering `mailto:undefined`. */
10
10
  export const SUPPORT_EMAIL = env.VITE_APP_SUPPORT_EMAIL;
11
+
12
+ // @feature:start telemetry
13
+ /** OpenObserve browser monitoring (see @acme/telemetry); unset keeps it off. */
14
+ export const TELEMETRY = {
15
+ url: env.VITE_OPENOBSERVE_URL as string | undefined,
16
+ org: env.VITE_OPENOBSERVE_ORG as string | undefined,
17
+ clientToken: env.VITE_OPENOBSERVE_CLIENT_TOKEN as string | undefined,
18
+ environment: env.MODE
19
+ };
20
+ // @feature:end