Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eb5c128
fix: stop timeline infinite spinner on pagination errors (#22)
alramalho May 22, 2026
4e85d21
perf: speed up photo proof uploads (#23)
alramalho May 22, 2026
9b7f0b6
fix: merge joint activity timeline cards
alramalho May 24, 2026
e14a192
feat: merge joint activity cards
alramalho May 25, 2026
7f43c3b
fix: polish merged joint activity cards
alramalho May 25, 2026
c0e6e3e
Improve coach setup flow
alramalho May 26, 2026
5498dcc
Merge timeline photo zoom
alramalho May 26, 2026
ed4966b
Refine merged activity card identity
alramalho May 26, 2026
b660b1a
Polish merged activity card layout
alramalho May 26, 2026
6d55185
Improve coach activity grounding
alramalho May 26, 2026
187df8c
Fix merged activity profile navigation
alramalho May 26, 2026
76aabe6
Hide duplicate shared activity entries
alramalho May 26, 2026
eb8924d
Merge branch 'fix/merged-joint-activity-cards' into perf/profile-time…
alramalho May 27, 2026
c92ac00
Merge remote-tracking branch 'origin/main' into perf/profile-timeline…
alramalho May 27, 2026
fdcb186
Merge pull request #25 from alramalho/perf/profile-timeline-load
alramalho May 27, 2026
fd637ba
Fix local auth redirect and profile heatmap scroll
alramalho May 29, 2026
a642b2d
Fix profile point totals for viewed users
alramalho May 29, 2026
605961e
Fix coach plan visibility and citations
alramalho May 29, 2026
c3fa744
Polish coach citations and plan proposals
alramalho May 29, 2026
f59dce1
Filter archived and paused plans from coach context
alramalho May 31, 2026
8c6ca14
Add coach upcoming-sessions ghost grid, generalize coach to all plans…
alramalho May 31, 2026
d042f1e
Speed up profile loading
alramalho Jun 1, 2026
e30ef1d
Distinguish today from selected day and show completed activities in …
alramalho Jun 1, 2026
c097959
docs: clarify local readiness checks
alramalho Jun 1, 2026
e56ccf5
docs: align backend health check port
alramalho Jun 2, 2026
1be8779
fix: configure Vercel monorepo output directory
alramalho Jun 10, 2026
097268e
fix: align Vercel output config with app root
alramalho Jun 12, 2026
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
61 changes: 30 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,44 +42,43 @@

## Running locally

To run the project locally, follow these steps:
This repository is a pnpm/Turbo monorepo. The current app lives in `apps/frontend-vite` and `apps/backend-node`; the old `frontend`/`backend` yarn + Python instructions no longer match the repo layout.

1. Navigate to the frontend directory and start the development server:
Install dependencies and generate Prisma client code from the repository root:

```sh
cd frontend
yarn dev
```
```sh
pnpm install --frozen-lockfile
pnpm --filter @tsw/prisma db:generate
```

2. Navigate to the backend directory and start the backend server:
```sh
cd backend
python -m main
```
Create local env files from the examples and fill in the credentials needed for the flows you want to exercise:

## Installing dependencies
```sh
cp apps/backend-node/.env.example apps/backend-node/.env
cp apps/frontend-vite/.env.example apps/frontend-vite/.env
```

1. Ensure all dependencies are installed beforehand.
For normal local development, run the database, backend, and frontend in separate terminals:

**frontend**
```sh
supabase start
pnpm --filter @tsw/prisma db:push
pnpm --filter backend-node dev
pnpm --filter frontend-vite dev
```

```sh
yarn
```
After copying `apps/backend-node/.env.example`, backend health should be available at `http://localhost:8000/health` (or the port you set in `apps/backend-node/.env`). Vite will print the frontend URL, usually `http://localhost:5173`.

**backend** <small>We recommended to use a local virtual environment (.venv):</smalll>
### Readiness checks vs. app startup

```sh
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
The app can build and start locally even when optional quality commands are not clean. Use these checks for CI/readiness work, not as proof that local app startup is broken:

2. Make sure you have a [ngrok account and auth token setup](https://ngrok.com/docs/getting-started/)
3. Make sure you create and link your [clerk](https://clerk.com/) account and link the necessary env vars
- frontend
- `CLERK_SECRET_KEY`
- `CLERK_JWT_PUBLIC_KEY`
- backend
- `CLERK_JWT_PUBLIC_KEY` (get this in API Keys > Show JWT Public Key)
4.
```sh
pnpm build
pnpm --filter frontend-vite lint
pnpm --filter backend-node lint
pnpm --filter backend-node test:ci
pnpm --filter e2e-tests exec playwright test --list
```

At the time this note was added, `pnpm build` passes on `main`, while some lint/test/e2e checks still need separate cleanup or credentials. Treat those as CI hygiene items unless your current task is specifically to make those commands green.
122 changes: 84 additions & 38 deletions apps/backend-node/src/routes/activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,78 @@ const uploadActivityEntryPhotos = async (
);
};

const notifyConnectionsAboutActivityPhotos = async ({
user,
activity,
entry,
quantity,
uploadedImageCount,
}: {
user: NonNullable<AuthenticatedRequest["user"]>;
activity: { title: string; emoji: string; measure: string };
entry: ActivityEntry;
quantity: string | number;
uploadedImageCount: number;
}) => {
const startedAt = Date.now();
try {
const userWithConnections = await prisma.user.findUnique({
where: { id: user.id },
include: {
connectionsFrom: {
where: { status: "ACCEPTED" },
include: { to: true },
},
connectionsTo: {
where: { status: "ACCEPTED" },
include: { from: true },
},
},
});

if (!userWithConnections) return;

const connectedUsersById = new Map(
[
...userWithConnections.connectionsFrom.map((conn) => conn.to),
...userWithConnections.connectionsTo.map((conn) => conn.from),
].map((connectedUser) => [connectedUser.id, connectedUser])
);
const connectedUsers = Array.from(connectedUsersById.values());

if (connectedUsers.length === 0) return;

const message = `${user.username} logged ${quantity} ${activity.measure} of ${activity.emoji} ${activity.title} with ${uploadedImageCount === 1 ? "a photo" : `${uploadedImageCount} photos`} 📸!`;

await Promise.all(
connectedUsers.map((connectedUser) =>
notificationService.createAndProcessNotification({
userId: connectedUser.id,
message,
type: "INFO",
relatedId: entry.id,
relatedData: {
activityEntryId: entry.id,
userPicture: user.picture,
userName: user.name,
userUsername: user.username,
},
})
)
);

logger.info(
`Photo activity notifications processed for ${connectedUsers.length} connection(s) in ${Date.now() - startedAt}ms`,
{ activityEntryId: entry.id }
);
} catch (error) {
logger.error(
`Error processing photo activity notifications for entry ${entry.id}:`,
error
);
}
};

// Get all activities for user
router.get(
"/",
Expand Down Expand Up @@ -436,6 +508,7 @@ router.post(
? timezoneFromCoords(latitude, longitude) ?? clientTimezone
: clientTimezone;
const photos = getUploadedActivityEntryPhotos(req);
let uploadedImageCount = 0;

// Check if activity exists and belongs to user
const activity = await prisma.activity.findFirst({
Expand Down Expand Up @@ -524,44 +597,7 @@ router.post(
logger.info(
`${uploadedImages.length} photo(s) uploaded successfully to S3 for activity entry ${entry.id}`
);

// Create notifications for connected users about the photo
const userWithConnections = await prisma.user.findUnique({
where: { id: req.user!.id },
include: {
connectionsFrom: {
where: { status: "ACCEPTED" },
include: { to: true },
},
connectionsTo: {
where: { status: "ACCEPTED" },
include: { from: true },
},
},
});

if (userWithConnections) {
const connectedUsers = [
...userWithConnections.connectionsFrom.map((conn) => conn.to),
...userWithConnections.connectionsTo.map((conn) => conn.from),
];

for (const connectedUser of connectedUsers) {
const message = `${req.user!.username} logged ${quantity} ${activity.measure} of ${activity.emoji} ${activity.title} with ${uploadedImages.length === 1 ? "a photo" : `${uploadedImages.length} photos`} 📸!`;
await notificationService.createAndProcessNotification({
userId: connectedUser.id,
message,
type: "INFO",
relatedId: entry.id,
relatedData: {
activityEntryId: entry.id,
userPicture: req.user!.picture,
userName: req.user!.name,
userUsername: req.user!.username,
},
});
}
}
uploadedImageCount = uploadedImages.length;
} catch (error) {
logger.error("Error uploading photo to S3:", error);
// Continue without photo - don't fail the entire activity logging
Expand Down Expand Up @@ -672,6 +708,16 @@ router.post(
candidates: sharedActivityCandidates.map((c: any) => ({ id: c.activityEntryId, score: c.score })),
});
res.json({ ...entry, entry, sharedActivityCandidates, sharedActivityInvite });

if (uploadedImageCount > 0) {
void notifyConnectionsAboutActivityPhotos({
user: req.user!,
activity,
entry,
quantity,
uploadedImageCount,
});
}
} catch (error) {
logger.error("Error logging activity:", error);
res.status(500).json({ error: "Failed to log activity" });
Expand Down
Loading