# Project export: Rally

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Poke for tech startups: calendar, shared brain, and PRs from a text.
- Devpost: https://devpost.com/software/rally-fmiosz
- GitHub: https://github.com/aaditgupta21/treehacks26
- Video: https://www.youtube.com/embed/DvXLZAldqwg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Aadit Gupta (11 commits), Cursor (9 commits), bassiarmaan (3 commits)

## Devpost submission (written by the team)

### Inspiration

Modern teams are drowning in tools but starving for alignment. Tasks live in one app, calendars in another, decisions get lost in chat, and critical context disappears as conversations scroll by. We kept running into the same pain while building with friends: the work wasn’t hard — staying synced was. So we asked a simple question: what if every team had a shared operational brain? Not another dashboard. Not another “process.” A brain that lives where teams already operate: messages. The same “text an AI and it just does it” experience (à la Poke), but tuned for how dev teams actually work: calendar tetris, shared knowledge, lists — and the piece that felt missing — code. If you can text “add a blue CTA to the landing page,” why can’t you get a real PR back? That became Rally: one MCP server that plugs into Poke and turns group chat into alignment + execution.

### What it does

Rally is an AI-powered operational brain for teams — built for messaging-first startups. Teams message Rally through iMessage (via Poke) to: Schedule meetings + find team availability Store and retrieve knowledge (decisions, brand, runbooks) Manage shared lists (shopping, tasks, launch checklists) Ship code from a text (“add a blue button” → PR link) Instead of forcing new workflows, Rally turns messy team conversation into structured, searchable team state — so context doesn’t die in the scroll. Rally workflow 1) Messaging → structured team state When teammates text Rally in their own 1:1 Poke threads, Rally writes into a team-scoped database: Calendar events (team + personal opt-in visibility) Decisions / notes / runbooks Lists and recurring ops Rally normalizes messages into structured objects (events, decisions, tasks) so they’re queryable and consistent even when people ask things differently. 2) Semantic “team memory” retrieval When someone asks “what did we decide about onboarding?” or “what’s the launch checklist?”, Rally: generates embeddings for new notes stores them in the team index retrieves relevant context by meaning (not keywords) responds conversationally with the source context So the brain gets smarter over time, without anyone maintaining documentation manually. 3) Text-to-PR pipeline (the “ship it” moment) When someone requests a code change (“make the CTA blue”, “add rate limiting”, “fix the navbar on mobile”), Rally: pulls repo context (registered once) clones the repo into a sandbox has an agent propose a plan + file-level edits runs checks/tests commits, pushes a branch, and opens a PR returns the PR link right back into iMessage One message → a real PR, with verification. Rally vs Poke group chat (why we exist) Poke group chats are intentionally limited for privacy: no one’s personal calendars, email, etc. In group chat, Poke is mostly web/search/images/reminders. Rally is the opt-in “shared layer.” If your team wants shared context — shared calendar, shared knowledge, shared lists, and text-to-PR — you add Rally. Everything is team-scoped by default, and personal visibility is explicit opt-in. So: Poke 1:1 = your personal assistant Poke group chat = lightweight coordination + chaos Poke + Rally = a real team operating system in messages

### How we built it

MCP Server (Python / FastMCP): exposes tools for calendar, knowledge, lists, and code workflows Team state + API: persistent DB + REST endpoints for a lightweight dashboard Semantic memory: embeddings + search index for “decision retrieval” and runbook recall Text-to-PR: GitHub repo registration → sandbox clone → agent edits → checks → PR via GitHub API Poke integration: Rally appears as a single MCP integration (“Rally”) so Poke can reliably discover and call tools

### Challenges we ran into

Multi-user async consistency: two people can ask for updates in different threads at different times — keeping shared state deterministic (especially calendar logic) was harder than it looked. Tool discoverability + naming: MCP clients are picky; small naming mismatches made tools appear but not callable. We unified tool naming and reduced integration ambiguity so Poke consistently executed the right functions.

### What we learned

Teams don’t struggle from a lack of tools — they struggle from fragmented context. The fastest way to ship isn’t more dashboards; it’s reducing coordination overhead by making the “state of the team” always available, always current, and living where people already talk. We also learned that messaging integrations live and die by reliability: one clean MCP integration, deterministic tool behavior, and clear boundaries around shared vs personal data makes the experience feel real.

### What's next

Proactive “ops nudges” (blockers, risks, stale decisions) Automatic meeting summaries → action items → owners Decision tracking with versioning (“what changed since last week?”) Deeper integrations (Linear/Jira, Notion, Slack, Figma) Smarter code changes: multi-step PRs, review suggestions, and rollback safety

## README (from the GitHub repository)

# Rapid

Multiplayer Poke — a shared AI assistant for teams. Connect this MCP server to Poke for:

- **Calendar tetris** — Find 30-min windows when the whole dev team is free
- **Shared knowledge** — "Remember: our brand colors are #FF6B35 and #004E89"
- **Meeting booking** — Book slots and relay to team members
- **Shopping lists** — Team shopping lists (Visa commerce track)

## Quick Start

### 1. Install & run locally

```bash
python -m venv venv
source venv/bin/activate   # or `venv\Scripts\activate` on Windows
pip install -r requirements.txt
python src/server.py
```

Server runs at `http://localhost:8000/mcp`

### 2. Connect to Poke

**Option A: Tunnel (local dev)** — if you have the Poke CLI:

```bash
poke tunnel http://localhost:8000/mcp --name "Team Brain"
```

**Option B: Add remote URL** — no Poke CLI needed: run `ngrok http 8000`, then use `https://YOUR-NGROK-URL/mcp` in Poke settings.

1. Deploy to Render (see below) or expose via ngrok
2. Go to [poke.com/settings/connections](https://poke.com/settings/connections)
3. Create Integration → MCP Server URL: `https://your-url/mcp` → Name: "Team Brain"

### 3. Try it in Poke

Ask Poke things like:

- *"Use the Team Brain integration to store that our brand colors are #FF6B35 and #004E89"*
- *"Use Team Brain to add milk to the shopping list"*
- *"Use Team Brain to find when Alice and Bob are free next week"*

**Text-to-PR (Vercel / GitHub):** Register a repo once, then request changes via text. Poke will use Claude to edit the code and open a PR.

1. *"Use Team Brain to register my project: repo https://github.com/username/my-app, branch main, name my Vercel project"* → calls `register_project`
2. *"Use Team Brain to make changes on my Vercel project: make the header say Welcome and add a blue CTA button"* → calls `request_code_change`; you get back a PR link (and optional Vercel preview).

Requires on the server: `GITHUB_TOKEN` (repo push + create PR), `ANTHROPIC_API_KEY` (Claude for code edits). Optional: `CLAUDE_CODING_MODEL` (default `claude-sonnet-4-5`).

## Tools (14 total)

| Tool | Description |
|------|-------------|
| `set_availability` | Record when a team member is free |
| `find_availability` | Find overlapping free slots for team |
| `book_meeting` | Book a meeting — stores internally and pushes to teammates' Pokes |
| `list_team_calendar` | List all booked meetings (shared team calendar) |
| `register_for_calendar_sync` | Register your Poke webhook/API key to receive calendar invites |
| `store_knowledge` | Store team facts (brand, policy, etc.) |
| `query_knowledge` | Search team knowledge |
| `add_to_shopping_list` | Add item to shared list |
| `get_shopping_list` | Get shopping list |
| `remove_from_shopping_list` | Remove item |
| `register_project` | Register a GitHub repo for “text-to-PR” (Vercel project) |
| `list_projects_tool` | List registered projects for the team |
| `request_code_change` | Request code changes on a registered project → agent makes edits and opens a PR |
| `get_team_brain_info` | Server info |

Use `team_id` (default: `"default"`) to scope to a team. Each teammate adds the same MCP server; the server stores data per team.

### Calendar sync flow

1. **Add API keys** — Create `poke_api_keys.txt` in the project root with your and teammates' Poke API keys (one per line):
   ```
   me:pk_your_key
   friend:pk_their_key
   ```
   Get keys at [poke.com/kitchen/api-keys](https://poke.com/kitchen/api-keys)

2. **You text Cortex** (e.g. via SMS): *"Book 7pm for tonight"* or *"Make a calendar invite for today at 7pm"*
3. **Poke** calls `book_meeting` → event stored in Team Brain
4. **All keys in the file** receive the invite in their Poke → adds to their calendar (Poke calendar sync)
5. **Anyone** can ask *"What's on the team calendar?"* → sees all meetings

## Deploy to Render

1. Push to GitHub
2. Connect repo to Render
3. New Web Service → Render will use `render.yaml`
4. Your MCP URL: `https://team-brain-mcp.onrender.com/mcp`

## Frontend (Next.js)

The dashboard displays calendar, knowledge, and shopping data from the API (Elasticsearch when configured):

```bash
# Terminal 1: MCP server (for Poke)
python src/server.py

# Terminal 2: REST API (for frontend) — run from project root so .env loads
python run_api.py

# Terminal 3: Frontend
cd frontend && npm install && npm run dev
```

- MCP (Poke): http://localhost:8000/mcp
- REST API: http://localhost:8001/api/...
- Frontend: http://localhost:3000

Set `NEXT_PUBLIC_API_URL` in `frontend/.env.local` to override the API URL (e.g. when deployed).

## Elasticsearch + JINA (Optional)

For persistent storage and semantic knowledge search:

1. **Elastic Cloud** — Create a deployment at [cloud.elastic.co](https://cloud.elastic.co)
2. **Env vars:**
   - `ELASTIC_CLOUD_ID` + `ELASTIC_API_KEY` (from Elastic Cloud)
   - Or `ELASTIC_URL` (e.g. `http://localhost:9200`) + `ELASTIC_API_KEY` for self-hosted
   - `JINA_API_KEY` — from [jina.ai](https://jina.ai) for embeddings
3. **Knowledge base** — `store_knowledge` and `query_knowledge` use JINA v3 embeddings for semantic search (finds by meaning, not just keywords)

The `elastic/` folder contains a workflow template for Elastic Agent Builder integration.

## Sponsor Tracks

- **Poke / Interaction Co.** — MCP server, Poke-native, team coordination
- **Elastic** — JINA v3 embeddings, semantic search, Elastic Cloud, Agent Builder workflows
- **Decagon** — Multi-turn conversational agent
- **Anthropic** — Reduces calendar anxiety; tool-using agent
- **Greylock** — Multi-turn agent; chains tools
- **Visa** — Shopping automation; shared lists
- **Graphite** — Real product; team calendar + shared brain


## Detected evidence (automated analysis)

Indexed codebase: 28 recognized source files, 112 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (38 of 38)

```
.env.example
.gitignore
elastic/workflow-team-calendar.yaml
frontend/.gitignore
frontend/eslint.config.mjs
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/src/app/dashboard/layout.tsx
frontend/src/app/dashboard/page.tsx
frontend/src/app/globals.css
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/components/AddEntryForm.tsx
frontend/src/components/CalendarView.tsx
frontend/src/components/EntriesView.tsx
frontend/src/components/FluidCanvas.tsx
frontend/src/components/ShoppingListView.tsx
frontend/src/lib/api.ts
frontend/src/lib/data.ts
frontend/tsconfig.json
ngrok.sh
package.json
poke_api_keys.example.txt
README.md
render.yaml
requirements.txt
run_api.py
src/api.py
src/coding_agent.py
src/config.py
src/elastic_store.py
src/embeddings.py
src/poke_relay.py
src/project_store.py
src/server.py
src/store.py
```

### Dependencies

- frontend/package.json: @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.1.6, next@16.1.6, react@19.2.3, react-dom@19.2.3, tailwindcss@^4, typescript@^5
- package.json: poke@^0.3.1
- requirements.txt: anthropic@>=0.39.0, elasticsearch@>=8.0.0, fastmcp@>=2.0.0,<3, flask@>=3.0.0, flask-cors@>=4.0.0, httpx@>=0.25.0, python-dotenv@>=1.0.0

### Recent commits (newest first)

- Update README.md
- Revert "Calendar: personal vs team events, frontend team-only, clearer Brain instructions"
- Calendar: personal vs team events, frontend team-only, clearer Brain instructions
- Merge feature/text-to-pr: coding agent logs, Claude 4.5, .env loading
- Coding agent: logs, Claude 4.5 default, load .env from project root
- Add Team Brain text-to-PR: register_project, request_code_change, coding agent
- Remove Add Entry tab from frontend dashboard
- Calendar: sync next 7 days only, get_member_availability, check calendars before availability, clarify synced vs team events (never book synced to other calendars)
- Add meeting delete: frontend button, DELETE /api/meetings, delete_meeting MCP tool, attendee names from poke_api_keys
- Elasticsearch-only store, separate REST API for frontend, remove in-memory fallback
- nex.js frontend
- Elasticsearch backend, JINA embeddings, .env, ES fetch logging
- added frontend
- Team Brain: calendar sync via JWT, pure HTTP poke relay
- initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "team-brain",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "poke": "^0.3.1"
  }
}

```

### requirements.txt

```
fastmcp>=2.0.0,<3
elasticsearch>=8.0.0
httpx>=0.25.0
python-dotenv>=1.0.0
flask>=3.0.0
flask-cors>=4.0.0
anthropic>=0.39.0

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "next": "16.1.6",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### src/server.py

```python
#!/usr/bin/env python3
"""
Team Brain MCP Server — Shared AI assistant for teams.
Connect this to Poke for calendar tetris, knowledge base, shopping lists, and more.
"""

import os
from pathlib import Path
from datetime import datetime

from dotenv import load_dotenv

# Load .env from project root so ANTHROPIC_API_KEY etc. are set regardless of cwd
_root = Path(__file__).resolve().parent.parent
for p in [_root / ".env", Path.cwd() / ".env"]:
    if p.exists():
        load_dotenv(p)
        break
else:
    load_dotenv()

from fastmcp import FastMCP

from store import (
    add_calendar_slot,
    add_knowledge,
    add_shopping_item,
    get_or_create_team,
    get_team,
    get_shopping_list as store_get_shopping_list,
    has_synced_calendar,
    remove_shopping_item,
    remove_meeting,
    search_knowledge,
    sync_member_calendar,
)
from elastic_store import get_calendar_slots as es_get_calendar_slots
from poke_relay import send_to_all_pokes
from config import load_poke_api_keys, load_poke_api_keys_with_names
from project_store import register_project as store_register_project, get_project, list_projects
from coding_agent import run_code_change

mcp = FastMCP(
    "Team Brain",
    instructions="""IMPORTANT — two types of calendar events:

1. TEAM EVENTS (use book_meeting): When user wants to schedule something for BOTH of them (me and Armaan, team meeting, dp). Use book_meeting — it adds to both calendars and both attend. Never add team events via personal calendar.

2. SYNCED / INDIVIDUAL EVENTS (use sync_my_calendar): When user says 'sync my calendar', get THEIR events (next 7 days) and call sync_my_calendar. These are that person's personal events — stored in Team Brain ONLY for visibility so teammates can see when they're busy. NEVER add or book these events to anyone else's calendar. Armaan's synced events stay his; do NOT put them on Aadit's calendar. When checking availability, we read synced events to know when someone is busy — we do NOT copy them to other people's calendars.

Sync: member_id 'aadit' or 'armaan' based on who is messaging. Do NOT ask for API keys.
Availability: get_member_availability for whole-day, check_member_free for specific time. Pass requester_id.

CODE CHANGES & GITHUB: Team Brain CAN push code and open PRs. When the user asks for code changes on a repo, Vercel project, or "my project": (1) If they give a repo URL, use register_project first (team_id default, repo_url, default_branch main, optional name). (2) Then use request_code_change with that project name/id and their instruction. Reply with the PR link you get back. Do NOT say Team Brain can't do code or that they need to use VSCode — use the tools.""",
)


# --- Calendar tools ---

@mcp.tool(
    description="Set a team member's availability. Use this when someone shares when they're free. member_id can be name or identifier."
)
def set_availability(
    team_id: str,
    member_id: str,
    member_name: str,
    start: str,
    end: str,
    summary: str = "",
) -> str:
    """Register when a team member is available."""
    get_or_create_team(team_id)
    add_calendar_slot(team_id, member_id, member_name, start, end, summary, slot_type="availability")
    return f"Recorded availability for {member_name}: {start} to {end}"


@mcp.tool(
    description="Find time slots when ALL listed team members are free. Pass list of member_ids/names and optional date range."
)
def find_availability(
    team_id: str,
    member_ids: list[str],
    date_start: str = "",
    date_end: str = "",
) -> str:
    """Find overlapping availability for team members."""
    team = get_team(team_id)
    if not team:
        return f"Team '{team_id}' not found. Use set_availability first to add members."
    slots = team.calendar_slots
    if not slots:
        return "No availability data yet. Ask team members to share when they're free via set_availability."
    # Simple overlap logic: for MVP we return all slots; real impl would compute intersections
    member_set = {m.lower() for m in member_ids}
    relevant = [
        s for s in slots
        if s.member_id.lower() in member_set or s.member_name.lower() in member_set
    ]
    if not relevant:
        return f"No availability found for members: {member_ids}"
    lines = [f"- {s.member_name}: {s.start} to {s.end}" for s in relevant]
    return "Availability:\n" + "\n".join(lines)


@mcp.tool(
    description="Book a team calendar event. USE THIS when user wants to schedule for the team, dp, me and Armaan, etc. Stores in database and sends to BOTH Pokes (yours and Armaan's) so it shows up on both calendars. API keys from poke_api_keys.txt."
)
def book_meeting(
    team_id: str,
    title: str,
    start: str,
    end: str,
    attendees: list[str] | None = None,
) -> str:
    """Book meeting in DB, then send to every Poke in poke_api_keys.txt."""
    get_or_create_team(team_id)
    # Use names from poke_api_keys.txt as attendees (everyone who receives the invite)
    # Don't merge with AI-passed attendees — avoids duplicates like "Aadit" + "Aadit Gupta"
    keys_with_names = load_poke_api_keys_with_names()
    attendee_names = [n for n, _ in keys_with_names]
    keys = [k for _, k in keys_with_names]
    summary = f"Booked: {', '.join(attendee_names)}"
    add_calendar_slot(team_id, "meeting", title, start, end, summary, slot_type="meeting")

    if not keys:
        return f"Booked '{title}' from {start} to {end} in database. No API keys in poke_api_keys.txt — add yours and Armaan's keys (pk_xxx from poke.com/kitchen/api-keys) to push to calendars."

    print(f"[Team Brain] Pushing to {len(keys)} Poke(s)...", flush=True)
    results = send_to_all_pokes(keys, title, start, end)
    for prefix, ok, err in results:
        print(f"  {prefix}: {'OK' if ok else 'FAIL ' + err}", flush=True)
    ok = sum(1 for _, success, _ in results if success)
    fail = [(p, e) for p, success, e in results if not success]

    out = f"Booked '{title}' from {start} to {end}. Sent 
[truncated — 16395 more characters]
```

### frontend/src/app/page.tsx

```typescript
import { redirect } from "next/navigation";

export default function Home() {
  redirect("/dashboard");
}

```

### frontend/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";
import FluidCanvas from "@/components/FluidCanvas";

export const metadata: Metadata = {
  title: "Rally",
  description: "AI that works as one with your team",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body suppressHydrationWarning>
        <FluidCanvas />
        <div className="app-shell">{children}</div>
      </body>
    </html>
  );
}

```

### frontend/src/app/dashboard/layout.tsx

```typescript
"use client";

import Link from "next/link";
import { useEffect, useState } from "react";

function HeaderDate() {
  const [dateStr, setDateStr] = useState("");

  useEffect(() => {
    setDateStr(
      new Date().toLocaleDateString("en-US", {
        weekday: "short",
        month: "short",
        day: "numeric",
      })
    );
  }, []);

  return <span className="header-date">{dateStr}</span>;
}

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      <header className="header-bar">
        <div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
          <HeaderDate />
        </div>
        <Link href="/" className="logo">
          RALLY
        </Link>
        <div />
      </header>
      {children}
    </>
  );
}

```

### frontend/src/app/dashboard/page.tsx

```typescript
"use client";

import { useState, useCallback, useEffect } from "react";
import {
  getMeetings,
  getKnowledge,
  getShoppingList,
  removeShoppingItem as apiRemoveShoppingItem,
  deleteMeeting as apiDeleteMeeting,
  type Meeting,
  type KnowledgeEntry,
  type ShoppingItem,
} from "@/lib/api";
import CalendarView from "@/components/CalendarView";
import EntriesView from "@/components/EntriesView";
import ShoppingListView from "@/components/ShoppingListView";

type Tab = "calendar" | "entries" | "shopping";

const TABS: { id: Tab; label: string }[] = [
  { id: "calendar", label: "Calendar" },
  { id: "entries", label: "Knowledge" },
  { id: "shopping", label: "Shopping" },
];

export default function DashboardPage() {
  const [activeTab, setActiveTab] = useState<Tab>("calendar");
  const [meetings, setMeetings] = useState<Meeting[]>([]);
  const [entries, setEntries] = useState<KnowledgeEntry[]>([]);
  const [shoppingList, setShoppingList] = useState<ShoppingItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const teamId = "default";

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const [m, e, s] = await Promise.all([
        getMeetings(teamId),
        getKnowledge(teamId),
        getShoppingList(teamId),
      ]);
      setMeetings(Array.isArray(m) ? m : []);
      setEntries(Array.isArray(e) ? e : []);
      setShoppingList(Array.isArray(s) ? s : []);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to load data");
      setMeetings([]);
      setEntries([]);
      setShoppingList([]);
    } finally {
      setLoading(false);
    }
  }, [teamId]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  const handleRemoveShopping = useCallback(
    async (item: string) => {
      try {
        await apiRemoveShoppingItem(teamId, item);
        setShoppingList((prev) => prev.filter((i) => i.name !== item));
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to remove item");
      }
    },
    [teamId]
  );

  const handleRemoveMeeting = useCallback(
    async (meeting: Meeting) => {
      try {
        await apiDeleteMeeting(teamId, meeting.title, meeting.start, meeting.end);
        setMeetings((prev) =>
          prev.filter(
            (m) =>
              !(m.title === meeting.title && m.start === meeting.start && m.end === meeting.end)
          )
        );
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to delete meeting");
      }
    },
    [teamId]
  );

  if (loading) {
    return (
      <div className="glass-card" style={{ textAlign: "center", padding: "2rem" }}>
        Loading...
      </div>
    );
  }

  if (error) {
    return (
      <div className="glass-card" style={{ padding: "1.5rem" }}>
        <p style={{ color: "var(--peach-dark)", marginBottom: "0.5rem", fontWeight: 600 }}>
          Could not load data
        </p>
        <p style={{ fontSize: "0.9rem", color: "var(--text-muted)", marginBottom: "0.5rem" }}>
          {error}
        </p>
        <p style={{ fontSize: "0.85rem", color: "var(--text-muted)" }}>
          Start the API server: <code style={{ background: "rgba(0,0,0,0.1)", padding: "0.2em 0.4em", borderRadius: 4 }}>python run_api.py</code>
        </p>
        <button className="btn btn-primary" onClick={fetchData} style={{ marginTop: "1rem" }}>
          Retry
        </button>
      </div>
    );
  }

  return (
    <>
      <div className="dash-tabs">
        {TABS.map((tab) => (
          <button
            key={tab.id}
            className={`dash-tab ${activeTab === tab.id ? "active" : ""}`}
            onClick={() => setActiveTab(tab.id)}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {activeTab === "calendar" && (
        <CalendarView meetings={meetings} onRemove={handleRemoveMeeting} />
      )}
      {activeTab === "entries" && <EntriesView entries={entries} />}
      {activeTab === "shopping" && <ShoppingListView items={shoppingList} onRemove={handleRemoveShopping} />}
    </>
  );
}

```

### render.yaml

```yaml
services:
  - type: web
    name: team-brain-mcp
    runtime: python
    buildCommand: pip install -r requirements.txt
    startCommand: python src/server.py
    plan: free
    envVars:
      - key: ENVIRONMENT
        value: production

```

### ngrok.sh

```shell
#!/usr/bin/env bash
# Expose local Team Brain server via ngrok for Poke connection.
# Start the server first: python src/server.py
# Then run: ./ngrok.sh  (or: ngrok http 8000)

PORT="${PORT:-8000}"
echo "Exposing http://localhost:$PORT (add /mcp for Poke MCP URL)"
exec ngrok http "$PORT"

```

[19 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]