@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.
- package/README.md +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- 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();
|