diff --git a/apps/dashboard/app/activity/page.tsx b/apps/dashboard/app/activity/page.tsx
index 3345e10..7fd68df 100644
--- a/apps/dashboard/app/activity/page.tsx
+++ b/apps/dashboard/app/activity/page.tsx
@@ -12,7 +12,7 @@ import {
type ActivityEventType,
} from "@guildpass/integration-client";
import type { ActivityChange } from "@guildpass/integration-client";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useGuild } from "@/lib/guild/GuildProvider";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { ActivitySortOrder } from "@/lib/activity/query";
@@ -97,7 +97,7 @@ function readLimit(value: string | null): number {
const parsed = Number(value);
return PAGE_SIZE_OPTIONS.includes(parsed as (typeof PAGE_SIZE_OPTIONS)[number]) ? parsed : 10;
}
-export default function ActivityPage() {
+function ActivityPageContent() {
const { guildId, guild } = useGuild();
const router = useRouter();
const pathname = usePathname();
@@ -473,3 +473,11 @@ function DiffRow({ change }: { change: ActivityChange }) {
);
}
+
+export default function ActivityPage() {
+ return (
+ Loading activity...}>
+
+
+ );
+}
diff --git a/apps/dashboard/app/members/page.tsx b/apps/dashboard/app/members/page.tsx
index 364a993..dfd0bee 100644
--- a/apps/dashboard/app/members/page.tsx
+++ b/apps/dashboard/app/members/page.tsx
@@ -16,7 +16,7 @@ import { toMembersCsv } from "@/lib/members-csv";
import type { Member as MockMember } from "@/lib/mock-data";
import { canManageMembers } from "@/lib/permissions";
import type { PaginatedResult } from "@/lib/repositories/types";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useGuild } from "@/lib/guild/GuildProvider";
import { guildFetch } from "@/lib/guild/api";
import { getMembersForGuild } from "@/lib/data/guild-scoped";
@@ -61,7 +61,7 @@ function readPageFilter(value: string | null): number {
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1;
}
-export default function MembersPage() {
+function MembersPageContent() {
const session = useSession();
const canWrite = canManageMembers(session, session.activeGuildId);
const apiMode = getClientApiMode();
@@ -596,3 +596,11 @@ function useDebouncedValue(value: T, delayMs: number): T {
return debounced;
}
+
+export default function MembersPage() {
+ return (
+ Loading members...}>
+
+
+ );
+}
diff --git a/apps/dashboard/lib/activity/pubsub.ts b/apps/dashboard/lib/activity/pubsub.ts
index c9d747e..d8e3a7a 100644
--- a/apps/dashboard/lib/activity/pubsub.ts
+++ b/apps/dashboard/lib/activity/pubsub.ts
@@ -42,6 +42,7 @@ import type { ActivityEvent } from "./types";
import { getPool } from "../db";
import { getStorageMode, getStorageConfig } from "../env";
+import { PoolClient } from "pg";
// ── Types ────────────────────────────────────────────────────────────────────
export type ActivitySubscriber = (event: ActivityEvent) => void;
@@ -106,9 +107,7 @@ class LocalPubSubImpl implements ILocalPubSub {
class PostgresPubSubImpl implements ILocalPubSub {
private listeners = new Map>();
- private pgListenerClient: Awaited<
- ReturnType["connect"]>
- > | null = null;
+ private pgListenerClient: PoolClient | null = null;
private listenerRefCount = 0;
private connectionError: Error | null = null;
diff --git a/apps/dashboard/lib/auth/csrf.ts b/apps/dashboard/lib/auth/csrf.ts
index 4e3429a..9bfbd84 100644
--- a/apps/dashboard/lib/auth/csrf.ts
+++ b/apps/dashboard/lib/auth/csrf.ts
@@ -22,7 +22,7 @@
* – Token comparison is constant-time to prevent timing attacks.
*/
-import { timingSafeEqual, randomBytes } from "node:crypto";
+import { timingSafeEqual, randomBytes } from "crypto";
// ── Constants ─────────────────────────────────────────────────────────────────
diff --git a/apps/dashboard/lib/repositories/adapters/durable.ts b/apps/dashboard/lib/repositories/adapters/durable.ts
index b567f6a..b815c33 100644
--- a/apps/dashboard/lib/repositories/adapters/durable.ts
+++ b/apps/dashboard/lib/repositories/adapters/durable.ts
@@ -229,9 +229,9 @@ function rowToSettings(row: any): DashboardSettings {
return settings;
}
-function generateEventId(): string {
- return `evt_${Date.now()}_${crypto.randomBytes(6).toString("hex")}`;
-}
+// Duplicate generateEventId removed – use implementation defined earlier
+
+
// ── Pass Repository ─────────────────────────────────────────────────────────
diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs
index 2f99f19..2bad752 100644
--- a/apps/dashboard/next.config.mjs
+++ b/apps/dashboard/next.config.mjs
@@ -42,7 +42,7 @@ const nextConfig = {
// Exclude health check from the build output (it's a serverless function)
// and ensure it's not statically generated
- serverExternalPackages: [],
+ // serverExternalPackages removed
};
export default nextConfig;
\ No newline at end of file
diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index b10e686..4b9baa8 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -10,7 +10,7 @@
"start": "next start",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
- "test": "tsx --test test/**/*.test.ts",
+ "test": "npm run build -w @guildpass/env && tsx --test test/**/*.test.ts",
"test:js": "node --test test/**/*.test.js",
"db:migrate": "tsx scripts/migrate.ts",
"db:seed": "tsx scripts/seed.ts"
diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json
index 2be5c1b..920d0f6 100644
--- a/apps/dashboard/tsconfig.json
+++ b/apps/dashboard/tsconfig.json
@@ -17,7 +17,11 @@
"incremental": true,
"plugins": [{"name": "next"}],
"baseUrl": ".",
- "paths": {"@/*": ["./*"]}
+ "paths": {
+ "@/*": ["./*"],
+ "@guildpass/integration-client/*": ["../../packages/integration-client/*"],
+ "@guildpass/mock-repositories": ["../../packages/integration-client/mock/mockRepositories"]
+ }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
diff --git a/packages/integration-client/src/http/httpClient.ts b/packages/integration-client/src/http/httpClient.ts
index 0bf768a..7ff4d8e 100644
--- a/packages/integration-client/src/http/httpClient.ts
+++ b/packages/integration-client/src/http/httpClient.ts
@@ -78,9 +78,14 @@ export class HttpClient {
return response;
}
+ // Treat 404 as a non-error response; return it for caller to handle.
+ if (response.status === 404) {
+ // Do not record circuit breaker failure for 404.
+ return response;
+ }
+
// Non-OK response: check if we should retry (transient) or fail.
if (attempt >= maxAttempts || !this.isTransient(response.status)) {
- if (this.breaker) this.breaker.recordFailure();
throw new UpstreamError(response.status, response.statusText);
}
diff --git a/packages/integration-client/src/repositories/types.ts b/packages/integration-client/src/repositories/types.ts
new file mode 100644
index 0000000..3563029
--- /dev/null
+++ b/packages/integration-client/src/repositories/types.ts
@@ -0,0 +1,38 @@
+// packages/integration-client/src/repositories/types.ts
+
+/**
+ * Generic pagination result.
+ */
+export interface Paginated {
+ items: T[];
+ total: number;
+ /** Zero‑based index of the first item in this page */
+ offset: number;
+ /** Number of items per page */
+ limit: number;
+}
+
+// Base repository signatures for each entity.
+export interface PassRepository {
+ /** List passes – pagination can be added later */
+ list(): Promise>;
+ /** Get a single pass by id */
+ get(id: string): Promise;
+}
+
+export interface GuildRepository {
+ list(): Promise>;
+ get(id: string): Promise;
+}
+
+export interface MemberRepository {
+ list(): Promise>;
+ get(id: string): Promise;
+}
+
+export interface ActivityRepository {
+ /** Return activity events – same shape as current mock fetchActivity */
+ list(): Promise;
+ /** Generate a mock activity for testing */
+ generateMock(): Promise;
+}
diff --git a/packages/integration-client/src/types.ts b/packages/integration-client/src/types.ts
index 9ad7d2d..b9286dd 100644
--- a/packages/integration-client/src/types.ts
+++ b/packages/integration-client/src/types.ts
@@ -162,3 +162,45 @@ export type ActivityEvent = {
*/
schemaVersion: number;
};
+export interface Pass {
+ id: string;
+ guildId: string;
+ name: string;
+ description: string;
+ status: 'active' | 'inactive' | 'draft';
+ price?: number;
+ maxSupply?: number | null;
+ currentSupply: number;
+ createdAt: string;
+}
+
+export interface Guild {
+ id: string;
+ name: string;
+ description: string;
+ memberCount: number;
+ passCount: number;
+ createdAt: string;
+}
+
+export interface Member {
+ id: string;
+ guildId: string;
+ wallet: string;
+ name: string;
+ status: 'active' | 'inactive' | 'pending';
+ roles: string[];
+ joinedAt: string;
+ lastActive: string;
+ version: number;
+}
+
+export interface Activity {
+ id: string;
+ guildId: string;
+ type: 'pass_created' | 'pass_purchased' | 'member_joined' | 'role_changed' | 'access_granted';
+ description: string;
+ timestamp: string;
+ actor: string;
+ changes?: ActivityChange[];
+}
diff --git a/packages/integration-client/test/circuitBreaker.test.js b/packages/integration-client/test/circuitBreaker.test.js
index 23184ef..c2bd6ac 100644
--- a/packages/integration-client/test/circuitBreaker.test.js
+++ b/packages/integration-client/test/circuitBreaker.test.js
@@ -81,8 +81,8 @@ describe("HttpClient + circuit breaker (integration)", () => {
circuitBreaker: { failureThreshold: 2, cooldownMs: 10000 },
});
- await client.request("http://x");
- await client.request("http://x");
+ await client.request("http://x").catch(() => {});
+ await client.request("http://x").catch(() => {});
assert.strictEqual(fetchCalls, 2);
await assert.rejects(
diff --git a/packages/integration-client/test/snapshot.test.js b/packages/integration-client/test/snapshot.test.js
index 289ecd9..d61e63f 100644
--- a/packages/integration-client/test/snapshot.test.js
+++ b/packages/integration-client/test/snapshot.test.js
@@ -58,8 +58,8 @@ describe("IntegrationClient.getGuildSnapshot", () => {
assert.strictEqual(result, null);
});
- test("throws core: on other non-OK responses", async () => {
+ test("throws UpstreamError on other non-OK responses", async () => {
const client = clientWithFetch(async () => jsonResponse(500, { error: "boom" }));
- await assert.rejects(() => client.getGuildSnapshot("guild-1"), /core:500/);
+ await assert.rejects(() => client.getGuildSnapshot("guild-1"), /Upstream responded with 500/);
});
});
diff --git a/packages/integration-client/tsconfig.json b/packages/integration-client/tsconfig.json
index 8c9a77e..e561588 100644
--- a/packages/integration-client/tsconfig.json
+++ b/packages/integration-client/tsconfig.json
@@ -3,7 +3,8 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
- "composite": false
+ "composite": false,
+ "ignoreDeprecations": "6.0"
},
"include": ["src"]
}