Skip to content

lcsburati/team-focus

Repository files navigation

🍅 FocusTeam — Real-time team Pomodoro

Languages: English · Português · 中文

A collaborative Pomodoro for teams. Everyone joins a room, runs their own focus and break cycles, and the whole team sees each other's state live — who's in focus, who's on a break, and who marked "do not disturb." Not surveillance: a shared, respectful rhythm.

Stack: Docker · NestJS (REST + WebSocket) · Angular 17 · PostgreSQL · Socket.io · JWT


💡 Why it exists

Small teams and study groups work side by side but constantly break each other's concentration. FocusTeam was built to improve the team's organizational climate and protect individual focus. Each person controls their own Pomodoro, while the group can tell at a glance who's heads-down and who's free — so people reach out at the right moment instead of interrupting deep work.

What changed for the team

The tool is in active, daily use and has been delivering results:

  • Focus blocks are respected — fewer mistimed interruptions, because "do not disturb" is visible to everyone.
  • Healthier organizational climate — async by default; questions go to the room chat instead of breaking someone's concentration.
  • Availability at a glance — no more "are you busy?" pings; the live board answers it.
  • A shared rhythm — breaks naturally line up, so conversation happens when people are actually free.

🤖 Built with AI

This project was built as a human + AI collaboration — pair-programming with an AI assistant from architecture through implementation and these docs. It's both a real internal tool and a portfolio piece showcasing an AI-assisted, full-stack workflow, end to end.


✨ Features

  • JWT login — register and authenticate with e-mail/password (bcrypt + tokens).
  • Rooms — create a room and invite the team with a 6-character code.
  • Per-person timer — each member runs their own cycle (focus / short break / long break).
  • Off-page alerts — Web Push notifies about messages and phase changes even when the tab is in the background or closed.
  • Chat mentions — type @ to pick a person. Mentions are highlighted with a red counter in the main chat, the widget, and Picture-in-Picture.
  • Timer widget — on supported browsers, the timer can float in a Picture-in-Picture window; the remaining time also shows in the tab title.
  • Per-person break settings — durations, sessions until the long break, and auto-start of breaks/focus. Settings are individual.
  • Live presence — each person's card shows the current phase (🎯 focus, ☕/🌿 break, 💤 idle) and a countdown synchronized in real time for everyone.
  • Available / Busy toggle — signal "🔕 do not disturb" to the team.
  • Online/offline — anyone with the room open shows as online.
  • Server is the source of truth for time — phase transitions happen on the backend (via setTimeout) and are broadcast over WebSocket, so everyone sees the same state even if a tab is closed.

💬 Chat with channels — General, DMs, and groups

Each room has a real-time chat (floating button at the bottom-right of the dashboard), organized as a conversations panel (messenger-style):

  • # General — a channel with everyone in the room (always present).
  • Direct messages (1:1) — talk privately with a member.
  • Groups — create subgroups with the people you choose, with an optional name, and add/remove members later (the 👥 button at the top of the conversation).
  • Truly private delivery — DMs and groups are sent only to participants (each user has a personal Socket.io room). General goes to the whole room.
  • History (last 50 messages) loaded on join, unread counts per conversation, plus a total badge on the floating button.
  • Notifications — toast preview, a short sound, and a browser notification (when the tab is in the background).
  • Respects "do not disturb" — while you're marked Busy, all notifications are silenced automatically (the unread count keeps tracking, without noise).
  • Bell 🔔/🔕 at the top of the chat to toggle notifications manually (saved in the browser).

🚀 Quick start (Docker)

Prerequisite: Docker + Docker Compose.

# 1. (optional) adjust variables — sensible defaults are provided
cp .env.example .env

# 2. bring everything up (Postgres + API + frontend)
docker compose up --build

Then open http://localhost:8080.

Services and ports:

Service Port Description
frontend 8080 Angular served by nginx (proxies to the API)
backend 3000 NestJS API + Socket.io (/api, /socket.io)
db 5432 PostgreSQL 16

The frontend's nginx proxies /api and /socket.io to the backend, so everything runs on the same origin (:8080) — no CORS headaches.

Try real-time presence

  1. Open http://localhost:8080 and create an account.
  2. Create a room and copy the code.
  3. Open a second incognito window (or another browser), create another account, and join via "Join by code."
  4. Start a timer in one window and watch the card change state in the other instantly. Toggle Available/Busy and see the signal update.

🧑‍💻 Development mode (no Docker for the app)

Run only the database in Docker and the apps locally:

# database
docker compose up db

# backend (port 3000)
cd backend
npm install
DB_HOST=localhost npm run start:dev

# frontend (port 4200, proxied to the backend)
cd frontend
npm install
npm start

Dev frontend: http://localhost:4200 (proxy.conf.json forwards /api and /socket.io to localhost:3000).


⚙️ Environment variables

Defined in docker-compose.yml (override via .env):

Variable Default Use
POSTGRES_USER pomodoro Postgres user
POSTGRES_PASSWORD pomodoro Postgres password
POSTGRES_DB pomodoro database name
JWT_SECRET super-secret-change-me change in production
JWT_EXPIRES_IN 7d token lifetime
CORS_ORIGIN * origin allowed by the API
VAPID_PUBLIC_KEY development key Web Push public key
VAPID_PRIVATE_KEY development key Web Push private key
VAPID_SUBJECT mailto:admin@focusteam.local Push administrator contact

For production, generate a new VAPID pair and set it in .env. Web Push requires HTTPS, except on localhost, where it works without a certificate. Permission is only requested when a person clicks Enable alerts inside a room.


🏗️ Architecture

┌────────────┐    HTTP /api      ┌───────────────────────────┐
│  Angular   │ ────────────────▶ │  NestJS                    │
│  (nginx)   │   WS /socket.io   │  • Auth (JWT)              │     ┌────────────┐
│  :8080     │ ◀───────────────▶ │  • Rooms / Memberships     │ ──▶ │ PostgreSQL │
└────────────┘                   │  • TimerGateway (Socket.io)│     │  :5432     │
                                 │  • TimerService (ticker)   │     └────────────┘
                                 └───────────────────────────┘

Backend (/backend) — NestJS 10 + TypeORM

  • auth/ — registration, login, JWT strategy and guard.
  • users/ — user entity + service.
  • rooms/Room and Membership entities (presence + per-person timer state/config), REST to create/join/list rooms, and room snapshot building.
  • timer/TimerService (phase-transition rules + scheduling via setTimeout, re-arms timers after a restart) and TimerGateway (Socket.io: join, timer actions, presence, config; broadcasts room:state to the room).
  • chat/conversations, conversation_participants, and messages entities + service.
  • push/ — Web Push subscriptions + service.

Frontend (/frontend) — Angular 17 standalone + signals

  • core/AuthService, JWT interceptor + guard, ApiService, SocketService.
  • pages/login — login/registration.
  • pages/rooms — room list, create, and join by code.
  • pages/roomdashboard: personal control panel (progress ring, start/pause/skip/reset, break settings, available/busy toggle) + live member grid.
  • components/MemberCardComponent and TimerConfigComponent.

WebSocket events

Event (client → server) Payload
room:join { roomId }
room:leave { roomId }
timer:action { roomId, action } (start/pause/skip/reset)
presence:set { roomId, status } (available/busy)
config:update { roomId, config }
chat:open { conversationId }
chat:send { conversationId, text }
chat:createConversation { roomId, kind, participantIds, name? }
chat:addParticipants / chat:removeParticipant { conversationId, ... }
Event (server → client) Payload
room:state full room snapshot
chat:conversations the user's conversation list
chat:conversation / chat:conversationOpened a single conversation
chat:history recent messages on open
chat:message new message (broadcast to participants)
error:auth / error:room error message

📝 Notes

  • TypeORM runs with synchronize: true to keep the MVP simple (it creates the schema automatically). In production, disable it and use migrations.
  • The JWT token is stored in the browser's localStorage.
  • The floating window uses Document Picture-in-Picture, available mainly on Chrome and Edge desktop; on other browsers the tab title keeps showing the remaining time.
  • The timer is server-authoritative: when a phase expires the backend transitions and re-emits state, so the front-end countdown is just a rendering of the received phaseEndsAt.
  • If something looks off after an update, run docker compose down -v once to reset the database, then bring it back up.

Built with care to help a real team focus — and as a portfolio demonstration of an AI-assisted, full-stack workflow.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages