@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Umami Database Client
3
+ * Reads analytics data directly from Umami's PostgreSQL database.
4
+ * No API key needed — we connect to the same DB Umami uses.
5
+ */
6
+
7
+ import { createLogger } from "@techstream/quark-core";
8
+
9
+ const log = createLogger("lib:umami-db");
10
+
11
+ let _pool = null;
12
+
13
+ /**
14
+ * Get or create the Umami DB connection pool.
15
+ * Uses pg module — falls back to a mock if pg is unavailable.
16
+ * @returns {Promise<object>} Pool with query() method
17
+ */
18
+ async function getPool() {
19
+ if (_pool) return _pool;
20
+
21
+ const connectionString = process.env.UMAMI_DATABASE_URL;
22
+
23
+ if (
24
+ connectionString &&
25
+ (connectionString.startsWith("postgresql://") ||
26
+ connectionString.startsWith("postgres://"))
27
+ ) {
28
+ // Try to import pg dynamically (it's a dependency of web, but worker may have it via prisma)
29
+ try {
30
+ const { default: pg } = await import("pg");
31
+ _pool = new pg.Pool({ connectionString });
32
+ // Test connection
33
+ const client = await _pool.connect();
34
+ await client.query("SELECT 1");
35
+ client.release();
36
+ log.info("Connected to Umami database");
37
+ return _pool;
38
+ } catch (err) {
39
+ log.warn("Cannot connect to Umami DB directly, using mock", {
40
+ error: err.message,
41
+ });
42
+ }
43
+ }
44
+
45
+ // Fallback mock — returns empty data when Umami DB isn't configured
46
+ log.info("Umami DB not configured — analytics will return empty data");
47
+ _pool = {
48
+ query: async () => ({ rows: [] }),
49
+ end: async () => {},
50
+ };
51
+ return _pool;
52
+ }
53
+
54
+ /**
55
+ * Execute a query against Umami's database.
56
+ * @param {string} text - SQL query
57
+ * @param {Array} [params] - Query parameters
58
+ * @returns {Promise<Array>} Result rows
59
+ */
60
+ async function query(text, params = []) {
61
+ try {
62
+ const pool = await getPool();
63
+ const result = await pool.query(text, params);
64
+ return result.rows;
65
+ } catch (error) {
66
+ log.error("Umami DB query failed", {
67
+ error: error.message,
68
+ text: text.slice(0, 100),
69
+ });
70
+ return [];
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Get the Umami website ID for a given managed project.
76
+ * @param {string} umamiWebsiteId - The umamiWebsiteId stored on ManagedProject
77
+ * @returns {Promise<string|null>} Umami website UUID
78
+ */
79
+ export async function getWebsiteId(umamiWebsiteId) {
80
+ if (!umamiWebsiteId) return null;
81
+
82
+ // The umamiWebsiteId on ManagedProject stores the UUID from Umami's website table
83
+ const rows = await query(
84
+ "SELECT website_id FROM website WHERE website_id::text = $1 OR domain = $1 LIMIT 1",
85
+ [umamiWebsiteId],
86
+ );
87
+
88
+ return rows.length > 0 ? rows[0].website_id : null;
89
+ }
90
+
91
+ /**
92
+ * Get pageview counts for a website within a date range.
93
+ * @param {string} websiteId - Umami website UUID
94
+ * @param {Date} startDate - Start of range
95
+ * @param {Date} endDate - End of range
96
+ * @returns {Promise<number>} Pageview count
97
+ */
98
+ export async function getPageviews(websiteId, startDate, endDate) {
99
+ const rows = await query(
100
+ `SELECT COUNT(*) as count
101
+ FROM website_event
102
+ WHERE website_id::text = $1
103
+ AND created_at >= $2
104
+ AND created_at <= $3
105
+ AND event_type = 1`,
106
+ [websiteId, startDate, endDate],
107
+ );
108
+ return parseInt(rows[0]?.count || "0", 10);
109
+ }
110
+
111
+ /**
112
+ * Get unique visitor count for a website within a date range.
113
+ * @param {string} websiteId - Umami website UUID
114
+ * @param {Date} startDate - Start of range
115
+ * @param {Date} endDate - End of range
116
+ * @returns {Promise<number>} Visitor count
117
+ */
118
+ export async function getVisitors(websiteId, startDate, endDate) {
119
+ const rows = await query(
120
+ `SELECT COUNT(DISTINCT session_id) as count
121
+ FROM website_event
122
+ WHERE website_id::text = $1
123
+ AND created_at >= $2
124
+ AND created_at <= $3`,
125
+ [websiteId, startDate, endDate],
126
+ );
127
+ return parseInt(rows[0]?.count || "0", 10);
128
+ }
129
+
130
+ /**
131
+ * Get top pages by pageview count for a website.
132
+ * @param {string} websiteId - Umami website UUID
133
+ * @param {Date} startDate - Start of range
134
+ * @param {Date} endDate - End of range
135
+ * @param {number} [limit=10] - Max results
136
+ * @returns {Promise<Array>} Top pages with url and count
137
+ */
138
+ export async function getTopPages(websiteId, startDate, endDate, limit = 10) {
139
+ return query(
140
+ `SELECT url_path as url, COUNT(*) as count
141
+ FROM website_event
142
+ WHERE website_id::text = $1
143
+ AND created_at >= $2
144
+ AND created_at <= $3
145
+ AND event_type = 1
146
+ GROUP BY url_path
147
+ ORDER BY count DESC
148
+ LIMIT $4`,
149
+ [websiteId, startDate, endDate, limit],
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Get bounce rate for a website within a date range.
155
+ * Bounce rate = sessions with only one event / total sessions.
156
+ * @param {string} websiteId - Umami website UUID
157
+ * @param {Date} startDate - Start of range
158
+ * @param {Date} endDate - End of range
159
+ * @returns {Promise<number>} Bounce rate percentage (0-100)
160
+ */
161
+ export async function getBounceRate(websiteId, startDate, endDate) {
162
+ const rows = await query(
163
+ `WITH session_event_counts AS (
164
+ SELECT session_id, COUNT(*) as event_count
165
+ FROM website_event
166
+ WHERE website_id::text = $1
167
+ AND created_at >= $2
168
+ AND created_at <= $3
169
+ GROUP BY session_id
170
+ )
171
+ SELECT
172
+ COUNT(*) FILTER (WHERE event_count = 1) as bounced_sessions,
173
+ COUNT(*) as total_sessions
174
+ FROM session_event_counts`,
175
+ [websiteId, startDate, endDate],
176
+ );
177
+
178
+ const total = parseInt(rows[0]?.total_sessions || "0", 10);
179
+ const bounced = parseInt(rows[0]?.bounced_sessions || "0", 10);
180
+
181
+ return total > 0 ? Math.round((bounced / total) * 10000) / 100 : 0;
182
+ }
183
+
184
+ /**
185
+ * Get referrer breakdown for a website.
186
+ * @param {string} websiteId - Umami website UUID
187
+ * @param {Date} startDate - Start of range
188
+ * @param {Date} endDate - End of range
189
+ * @param {number} [limit=10] - Max results
190
+ * @returns {Promise<Array>} Referrers with count
191
+ */
192
+ export async function getTrafficSources(
193
+ websiteId,
194
+ startDate,
195
+ endDate,
196
+ limit = 10,
197
+ ) {
198
+ return query(
199
+ `SELECT
200
+ COALESCE(NULLIF(referrer_domain, ''), 'direct') as source,
201
+ COUNT(*) as count
202
+ FROM website_event
203
+ WHERE website_id::text = $1
204
+ AND created_at >= $2
205
+ AND created_at <= $3
206
+ AND event_type = 1
207
+ GROUP BY source
208
+ ORDER BY count DESC
209
+ LIMIT $4`,
210
+ [websiteId, startDate, endDate, limit],
211
+ );
212
+ }
213
+
214
+ /**
215
+ * Get time-series pageview data for charts.
216
+ * @param {string} websiteId - Umami website UUID
217
+ * @param {Date} startDate - Start of range
218
+ * @param {Date} endDate - End of range
219
+ * @returns {Promise<Array>} Daily pageview counts
220
+ */
221
+ export async function getTimeSeries(websiteId, startDate, endDate) {
222
+ return query(
223
+ `SELECT
224
+ DATE(created_at) as date,
225
+ COUNT(*) as pageviews,
226
+ COUNT(DISTINCT session_id) as visitors
227
+ FROM website_event
228
+ WHERE website_id::text = $1
229
+ AND created_at >= $2
230
+ AND created_at <= $3
231
+ AND event_type = 1
232
+ GROUP BY DATE(created_at)
233
+ ORDER BY date ASC`,
234
+ [websiteId, startDate, endDate],
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Get all analytics data for a project in one call.
240
+ * @param {string} umamiWebsiteId - Umami website ID from ManagedProject
241
+ * @param {number} [days=30] - Lookback period
242
+ * @returns {Promise<object>} Aggregated analytics data
243
+ */
244
+ export async function getProjectAnalytics(umamiWebsiteId, days = 30) {
245
+ if (!umamiWebsiteId) {
246
+ return { error: "No Umami website ID configured" };
247
+ }
248
+
249
+ const websiteId = await getWebsiteId(umamiWebsiteId);
250
+ if (!websiteId) {
251
+ return { error: `Umami website "${umamiWebsiteId}" not found` };
252
+ }
253
+
254
+ const endDate = new Date();
255
+ const startDate = new Date();
256
+ startDate.setDate(startDate.getDate() - days);
257
+
258
+ const thirtyDaysAgo = startDate;
259
+ const sevenDaysAgo = new Date();
260
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
261
+
262
+ const [
263
+ pageviews,
264
+ visitors,
265
+ bounceRate,
266
+ topPages,
267
+ trafficSources,
268
+ timeSeries,
269
+ pageviews7d,
270
+ visitors7d,
271
+ ] = await Promise.all([
272
+ getPageviews(websiteId, thirtyDaysAgo, endDate),
273
+ getVisitors(websiteId, thirtyDaysAgo, endDate),
274
+ getBounceRate(websiteId, thirtyDaysAgo, endDate),
275
+ getTopPages(websiteId, thirtyDaysAgo, endDate),
276
+ getTrafficSources(websiteId, thirtyDaysAgo, endDate),
277
+ getTimeSeries(websiteId, thirtyDaysAgo, endDate),
278
+ getPageviews(websiteId, sevenDaysAgo, endDate),
279
+ getVisitors(websiteId, sevenDaysAgo, endDate),
280
+ ]);
281
+
282
+ return {
283
+ websiteId,
284
+ period: { days, startDate: thirtyDaysAgo, endDate },
285
+ summary: {
286
+ pageviews,
287
+ visitors,
288
+ bounceRate,
289
+ avgDailyPageviews: days > 0 ? Math.round(pageviews / days) : 0,
290
+ },
291
+ trends: {
292
+ last7days: { pageviews: pageviews7d, visitors: visitors7d },
293
+ last30days: { pageviews, visitors },
294
+ },
295
+ topPages,
296
+ trafficSources,
297
+ timeSeries,
298
+ };
299
+ }
@@ -0,0 +1,20 @@
1
+ packages:
2
+ - apps/*
3
+ - packages/*
4
+
5
+ onlyBuiltDependencies:
6
+ - '@prisma/engines'
7
+ - prisma
8
+ - esbuild
9
+ - msgpackr-extract
10
+ - sharp
11
+ - simple-git-hooks
12
+
13
+ overrides:
14
+ fast-xml-builder@<1.1.7: ">=1.1.7"
15
+ fast-xml-parser@<5.10.1: ">=5.10.1"
16
+ lodash@>=4.0.0 <=4.17.22: ">=4.17.23"
17
+ hono@<4.12.25: ">=4.12.25"
18
+ fast-uri@<4.1.3: ">=4.1.4"
19
+ deepmerge-ts@<8.0.0: ">=8.0.0"
20
+ mysql2@<3.22.0: ">=3.22.0"
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Audit script: check every DB-backed page has a sibling loading.js.
3
+ *
4
+ * Run: node scripts/check-loading.mjs
5
+ *
6
+ * Exit code 0 = all pages covered.
7
+ * Exit code 1 = gaps found (prints paths).
8
+ */
9
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
10
+ import { join, relative } from "node:path";
11
+
12
+ const APP_DIR = new URL("../apps/web/src/app", import.meta.url).pathname;
13
+ const SKIP_DIRS = new Set(["node_modules", ".next", "api"]);
14
+
15
+ let failed = false;
16
+
17
+ function isDbBacked(pagePath) {
18
+ const content = readFileSync(pagePath, "utf-8");
19
+ // Matches scoped db packages in scaffolds (@scope/db) and the monorepo
20
+ // package name (@techstream/quark-db).
21
+ return /@[^/"'\s]+\/(?:quark-)?db["']/.test(content);
22
+ }
23
+
24
+ function visit(dir) {
25
+ const entries = readdirSync(dir, { withFileTypes: true });
26
+ const pagePath = join(dir, "page.js");
27
+ if (existsSync(pagePath) && isDbBacked(pagePath)) {
28
+ const loadingPath = join(dir, "loading.js");
29
+ if (!existsSync(loadingPath)) {
30
+ console.error("MISSING loading.js in", relative(APP_DIR, dir));
31
+ failed = true;
32
+ }
33
+ }
34
+ for (const entry of entries) {
35
+ if (!entry.isDirectory()) continue;
36
+ if (SKIP_DIRS.has(entry.name)) continue;
37
+ if (entry.name.startsWith("_") || entry.name.startsWith("(")) continue;
38
+ visit(join(dir, entry.name));
39
+ }
40
+ }
41
+
42
+ visit(APP_DIR);
43
+
44
+ if (failed) {
45
+ console.error("\nSome DB-backed pages are missing loading.js.");
46
+ process.exit(1);
47
+ } else {
48
+ console.log("All DB-backed pages have a sibling loading.js.");
49
+ }
@@ -0,0 +1,186 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const rootDir = process.cwd();
5
+ const rootsToScan = ["apps", "packages", "scripts"];
6
+ const ignoreDirNames = new Set([
7
+ "node_modules",
8
+ ".git",
9
+ ".next",
10
+ ".turbo",
11
+ "coverage",
12
+ ]);
13
+ const jsExtensions = new Set([".js", ".jsx", ".mjs"]);
14
+ const tsExtensions = new Set([".ts", ".tsx"]);
15
+ const throwNewErrorPattern = /\bthrow new Error\s*\(/;
16
+ const consolePattern = /\bconsole\.(?:log|warn|error|info)\s*\(/;
17
+
18
+ const consoleAllowlist = [
19
+ /^packages\/config\/src\/validate-env\.js$/,
20
+ /^packages\/core\/src\/testing\//,
21
+ /^packages\/cli\/src\//,
22
+ /^packages\/cli\/templates\/config\/src\/validate-env\.js$/,
23
+ /^packages\/cli\/templates\/base-project\/packages\/config\/src\/validate-env\.js$/,
24
+ /^packages\/admin\/src\/introspect\.js$/,
25
+ /^packages\/cli\/templates\/admin\/src\/introspect\.js$/,
26
+ ];
27
+
28
+ const typeScriptAllowlist = [
29
+ /\/src\/generated\//,
30
+ /^apps\/mobile\//,
31
+ /^packages\/cli\/templates\/mobile\//,
32
+ /^packages\/cli\/templates\/.*\.railway\/railway\.ts$/,
33
+ /^\.railway\/railway\.ts$/,
34
+ ];
35
+
36
+ function normalizePath(filePath) {
37
+ return path.relative(rootDir, filePath).split(path.sep).join("/");
38
+ }
39
+
40
+ function isTestFile(relativePath) {
41
+ return /\.test\.[jt]sx?$/.test(relativePath);
42
+ }
43
+
44
+ function isGeneratedPath(relativePath) {
45
+ return typeScriptAllowlist.some((pattern) => pattern.test(relativePath));
46
+ }
47
+
48
+ function isRuntimeAppPath(relativePath) {
49
+ return (
50
+ /^apps\/.*\/src\/.*\.(?:js|jsx|mjs)$/.test(relativePath) ||
51
+ /^packages\/cli\/templates\/worker\/src\/.*\.(?:js|jsx|mjs)$/.test(
52
+ relativePath,
53
+ ) ||
54
+ /^packages\/cli\/templates\/(?:admin-routes|cms-routes)\/.*\.(?:js|jsx|mjs)$/.test(
55
+ relativePath,
56
+ ) ||
57
+ /^packages\/cli\/templates\/base-project\/apps\/.*\/src\/.*\.(?:js|jsx|mjs)$/.test(
58
+ relativePath,
59
+ )
60
+ );
61
+ }
62
+
63
+ function isSourceFile(relativePath) {
64
+ return (
65
+ /^apps\/.*\/src\/.*\.(?:js|jsx|mjs)$/.test(relativePath) ||
66
+ /^packages\/.*\/src\/.*\.(?:js|jsx|mjs)$/.test(relativePath) ||
67
+ /^packages\/cli\/templates\/(?:worker|admin|cms|config|jobs|ui)\/src\/.*\.(?:js|jsx|mjs)$/.test(
68
+ relativePath,
69
+ ) ||
70
+ /^packages\/cli\/templates\/(?:admin-routes|cms-routes)\/.*\.(?:js|jsx|mjs)$/.test(
71
+ relativePath,
72
+ ) ||
73
+ /^packages\/cli\/templates\/base-project\/apps\/.*\/src\/.*\.(?:js|jsx|mjs)$/.test(
74
+ relativePath,
75
+ ) ||
76
+ /^packages\/cli\/templates\/base-project\/packages\/.*\/src\/.*\.(?:js|jsx|mjs)$/.test(
77
+ relativePath,
78
+ )
79
+ );
80
+ }
81
+
82
+ function isConsoleAllowed(relativePath) {
83
+ return consoleAllowlist.some((pattern) => pattern.test(relativePath));
84
+ }
85
+
86
+ function findMatchingLines(content, pattern) {
87
+ const matches = [];
88
+ for (const [index, line] of content.split("\n").entries()) {
89
+ if (pattern.test(line)) {
90
+ matches.push(index + 1);
91
+ }
92
+ }
93
+ return matches;
94
+ }
95
+
96
+ async function collectFiles(directoryPath) {
97
+ const entries = await readdir(directoryPath, { withFileTypes: true });
98
+ const files = [];
99
+
100
+ for (const entry of entries) {
101
+ if (ignoreDirNames.has(entry.name)) {
102
+ continue;
103
+ }
104
+
105
+ const entryPath = path.join(directoryPath, entry.name);
106
+
107
+ if (entry.isDirectory()) {
108
+ files.push(...(await collectFiles(entryPath)));
109
+ continue;
110
+ }
111
+
112
+ files.push(entryPath);
113
+ }
114
+
115
+ return files;
116
+ }
117
+
118
+ async function main() {
119
+ const allFiles = [];
120
+
121
+ for (const relativeRoot of rootsToScan) {
122
+ const absoluteRoot = path.join(rootDir, relativeRoot);
123
+ try {
124
+ allFiles.push(...(await collectFiles(absoluteRoot)));
125
+ } catch (error) {
126
+ if (error.code !== "ENOENT") {
127
+ throw error;
128
+ }
129
+ }
130
+ }
131
+
132
+ const violations = [];
133
+
134
+ for (const filePath of allFiles) {
135
+ const relativePath = normalizePath(filePath);
136
+ const extension = path.extname(relativePath);
137
+
138
+ if (tsExtensions.has(extension) && !isGeneratedPath(relativePath)) {
139
+ violations.push(
140
+ `${relativePath}: TypeScript files are not allowed outside generated code.`,
141
+ );
142
+ continue;
143
+ }
144
+
145
+ if (!jsExtensions.has(extension) || isGeneratedPath(relativePath)) {
146
+ continue;
147
+ }
148
+
149
+ const content = await readFile(filePath, "utf8");
150
+
151
+ if (isRuntimeAppPath(relativePath) && !isTestFile(relativePath)) {
152
+ for (const lineNumber of findMatchingLines(
153
+ content,
154
+ throwNewErrorPattern,
155
+ )) {
156
+ violations.push(
157
+ `${relativePath}:${lineNumber}: Use AppError/ValidationError in app runtime code instead of throw new Error().`,
158
+ );
159
+ }
160
+ }
161
+
162
+ if (
163
+ isSourceFile(relativePath) &&
164
+ !isTestFile(relativePath) &&
165
+ !isConsoleAllowed(relativePath)
166
+ ) {
167
+ for (const lineNumber of findMatchingLines(content, consolePattern)) {
168
+ violations.push(
169
+ `${relativePath}:${lineNumber}: Use createLogger() instead of console.* outside CLI/bootstrap allowlists.`,
170
+ );
171
+ }
172
+ }
173
+ }
174
+
175
+ if (violations.length > 0) {
176
+ console.error("Standards check failed:\n");
177
+ for (const violation of violations) {
178
+ console.error(`- ${violation}`);
179
+ }
180
+ process.exit(1);
181
+ }
182
+
183
+ console.log("Standards check passed.");
184
+ }
185
+
186
+ await main();