Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ NEXT_PUBLIC_API_URL=https://nodes-api-dev.desci.com
# Allows testing local env with a prod API, but carries some security risk due to loosened cookie security settings, should only be enabled in dev mode.
NEXT_PUBLIC_LOCAL_AUTH_COOKIE=true

DESCI_RESEARCH_CONNECTION_DATABASE_URL=""

NEXT_PUBLIC_BASE_URL=http://localhost:3000

SCIWEAVE_SUPABASE_URL=
SCIWEAVE_SUPABASE_SERVICE_KEY=
SCIWEAVE_SUPABASE_SERVICE_KEY=

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if .env.example ends with a newline
if [ -s .env.example ]; then
  tail -c 1 .env.example | od -An -tc | grep -q '\n' && echo "✓ File ends with newline" || echo "✗ Missing trailing newline"
fi

Repository: desci-labs/desci-admin

Length of output: 150


🏁 Script executed:

cat -n .env.example | tail -5

Repository: desci-labs/desci-admin

Length of output: 199


Add a trailing newline at the end of the file.

The file still lacks a newline after line 21. Add a single blank line to the end of .env.example.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 21-21: [UnorderedKey] The SCIWEAVE_SUPABASE_SERVICE_KEY key should go before the SCIWEAVE_SUPABASE_URL key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example at line 21, The file `.env.example` ends with the variable
declaration SCIWEAVE_SUPABASE_SERVICE_KEY= but is missing a trailing newline;
update the file to ensure there is a single blank line (newline character) after
that line so the file ends with a newline.

1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.15
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# DeSci Nodes Admin v2

This repo is for the next version of the [DeSci Nodes Web v1 web app](https://github.com/desci-labs/nodes-web).

## Getting Started

```bash
npm install
npm run dev
```
yarn
yarn dev
```
37 changes: 27 additions & 10 deletions src/app/(auth)/metrics/sciweave/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,34 @@ async function getUserSessionsAnalytics(
params.set("to", to);
params.set("interval", interval);

const res = await fetch(
`${
process.env.NEXT_PUBLIC_BASE_URL
}/api/sciweave-analytics/sessions?${params.toString()}`,
{
next: {
revalidate: 3600,
},
try {
const res = await fetch(
`${
process.env.NEXT_PUBLIC_BASE_URL
}/api/sciweave-analytics/sessions?${params.toString()}`,
{
next: {
revalidate: 3600,
},
}
);

if (!res.ok) {
console.error('Failed to fetch user sessions:', res.status, res.statusText);
return [];
}
);
return res.json() as Promise<UserSessionsDataItem[]>;

const data = await res.json();
if (!Array.isArray(data)) {
console.error('Expected array from user sessions API, got:', data);
return [];
}

return data as UserSessionsDataItem[];
} catch (error) {
console.error('Error fetching user sessions:', error);
return [];
}
}

async function getDevicesAnalytics(from: string, to: string, interval: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/(unauth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function Login() {
login={formAction}
email={state?.email}
message={state?.error}
disabled={state?.user !== undefined}
disabled={!!state?.user}
/>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export async function login(prevState: any, formData: FormData) {
// Set cookie
cookies().set(AUTH_COOKIE_FIELDNAME, response.user.token, {
path: "/",
expires: new Date(Date.now() + 1000 * 60 * 60 * 2), // 3 hours
expires: new Date(Date.now() + 1000 * 60 * 60 * 2), // 2 hours
httpOnly: true,
secure: process.env.NEXT_ENV === "production",
domain: process.env.NODE_ENV === "production" ? ".desci.com" : undefined,
domain: process.env.NEXT_ENV === "production" ? ".desci.com" : undefined,
});

if (
Expand All @@ -43,7 +43,7 @@ export async function login(prevState: any, formData: FormData) {
console.log("[cookie]", AUTH_COOKIE_FIELDNAME, process.env.NEXT_ENV);
cookies().set("auth", response.user.token, {
path: "/",
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 1), // 2 hours
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
httpOnly: true,
secure: true,
// domain: '.desci.com',
Expand Down
24 changes: 23 additions & 1 deletion src/app/api/download/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NODES_API_URL } from "@/lib/config";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export const maxDuration = 300;

export async function GET(_request: Request) {
try {
const response = await fetch(
Expand All @@ -14,7 +16,27 @@ export async function GET(_request: Request) {
},
}
);
return response

if (!response.ok) {
return NextResponse.json(
{ error: "Failed to fetch report" },
{ status: response.status }
);
}

if (!response.body) {
return NextResponse.json(
{ error: "Missing response body" },
{ status: 502 }
);
}

return new Response(response.body, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": 'attachment; filename="report.csv"',
},
});
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 });
}
Comment on lines 40 to 42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same error serialization issue as in downloadAnalyticsReport/route.ts.

Error objects serialize to {}. Extract err.message instead (see comment in the other file for proposed fix).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/download/route.ts` around lines 33 - 35, The catch block in the
download route returns the raw caught error object which serializes to {} (see
the catch in download/route.ts); update the catch to extract a serializable
message instead—use the caught variable (err) and pass err instanceof Error ?
err.message : String(err) (or similar) into NextResponse.json({ error: ... }, {
status: 500 }) so the response includes the actual error text; locate the catch
surrounding NextResponse.json in download/route.ts and replace the returned
error payload accordingly.

Expand Down
24 changes: 23 additions & 1 deletion src/app/api/downloadAnalyticsReport/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NODES_API_URL } from "@/lib/config";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export const maxDuration = 300;

export async function GET(request: Request) {
try {
const url = new URL(request.url);
Expand All @@ -15,7 +17,27 @@ export async function GET(request: Request) {
},
}
);
return response

if (!response.ok) {
return NextResponse.json(
{ error: "Failed to fetch report" },
{ status: response.status }
);
}

if (!response.body) {
return NextResponse.json(
{ error: "Missing response body" },
{ status: 502 }
);
}

return new Response(response.body, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": 'attachment; filename="report.csv"',
},
});
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 });
}
Comment on lines 41 to 43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error objects don't serialize to JSON properly.

Error instances serialize to {} when passed to JSON.stringify. This will produce an unhelpful response like { "error": {} }.

Proposed fix
   } catch (err) {
-    return NextResponse.json({ error: err }, { status: 500 });
+    return NextResponse.json(
+      { error: err instanceof Error ? err.message : "Internal server error" },
+      { status: 500 }
+    );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 });
}
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "Internal server error" },
{ status: 500 }
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/downloadAnalyticsReport/route.ts` around lines 34 - 36, The catch
block in the downloadAnalyticsReport route currently returns the raw Error
instance (err) which serializes to an empty object; change the error payload to
a serializable object (e.g., { name: err.name, message: err.message, stack:
err.stack }) and return that via NextResponse.json instead of err so consumers
receive useful error details; update the catch in route.ts (the handler that
uses NextResponse.json) to build and return this serializable error object.

Expand Down
20 changes: 19 additions & 1 deletion src/app/api/sciweave-analytics/chats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import pool from "@/lib/postgresClient";
import { PROD_FILTER_AND } from "@/lib/config";
import { analyticsQuerySchema, intervalToDateTrunc } from "@/lib/schema";

export async function GET(request: NextRequest) {
async function handleRequest(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const { from, to, interval } = analyticsQuerySchema.parse(
Expand Down Expand Up @@ -42,3 +42,21 @@ export async function GET(request: NextRequest) {
);
}
}

export async function GET(request: NextRequest) {
return handleRequest(request);
}

export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url);
const isGet = searchParams.get("get") === "true";

if (isGet) {
return handleRequest(request);
}

return NextResponse.json(
{ error: "POST method requires ?get=true parameter for replica compatibility" },
{ status: 400 }
);
}
66 changes: 26 additions & 40 deletions src/app/api/sciweave-analytics/devices/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import pool from "@/lib/postgresClient";
import { PROD_FILTER_AND } from "@/lib/config";
import { analyticsQuerySchema, intervalToDateTrunc } from "@/lib/schema";

export async function GET(request: NextRequest) {
async function handleRequest(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const { from, to, interval } = analyticsQuerySchema.parse(
Expand All @@ -14,45 +14,13 @@ export async function GET(request: NextRequest) {
const result = await client.query(
`
SELECT
date_trunc($3, created_at) AS DATE,
COUNT(
CASE
WHEN (device_info ->> 'isMobile') :: boolean THEN 1
END
) AS "mobile",
COUNT(
CASE
WHEN (device_info ->> 'isTablet') :: boolean THEN 1
END
) AS "tablet",
COUNT(
CASE
WHEN (device_info ->> 'isDesktop') :: boolean
AND device_info ->> 'osName' ILIKE 'mac%' THEN 1
END
) AS "mac",
COUNT(
CASE
WHEN (device_info ->> 'isDesktop') :: boolean
AND device_info ->> 'osName' ILIKE 'windows%' THEN 1
END
) AS "windows",
COUNT(
CASE
WHEN (device_info ->> 'isDesktop') :: boolean
AND device_info ->> 'osName' NOT ILIKE 'mac%'
AND device_info ->> 'osName' NOT ILIKE 'windows%' THEN 1
END
) AS "otherDesktops",
COUNT(
CASE
WHEN NOT (
(device_info ->> 'isMobile') :: boolean
OR (device_info ->> 'isTablet') :: boolean
OR (device_info ->> 'isDesktop') :: boolean
) THEN 1
END
) AS unknown
date_trunc($3, created_at) AS DATE,
SUM(((device_info->>'isMobile')::boolean)::int) AS "mobile",
SUM(((device_info->>'isTablet')::boolean)::int) AS "tablet",
SUM(((device_info->>'isDesktop')::boolean AND device_info->>'osName' ILIKE 'mac%')::int) AS "mac",
SUM(((device_info->>'isDesktop')::boolean AND device_info->>'osName' ILIKE 'windows%')::int) AS "windows",
SUM(((device_info->>'isDesktop')::boolean AND device_info->>'osName' NOT ILIKE 'mac%' AND device_info->>'osName' NOT ILIKE 'windows%')::int) AS "otherDesktops",
SUM((NOT((device_info->>'isMobile')::boolean OR (device_info->>'isTablet')::boolean OR (device_info->>'isDesktop')::boolean))::int) AS "unknown"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
FROM search_logs
WHERE created_at >= $1
AND created_at <= $2
Expand Down Expand Up @@ -82,3 +50,21 @@ export async function GET(request: NextRequest) {
);
}
}

export async function GET(request: NextRequest) {
return handleRequest(request);
}

export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url);
const isGet = searchParams.get("get") === "true";

if (isGet) {
return handleRequest(request);
}

return NextResponse.json(
{ error: "POST method requires ?get=true parameter for replica compatibility" },
{ status: 400 }
);
}
20 changes: 19 additions & 1 deletion src/app/api/sciweave-analytics/sessions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import pool from "@/lib/postgresClient";
import { PROD_FILTER_LEADING } from "@/lib/config";
import { analyticsQuerySchema, intervalToDateTrunc } from "@/lib/schema";

export async function GET(request: NextRequest) {
async function handleRequest(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const { from, to, interval } = analyticsQuerySchema.parse(
Expand Down Expand Up @@ -109,3 +109,21 @@ export async function GET(request: NextRequest) {
);
}
}

export async function GET(request: NextRequest) {
return handleRequest(request);
}

export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url);
const isGet = searchParams.get("get") === "true";

if (isGet) {
return handleRequest(request);
}

return NextResponse.json(
{ error: "POST method requires ?get=true parameter for replica compatibility" },
{ status: 400 }
);
}
20 changes: 19 additions & 1 deletion src/app/api/sciweave-analytics/users/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import pool from "@/lib/postgresClient";
import { PROD_FILTER_AND } from "@/lib/config";
import { analyticsQuerySchema, intervalToDateTrunc } from "@/lib/schema";

export async function GET(request: NextRequest) {
async function handleRequest(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const { from, to, interval } = analyticsQuerySchema.parse(
Expand Down Expand Up @@ -41,3 +41,21 @@ export async function GET(request: NextRequest) {
);
}
}

export async function GET(request: NextRequest) {
return handleRequest(request);
}

export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url);
const isGet = searchParams.get("get") === "true";

if (isGet) {
return handleRequest(request);
}

return NextResponse.json(
{ error: "POST method requires ?get=true parameter for replica compatibility" },
{ status: 400 }
);
}
8 changes: 8 additions & 0 deletions src/components/molecules/DesciResearchAnalytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -581,13 +581,21 @@ export function DesciResearchAnalytics({
}
};
const userSessionsData = useMemo(() => {
if (!Array.isArray(userSessions)) {
console.error('userSessions is not an array:', userSessions);
return [];
}
return userSessions.map((item) => ({
date: formatDate(item.date, "dd MMM"),
value: item.sessionCount,
}));
}, [userSessions]);

const userSessionsDurationData = useMemo(() => {
if (!Array.isArray(userSessions)) {
console.error('userSessions is not an array:', userSessions);
return [];
}
return userSessions.map((item) => ({
date: formatDate(item.date, "dd MMM"),
value: item.durationInSeconds,
Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const SubmitButton = (
if (props.email && pending) return "Verifying code";
if (!props.email && pending) return "Verifying email";
if (!props.email && !pending) return "Verify email";
if (props.email && !pending) return "Send code";
if (props.email && !pending) return "Verify code";
}, [props.email, pending]);
return (
<Button
Expand Down
Loading