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
12 changes: 10 additions & 2 deletions apps/dashboard/app/activity/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -473,3 +473,11 @@ function DiffRow({ change }: { change: ActivityChange }) {
</div>
);
}

export default function ActivityPage() {
return (
<Suspense fallback={<div>Loading activity...</div>}>
<ActivityPageContent />
</Suspense>
);
}
12 changes: 10 additions & 2 deletions apps/dashboard/app/members/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -596,3 +596,11 @@ function useDebouncedValue<T>(value: T, delayMs: number): T {

return debounced;
}

export default function MembersPage() {
return (
<Suspense fallback={<div>Loading members...</div>}>
<MembersPageContent />
</Suspense>
);
}
5 changes: 2 additions & 3 deletions apps/dashboard/lib/activity/pubsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -106,9 +107,7 @@ class LocalPubSubImpl implements ILocalPubSub {

class PostgresPubSubImpl implements ILocalPubSub {
private listeners = new Map<string, Set<ActivitySubscriber>>();
private pgListenerClient: Awaited<
ReturnType<ReturnType<typeof getPool>["connect"]>
> | null = null;
private pgListenerClient: PoolClient | null = null;
private listenerRefCount = 0;
private connectionError: Error | null = null;

Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/lib/auth/csrf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────────────────────────────────

Expand Down
6 changes: 3 additions & 3 deletions apps/dashboard/lib/repositories/adapters/durable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────────────────────────

Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion apps/dashboard/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
7 changes: 6 additions & 1 deletion packages/integration-client/src/http/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
38 changes: 38 additions & 0 deletions packages/integration-client/src/repositories/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// packages/integration-client/src/repositories/types.ts

/**
* Generic pagination result.
*/
export interface Paginated<T> {
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<Paginated<import('../types.js').Pass>>;
/** Get a single pass by id */
get(id: string): Promise<import('../types.js').Pass | undefined>;
}

export interface GuildRepository {
list(): Promise<Paginated<import('../types.js').Guild>>;
get(id: string): Promise<import('../types.js').Guild | undefined>;
}

export interface MemberRepository {
list(): Promise<Paginated<import('../types.js').Member>>;
get(id: string): Promise<import('../types.js').Member | undefined>;
}

export interface ActivityRepository {
/** Return activity events – same shape as current mock fetchActivity */
list(): Promise<import('../types.js').Activity[]>;
/** Generate a mock activity for testing */
generateMock(): Promise<import('../types.js').Activity>;
}
42 changes: 42 additions & 0 deletions packages/integration-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
4 changes: 2 additions & 2 deletions packages/integration-client/test/circuitBreaker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions packages/integration-client/test/snapshot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ describe("IntegrationClient.getGuildSnapshot", () => {
assert.strictEqual(result, null);
});

test("throws core:<status> 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/);
});
});
3 changes: 2 additions & 1 deletion packages/integration-client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": false
"composite": false,
"ignoreDeprecations": "6.0"
},
"include": ["src"]
}