From b16652a41baff4c18693b6819ef26c6eedf7a2f4 Mon Sep 17 00:00:00 2001 From: Parth Patidar Date: Mon, 11 May 2026 03:10:29 +0530 Subject: [PATCH 1/6] build analytics dashboard page at /devcard/analytics --- .../routes/devcard/analytics/+page.server.ts | 34 ++ .../src/routes/devcard/analytics/+page.svelte | 409 ++++++++++++++++++ 2 files changed, 443 insertions(+) create mode 100644 apps/web/src/routes/devcard/analytics/+page.server.ts create mode 100644 apps/web/src/routes/devcard/analytics/+page.svelte diff --git a/apps/web/src/routes/devcard/analytics/+page.server.ts b/apps/web/src/routes/devcard/analytics/+page.server.ts new file mode 100644 index 0000000..6fa597a --- /dev/null +++ b/apps/web/src/routes/devcard/analytics/+page.server.ts @@ -0,0 +1,34 @@ +import type { PageServerLoad } from './$types'; + +const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000'; + +export const load: PageServerLoad = async ({ fetch }) => { + try { + const [overviewRes, viewsRes] = await Promise.all([ + fetch(`${API_BASE}/api/analytics/overview`), + fetch(`${API_BASE}/api/analytics/views`) + ]); + + if (!overviewRes.ok || !viewsRes.ok) { + // If unauthorized, we might return mock data for demonstration purposes + // in this dev phase, but officially we should return error. + // Let's return error to follow "nothing that can break in production" + return { + overview: null, + views: null, + error: 'Please log in to view analytics' + }; + } + + const overview = await overviewRes.json(); + const views = await viewsRes.json(); + + return { overview, views, error: null }; + } catch (err) { + return { + overview: null, + views: null, + error: 'Analytics service unavailable' + }; + } +}; diff --git a/apps/web/src/routes/devcard/analytics/+page.svelte b/apps/web/src/routes/devcard/analytics/+page.svelte new file mode 100644 index 0000000..e9a4616 --- /dev/null +++ b/apps/web/src/routes/devcard/analytics/+page.svelte @@ -0,0 +1,409 @@ + + + + Analytics Dashboard — DevCard + + +
+
+
+

Analytics Dashboard

+

Track your DevCard performance and reach.

+
+ +
+ + {#if error} +
+
🔒
+

{error}

+

Accessing the dashboard requires an active session.

+ Return Home +
+ {:else if overview} + +
+
+ Total Views +
{overview.totalViews}
+ +
+
+ Views Today +
{overview.viewsToday}
+ +
+
+ Unique Viewers +
{overview.uniqueViewers}
+ +
+
+ Total Follows +
{overview.totalFollows}
+ +
+
+ +
+ +
+

Recent Activity

+
+ {#each overview.recentViews as view} +
+
+ {#if view.viewer?.avatarUrl} + + {:else} +
{view.viewer?.displayName?.charAt(0) || '?'}
+ {/if} +
+
+ {view.viewer?.displayName || 'Anonymous User'} + viewed via {view.source} +
+
{formatDate(view.createdAt)}
+
+ {/each} + {#if overview.recentViews.length === 0} +

No recent activity found.

+ {/if} +
+
+ + +
+
+

Detailed View Logs

+ {views?.meta?.total || 0} Total +
+
+ + + + + + + + + + + + {#each views?.data || [] as view} + + + + + + + + {/each} + +
ViewerCardSourceIP AddressDate
+
+ {view.viewer?.displayName || 'Guest'} + {#if view.viewer?.username} + @{view.viewer.username} + {/if} +
+
{view.card?.title || 'Profile'}{view.source}{view.viewerIp || '—'}{formatDate(view.createdAt)}
+ {#if !views?.data?.length} +
No detailed logs available yet.
+ {/if} +
+
+
+ {/if} +
+ + From 8b6fb19ff61c2952192c7e87ff300cb14c494f01 Mon Sep 17 00:00:00 2001 From: Parth Patidar Date: Sun, 17 May 2026 16:31:14 +0530 Subject: [PATCH 2/6] Fix database connection and add dev login bypass for analytics --- apps/backend/src/app.ts | 3 +++ apps/backend/src/routes/auth.ts | 22 +++++++++++++++++++ apps/web/postcss.config.js | 3 +++ .../routes/devcard/analytics/+page.server.ts | 8 ++++--- 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 apps/web/postcss.config.js diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index dc023a2..a414244 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -60,6 +60,9 @@ export async function buildApp() { // ─── Auth Decorator ─── app.decorate('authenticate', async function (request: any, reply: any) { try { + if (!request.headers.authorization && request.cookies && request.cookies.token) { + request.headers.authorization = `Bearer ${request.cookies.token}`; + } await request.jwtVerify(); } catch (err) { reply.status(401).send({ error: 'Unauthorized' }); diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index e12f10a..cd2a897 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -284,6 +284,28 @@ export async function authRoutes(app: FastifyInstance) { reply.clearCookie('token', { path: '/' }); return { message: 'Logged out' }; }); + + // ─── Dev Login Bypass ─── + app.get('/dev-login', async (request: FastifyRequest, reply: FastifyReply) => { + const user = await app.prisma.user.findUnique({ + where: { username: 'devcard-demo' }, + }); + if (!user) { + return reply.status(404).send({ error: 'Demo user not found' }); + } + const token = app.jwt.sign( + { id: user.id, username: user.username }, + { expiresIn: '30d' } + ); + reply.setCookie('token', token, { + httpOnly: true, + secure: false, + sameSite: 'lax', + path: '/', + maxAge: 30 * 24 * 60 * 60, + }); + return reply.redirect(`${process.env.PUBLIC_APP_URL || 'http://localhost:5173'}/devcard/analytics`); + }); } function generateState(): string { diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js new file mode 100644 index 0000000..5bacb78 --- /dev/null +++ b/apps/web/postcss.config.js @@ -0,0 +1,3 @@ +export default { + plugins: {} +}; diff --git a/apps/web/src/routes/devcard/analytics/+page.server.ts b/apps/web/src/routes/devcard/analytics/+page.server.ts index 6fa597a..4f2dbc4 100644 --- a/apps/web/src/routes/devcard/analytics/+page.server.ts +++ b/apps/web/src/routes/devcard/analytics/+page.server.ts @@ -2,11 +2,13 @@ import type { PageServerLoad } from './$types'; const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000'; -export const load: PageServerLoad = async ({ fetch }) => { +export const load: PageServerLoad = async ({ fetch, request }) => { try { + const cookie = request.headers.get('cookie') || ''; + const headers = { cookie }; const [overviewRes, viewsRes] = await Promise.all([ - fetch(`${API_BASE}/api/analytics/overview`), - fetch(`${API_BASE}/api/analytics/views`) + fetch(`${API_BASE}/api/analytics/overview`, { headers }), + fetch(`${API_BASE}/api/analytics/views`, { headers }) ]); if (!overviewRes.ok || !viewsRes.ok) { From ceb2c7746f4e53644c6eeb451b8ac7d31720ec0c Mon Sep 17 00:00:00 2001 From: Parth Patidar Date: Sun, 17 May 2026 16:35:32 +0530 Subject: [PATCH 3/6] Add dev login bypass button to frontend analytics lock screen --- apps/web/src/routes/devcard/analytics/+page.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/routes/devcard/analytics/+page.svelte b/apps/web/src/routes/devcard/analytics/+page.svelte index e9a4616..ce736ff 100644 --- a/apps/web/src/routes/devcard/analytics/+page.svelte +++ b/apps/web/src/routes/devcard/analytics/+page.svelte @@ -35,7 +35,10 @@
🔒

{error}

Accessing the dashboard requires an active session.

- Return Home +
+ Return Home + Dev Login Bypass +
{:else if overview} From 9a8b89b92f3253df756f2345ddca75f7c43b0964 Mon Sep 17 00:00:00 2001 From: Parth Patidar Date: Sun, 17 May 2026 16:46:52 +0530 Subject: [PATCH 4/6] fix backend typescript compilation and logging errors --- apps/backend/src/app.ts | 7 +++++++ apps/backend/src/routes/auth.ts | 4 ++-- apps/backend/src/routes/connect.ts | 7 ++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index a414244..65f38d3 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -8,6 +8,13 @@ import fastifyStatic from '@fastify/static'; import path from 'path'; import { fileURLToPath } from 'url'; +declare module 'fastify' { + interface FastifyInstance { + authenticate: any; + } +} + + import { prismaPlugin } from './plugins/prisma.js'; import { redisPlugin } from './plugins/redis.js'; import { authRoutes } from './routes/auth.js'; diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index cd2a897..7d8eb1e 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -134,7 +134,7 @@ export async function authRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`); } catch (err) { - app.log.error('GitHub auth error:', err); + app.log.error(err as any, 'GitHub auth error'); return reply.status(500).send({ error: 'Authentication failed' }); } }); @@ -235,7 +235,7 @@ export async function authRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`); } catch (err) { - app.log.error('Google auth error:', err); + app.log.error(err as any, 'Google auth error'); return reply.status(500).send({ error: 'Authentication failed' }); } }); diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index 88db14c..268f282 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -1,4 +1,5 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { encrypt } from '../utils/encryption.js'; const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; @@ -81,12 +82,12 @@ export async function connectRoutes(app: FastifyInstance) { const tokenData = (await tokenRes.json()) as any; if (tokenData.error) { - app.log.error('GitHub connect token error:', tokenData); + app.log.error(tokenData, 'GitHub connect token error'); return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=connect_failed`); } // Encrypt and store the token - const encryptedToken = app.encryption.encrypt(tokenData.access_token); + const encryptedToken = encrypt(tokenData.access_token); await app.prisma.oAuthToken.upsert({ where: { @@ -116,7 +117,7 @@ export async function connectRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?connected=github`); } catch (err) { - app.log.error('GitHub connect error:', err); + app.log.error(err as any, 'GitHub connect error'); return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=server_error`); } }); From e043b581f9383577ce630496d79aa20b39e73342 Mon Sep 17 00:00:00 2001 From: Parth Patidar Date: Sun, 17 May 2026 16:55:25 +0530 Subject: [PATCH 5/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Parth Patidar --- apps/web/postcss.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js index 5bacb78..8c589bb 100644 --- a/apps/web/postcss.config.js +++ b/apps/web/postcss.config.js @@ -1,3 +1,5 @@ export default { - plugins: {} + plugins: { + autoprefixer: {} + } }; From 299119cb5c7a424382710580ac5c746cd379b099 Mon Sep 17 00:00:00 2001 From: Parth Patidar Date: Sun, 17 May 2026 16:58:46 +0530 Subject: [PATCH 6/6] Implement daily views interactive charts and secure dev login analytics backend --- apps/backend/src/routes/auth.ts | 40 +- apps/web/postcss.config.js | 6 +- .../routes/devcard/analytics/+page.server.ts | 85 ++++- .../src/routes/devcard/analytics/+page.svelte | 347 +++++++++++++++++- 4 files changed, 435 insertions(+), 43 deletions(-) diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index 7d8eb1e..da47fcb 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -286,26 +286,28 @@ export async function authRoutes(app: FastifyInstance) { }); // ─── Dev Login Bypass ─── - app.get('/dev-login', async (request: FastifyRequest, reply: FastifyReply) => { - const user = await app.prisma.user.findUnique({ - where: { username: 'devcard-demo' }, - }); - if (!user) { - return reply.status(404).send({ error: 'Demo user not found' }); - } - const token = app.jwt.sign( - { id: user.id, username: user.username }, - { expiresIn: '30d' } - ); - reply.setCookie('token', token, { - httpOnly: true, - secure: false, - sameSite: 'lax', - path: '/', - maxAge: 30 * 24 * 60 * 60, + if (process.env.NODE_ENV !== 'production') { + app.get('/dev-login', async (request: FastifyRequest, reply: FastifyReply) => { + const user = await app.prisma.user.findUnique({ + where: { username: 'devcard-demo' }, + }); + if (!user) { + return reply.status(404).send({ error: 'Demo user not found' }); + } + const token = app.jwt.sign( + { id: user.id, username: user.username }, + { expiresIn: '30d' } + ); + reply.setCookie('token', token, { + httpOnly: true, + secure: false, + sameSite: 'lax', + path: '/', + maxAge: 30 * 24 * 60 * 60, + }); + return reply.redirect(`${process.env.PUBLIC_APP_URL || 'http://localhost:5173'}/devcard/analytics`); }); - return reply.redirect(`${process.env.PUBLIC_APP_URL || 'http://localhost:5173'}/devcard/analytics`); - }); + } } function generateState(): string { diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js index 8c589bb..8c0f51b 100644 --- a/apps/web/postcss.config.js +++ b/apps/web/postcss.config.js @@ -1,5 +1,5 @@ export default { - plugins: { - autoprefixer: {} - } + plugins: { + autoprefixer: {} + } }; diff --git a/apps/web/src/routes/devcard/analytics/+page.server.ts b/apps/web/src/routes/devcard/analytics/+page.server.ts index 4f2dbc4..8898e1b 100644 --- a/apps/web/src/routes/devcard/analytics/+page.server.ts +++ b/apps/web/src/routes/devcard/analytics/+page.server.ts @@ -1,36 +1,89 @@ import type { PageServerLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; +import { dev } from '$app/environment'; const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000'; -export const load: PageServerLoad = async ({ fetch, request }) => { +export interface AnalyticsOverview { + totalViews: number; + viewsToday: number; + uniqueViewers: number; + totalFollows: number; + recentViews: Array<{ + createdAt: string; + source: string; + viewer?: { + avatarUrl?: string; + displayName?: string; + }; + }>; +} + +export interface AnalyticsViews { + meta: { + total: number; + }; + data: Array<{ + createdAt: string; + source: string; + viewerIp?: string; + viewer?: { + displayName?: string; + username?: string; + }; + card?: { + title?: string; + }; + }>; +} + +export const load: PageServerLoad = async ({ fetch, cookies }) => { + const token = cookies.get('token'); + + if (!token) { + if (!dev) { + throw redirect(302, '/'); + } + return { + overview: null, + views: null, + error: 'Please log in to view analytics' + }; + } + try { - const cookie = request.headers.get('cookie') || ''; - const headers = { cookie }; + const headers = { Authorization: `Bearer ${token}` }; const [overviewRes, viewsRes] = await Promise.all([ fetch(`${API_BASE}/api/analytics/overview`, { headers }), fetch(`${API_BASE}/api/analytics/views`, { headers }) ]); if (!overviewRes.ok || !viewsRes.ok) { - // If unauthorized, we might return mock data for demonstration purposes - // in this dev phase, but officially we should return error. - // Let's return error to follow "nothing that can break in production" - return { - overview: null, - views: null, - error: 'Please log in to view analytics' + if (overviewRes.status === 401 || viewsRes.status === 401) { + if (!dev) { + throw redirect(302, '/'); + } + } + return { + overview: null, + views: null, + error: 'Please log in to view analytics' }; } - const overview = await overviewRes.json(); - const views = await viewsRes.json(); + const overview = (await overviewRes.json()) as AnalyticsOverview; + const views = (await viewsRes.json()) as AnalyticsViews; return { overview, views, error: null }; } catch (err) { - return { - overview: null, - views: null, - error: 'Analytics service unavailable' + // If it's a redirect thrown by SvelteKit, let it bubble up + if (err && typeof err === 'object' && 'status' in err && 'location' in err) { + throw err; + } + return { + overview: null, + views: null, + error: 'Analytics service unavailable' }; } }; diff --git a/apps/web/src/routes/devcard/analytics/+page.svelte b/apps/web/src/routes/devcard/analytics/+page.svelte index ce736ff..f530845 100644 --- a/apps/web/src/routes/devcard/analytics/+page.svelte +++ b/apps/web/src/routes/devcard/analytics/+page.svelte @@ -1,5 +1,6 @@ @@ -37,7 +124,9 @@

Accessing the dashboard requires an active session.

{:else if overview} @@ -65,6 +154,87 @@ + +
+
+
+

Daily Views (Last 7 Days)

+ Interactive +
+
+ {#if dailyViewsData.length > 0} + + + + + + + + + + + + + + + {#if areaPath} + + {/if} + + + {#if linePath} + + {/if} + + + {#each chartPoints as pt} + + + + {pt.count} + + + {/each} + + + {#each chartPoints as pt} + + {pt.label} + + {/each} + + {:else} +
No view statistics available yet.
+ {/if} +
+
+ +
+
+

Platform Click Rankings

+ Real-time +
+
+ {#each platformClicks as item, index} +
+
+ + #{index + 1} {item.platform} + + {item.count} clicks +
+
+
+
+
+ {/each} + {#if platformClicks.length === 0} +

No platform click details available yet.

+ {/if} +
+
+
+
@@ -73,7 +243,7 @@ {#each overview.recentViews as view}
- {#if view.viewer?.avatarUrl} + {#if isValidAvatar(view.viewer?.avatarUrl)} {:else}
{view.viewer?.displayName?.charAt(0) || '?'}
@@ -94,9 +264,17 @@
-
-

Detailed View Logs

- {views?.meta?.total || 0} Total +
+
+

Detailed View Logs

+ {views?.meta?.total || 0} Total +
+
@@ -398,7 +576,166 @@ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); } + /* Insights Row Grid */ + .insights-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-bottom: 3rem; + } + + .chart-block, .platform-block { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.5rem; + transition: border-color 0.2s; + } + + .chart-block:hover, .platform-block:hover { + border-color: var(--primary-light); + } + + .chart-container { + width: 100%; + padding-top: 1rem; + } + + .chart-svg { + width: 100%; + height: auto; + overflow: visible; + } + + .chart-point { + transition: r 0.2s ease, stroke-width 0.2s ease; + cursor: pointer; + } + + .chart-point-group:hover .chart-point { + r: 7; + stroke-width: 4px; + } + + .chart-value { + opacity: 0; + transition: opacity 0.2s ease, transform 0.2s ease; + pointer-events: none; + } + + .chart-point-group:hover .chart-value { + opacity: 1; + } + + /* Platform Progress Clicks */ + .platform-list { + display: flex; + flex-direction: column; + gap: 1.25rem; + margin-top: 0.5rem; + } + + .platform-row { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .platform-meta { + display: flex; + justify-content: space-between; + font-size: 0.9rem; + align-items: center; + } + + .platform-name { + font-weight: 700; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.5rem; + } + + .rank { + color: var(--primary); + font-size: 0.75rem; + font-weight: 800; + background: rgba(99, 102, 241, 0.1); + padding: 0.15rem 0.4rem; + border-radius: 4px; + } + + .platform-count { + font-weight: 600; + color: var(--text-muted); + } + + .progress-bar-container { + width: 100%; + height: 8px; + background: var(--bg-secondary); + border-radius: 4px; + overflow: hidden; + } + + .progress-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 1s cubic-bezier(0.4, 0, 0.2, 1); + } + + /* CSV Export Button */ + .btn-csv-download { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border); + padding: 0.5rem 1rem; + border-radius: var(--radius); + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + } + + .btn-csv-download:hover { + background: var(--primary); + color: white; + border-color: var(--primary); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2); + } + + .csv-icon { + transition: transform 0.2s; + } + + .btn-csv-download:hover .csv-icon { + transform: translateY(1px); + } + + .badge.secondary { + background: rgba(99, 102, 241, 0.1); + color: var(--primary); + border: 1px solid rgba(99, 102, 241, 0.2); + } + + .title-meta { + display: flex; + align-items: center; + gap: 1rem; + } + + .table-header { + margin-bottom: 1.5rem; + } + @media (max-width: 1024px) { + .insights-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } .dashboard-grid { grid-template-columns: 1fr; }