Skip to content
Open
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
11 changes: 11 additions & 0 deletions docs/queues/AnalyticsAggregation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# AnalyticsAggregation BullMQ Queue

The `AnalyticsAggregation` queue is responsible for compiling and aggregating raw telemetry and event data into materialized views or time-series rollup tables.

## Architecture

1. **Producer**: A cron-based job triggers the aggregation every hour.
2. **Worker**: The worker queries the raw event logs, aggregates counts and unique user metrics by time bucket (hourly, daily, weekly), and `UPSERT`s them into the `analytics_rollups` table.

## Retention Policy
The raw event logs are kept for 7 days, after which they are pruned by the `DataCleanup` job. The aggregated data is kept indefinitely.
12 changes: 12 additions & 0 deletions docs/queues/LocationHistorySync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# LocationHistorySync BullMQ Queue

The `LocationHistorySync` queue is responsible for flushing user location history from the Redis fast-path into the PostgreSQL database.

## Architecture

1. **Redis Cache**: High-frequency location updates are written to Redis sorted sets (ZSET) by the fast-path WebSocket handlers.
2. **Cron Producer**: A repeating BullMQ job runs every 5 minutes to trigger the sync process.
3. **Worker**: The worker pops the data from Redis, batches it into bulk `INSERT` statements using Drizzle ORM, and stores it in PostGIS.

## Fault Tolerance
If the worker fails, the data remains in Redis and will be picked up on the next cron execution, ensuring zero data loss.
24 changes: 24 additions & 0 deletions docs/queues/NotificationQueue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# NotificationQueue Configuration

The `NotificationQueue` handles sending push notifications and emails to users asynchronously via BullMQ.

## Architecture

1. **Redis**: Used as the backing store for BullMQ.
2. **Worker**: A dedicated worker processes the `NotificationQueue` and interfaces with APNs/FCM or SendGrid.
3. **Producer**: The main Fastify server adds jobs to the queue when specific events occur (e.g. `MESSAGE_RECEIVED`, `ROOM_INVITE`).

## Local Setup

1. Start Redis:
```bash
docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
```
2. Set `REDIS_URL` in your `.env` file:
```env
REDIS_URL=redis://localhost:6379
```
3. Start the worker process:
```bash
npm run worker:start
```
11 changes: 11 additions & 0 deletions docs/queues/ThumbnailGeneration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ThumbnailGeneration BullMQ Queue

The `ThumbnailGeneration` queue is responsible for offloading heavy image processing tasks (such as resizing user avatars and generating preview thumbnails for image attachments) to background workers.

## Architecture

1. **Producer**: Fastify route handles the direct file upload to S3-compatible storage, then pushes a job containing the `objectKey` to the BullMQ queue.
2. **Worker**: The worker downloads the source image, uses `sharp` to generate optimized thumbnails (e.g. 100x100 webp), uploads the derivatives back to storage, and updates the database record.

## Concurrency
To prevent CPU starvation, the ThumbnailGeneration worker is configured with a concurrency limit of `5`.
22 changes: 1 addition & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion packages/client/src/app/(app)/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,29 @@ export default function ProfilePage() {
}
}, [mounted, isAuthenticated, router, user]);

if (!mounted || !user) return null;
if (!mounted || !user) {
return (
<div className="flex-grow flex flex-col w-full animate-pulse">
<div className="w-full bg-[#111827] border-b border-[#27272a] relative">
<div className="h-48 w-full bg-[#1a2235]"></div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative pb-8">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end -mt-16 sm:-mt-20 sm:mb-6 mb-4 gap-4">
<div className="w-32 h-32 rounded-full border-4 border-background bg-zinc-800 shadow-xl"></div>
<div className="h-10 w-32 bg-white/5 rounded-full border border-white/10"></div>
</div>
<div className="max-w-3xl">
<div className="h-10 w-48 bg-white/5 rounded-md mb-2"></div>
<div className="flex flex-wrap items-center gap-4 mb-4">
<div className="h-4 w-24 bg-white/5 rounded-md"></div>
<div className="h-4 w-32 bg-white/5 rounded-md"></div>
</div>
<div className="h-20 w-full bg-white/5 rounded-md border-l-2 border-primary/20"></div>
</div>
</div>
</div>
</div>
);
}

const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
Expand Down
32 changes: 32 additions & 0 deletions packages/client/src/app/auth/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center p-4 bg-gradient-to-br from-black via-gray-900 to-black overflow-hidden relative">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(120,119,198,0.1),rgba(255,255,255,0))] -z-10" />

<div className="w-full max-w-md bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-8 shadow-2xl animate-pulse">
<div className="text-center mb-8 flex flex-col items-center">
<div className="h-8 w-48 bg-white/10 rounded-md mb-4"></div>
<div className="h-4 w-64 bg-white/5 rounded-md"></div>
</div>

<div className="space-y-4">
<div>
<div className="h-4 w-24 bg-white/10 rounded-md mb-2"></div>
<div className="h-10 w-full bg-white/5 rounded-md border border-white/10"></div>
</div>

<div>
<div className="h-4 w-24 bg-white/10 rounded-md mb-2"></div>
<div className="h-10 w-full bg-white/5 rounded-md border border-white/10"></div>
</div>

<div className="h-10 w-full bg-indigo-500/20 rounded-md mt-6"></div>

<div className="mt-4 flex justify-center">
<div className="h-4 w-32 bg-white/5 rounded-md"></div>
</div>
</div>
</div>
</div>
);
}
17 changes: 16 additions & 1 deletion packages/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import { dbTestRoutes } from "./routes/test-db";
import { authRoutes } from "./routes/auth";
import { roomRoutes } from "./routes/rooms";
import { waitlistRoutes } from "./routes/waitlist";
import { userRoutes } from "./routes/users";
import { groupRoutes } from "./routes/groups";
import { messageRoutes } from "./routes/messages";
import { locationRoutes } from "./routes/locations";
import { circleRoutes } from "./routes/circles";
import { postRoutes } from "./routes/posts";
import { eventRoutes } from "./routes/events";
import { websocketRoutes } from "./websocket/wsHandler";
import fastifyJwt from "@fastify/jwt";
import fastifyCookie from "@fastify/cookie";
Expand Down Expand Up @@ -152,9 +159,17 @@ export const buildApp = async () => {
// --- Routes ---
app.register(healthRoutes, { prefix: "/api" });
app.register(dbTestRoutes, { prefix: "/api" });
app.register(authRoutes, { prefix: "/api/auth" });
app.register(authRoutes, { prefix: "/api/v1/auth" });
app.register(groupRoutes, { prefix: "/api/v1/groups" });
app.register(messageRoutes, { prefix: "/api/v1/messages" });
app.register(locationRoutes, { prefix: "/api/v1/locations" });
app.register(userRoutes, { prefix: "/api/v1/users" });
app.register(circleRoutes, { prefix: "/api/v1/circles" });
app.register(postRoutes, { prefix: "/api/v1/posts" });
app.register(eventRoutes, { prefix: "/api/v1/events" });
app.register(roomRoutes, { prefix: "/api/rooms" });
app.register(waitlistRoutes, { prefix: "/api/waitlist" });
app.register(userRoutes, { prefix: "/api/users" });
app.register(websocketRoutes, { prefix: "/ws" });

// --- Observability: Per-Route HTTP Latency Tracking ---
Expand Down
18 changes: 18 additions & 0 deletions packages/server/src/routes/circles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FastifyInstance } from "fastify";
import { db } from "../db/index";
import { circles } from "@intellicircle/shared";
import { eq } from "drizzle-orm";
import { createSuccessResponse, createErrorResponse } from "../utils/response";

export async function circleRoutes(app: FastifyInstance) {
app.get("/", async (request, reply) => {
try {
const results = await db.select().from(circles).limit(50);
return reply.send(createSuccessResponse(results));
} catch (error) {
app.log.error(error);
// Fix unhandled promise rejection by properly catching and returning 500
return reply.status(500).send(createErrorResponse("Internal server error", "INTERNAL_SERVER_ERROR"));
}
});
}
31 changes: 31 additions & 0 deletions packages/server/src/routes/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FastifyInstance } from "fastify";
import { db } from "../db/index";
import { events } from "@intellicircle/shared";
import { createSuccessResponse, createErrorResponse } from "../utils/response";

export async function eventRoutes(app: FastifyInstance) {
/**
* @api {get} /api/v1/events/nearby Request Nearby Events
* @apiName GetNearbyEvents
* @apiGroup Events
*
* @apiParam {Number} lat Latitude of the location.
* @apiParam {Number} lng Longitude of the location.
* @apiParam {Number} [radius=10] Search radius in kilometers.
*
* @apiSuccess {Object[]} events List of events.
*/
app.get("/nearby", async (request, reply) => {
try {
// A simple implementation fetching first 50 events for scaffolding
const results = await db
.select()
.from(events)
.limit(50);
return reply.send(createSuccessResponse(results));
} catch (error) {
app.log.error(error);
return reply.status(500).send(createErrorResponse("Internal server error", "INTERNAL_SERVER_ERROR"));
}
});
}
8 changes: 8 additions & 0 deletions packages/server/src/routes/groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FastifyInstance } from "fastify";
import { createSuccessResponse, createErrorResponse } from "../utils/response";

export async function groupRoutes(app: FastifyInstance) {
app.get("/", async (request, reply) => {
return reply.send(createSuccessResponse([]));
});
}
8 changes: 8 additions & 0 deletions packages/server/src/routes/locations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FastifyInstance } from "fastify";
import { createSuccessResponse, createErrorResponse } from "../utils/response";

export async function locationRoutes(app: FastifyInstance) {
app.get("/", async (request, reply) => {
return reply.send(createSuccessResponse([]));
});
}
8 changes: 8 additions & 0 deletions packages/server/src/routes/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FastifyInstance } from "fastify";
import { createSuccessResponse, createErrorResponse } from "../utils/response";

export async function messageRoutes(app: FastifyInstance) {
app.get("/", async (request, reply) => {
return reply.send(createSuccessResponse([]));
});
}
Loading