Meeting Notes¶
The meeting notes system provides centralized management of meeting documentation with links to organizations and projects. Meeting content is stored as markdown files while metadata is tracked in the database.
Concepts¶
Meeting References¶
A meeting reference is a database record that links to a markdown file containing meeting notes. Each meeting has:
- Meeting ID: Unique identifier (e.g.,
mtg-2026-01-05-kickoff) - Organization Link: Optional association with an organization
- Project Link: Optional association with a project
- File Reference: Path to the markdown file containing the notes
Content Storage¶
Meeting notes content is stored in markdown files on the filesystem. The database stores only the reference path, enabling:
- Version control of meeting content via git
- Large document support without database bloat
- Direct editing with any text editor
Meeting-Creation Flow¶
Frontend (MeetingCreate.vue)¶
- User fills meeting_id, optional org / project, attendees (presents in the form), and markdown content.
- Create runs handleCreate, which builds a payload and calls the Pinia store
- Store (meetingRefStore.js): createItem → meetingRefsApi.create(meetingRef).
- HTTP (api.js): POST /meeting-refs/ with the JSON body (no file_ref from the client; the server chooses the path).
API layer (FastAPI)¶
- Router: app/api/meeting_refs.py, mounted at /api in main.py → POST /api/meeting-refs/.
- Handler: create_meeting_ref injects:
- db → get_db
- notes_service → MeetingNotesService / get_meeting_notes_service
Flow inside the handler:
- Conflict check: crud.get_meeting_ref_by_meeting_id — if meeting_id exists → 409.
- Optional FK resolution: if org_id / project_id are set, load org/project to get names for the folder layout (and 404 if missing).
- Filesystem write first: notes_service.save_note(...) with meeting_id, content, and optional org_name / project_name.
- DB insert: crud.create_meeting_ref with the returned file_ref string plus metadata.
Filesystem (MeetingNotesService)¶
- Root: settings.notes_root, default docs/notes (see app/core/config.py and config.yaml), resolved relative to the process working directory unless configured as absolute.
- Relative path (_build_file_path → save_note):
- Folders: {org}/meetings/{project} with sanitized org and project names, or general/meetings/general when org/project are missing.
- File name: {YYYY-MM-DD}-{sanitized_meeting_id}.md (date defaults to “now”).
- I/O: mkdir -p on the parent directory, then write_text for the markdown.
On disk: {notes_root}/{org}/meetings/{project}/{date}-{meeting_id}.md.
Organization Strategy / Notes: the database stores description_path (path relative to notes_root, e.g. acme/notes/strategy.md); the markdown content lives only in that file. GET responses still expose description as the file body for the UI, plus description_path for the reference. See app/services/organization_notes.py and tools/migrate_org_description_to_path.py for legacy DB migration.
Database (crud.create_meeting_ref)¶
app/db/crud/meeting.pybuilds a Meeting row: meeting_id, file_ref (relative path under notes_root), project_id, org_id, attendees, then add → commit → refresh.- The body of the note is not in SQLite; only metadata and file_ref are persisted, as the doc states.
Database Model¶
API Endpoints¶
Meeting references (app/api/meeting_refs.py):
- POST /api/meeting-refs/
- List Meeting Notes: GET /api/meeting-refs/?org_id=1&project_id=5
- Get Meeting Note: GET /api/meeting-refs/{id}
- Get Meeting Content: GET /api/meeting-refs/{id}/content
- Update Meeting Note: PUT /api/meeting-refs/{id}
- Delete Meeting Note: DELETE /api/meeting-refs/{id} — deletes the database record and the markdown file
- Search by Meeting ID: GET /api/meeting-refs/search/by-meeting-id?meeting_id=mtg-2026-01-05-kickoff
Meeting metrics (see Meeting metrics from markdown headings):
- GET /api/metrics/meetings/created?period=monthly&days=90 — time series from scanned headings
- POST /api/metrics/meetings/refresh — rescan notes without restarting the app
File Storage Structure¶
Meeting files and org strategy notes live under notes_root (default {workspace}/docs/notes):
{workspace}/docs/notes/
├── {organization-slug}/
│ ├── notes/
│ │ ├── strategy.md # organization description (Strategy / Notes)
│ │ └── images/ # images for strategy markdown
│ └── meetings/
│ ├── {project-slug}/
│ │ └── {date}-{meeting-id}.md
│ └── general/
│ └── {date}-{meeting-id}.md
└── general/
└── meetings/
└── general/
└── {date}-{meeting-id}.md
Meeting metrics from markdown headings¶
Dashboard “Meetings over time” counts dated Meeting headings in markdown, not Meeting.created_at insert time. Multiple meetings may live in one file (common in org strategy notes).
Sources scanned¶
- Each organization’s
description_pathfile (usually{org}/notes/strategy.md) - Each meeting reference’s
file_refundernotes_root
Heading rules¶
Match ## or ### headings whose title starts with Meeting and includes a parseable date:
Supported date forms: M/D, M/D/YY, M/D/YYYY (zero-padded allowed). Bare M/D uses the current calendar year, or the previous year if that date would fall more than 30 days in the future.
Ignored (not counted):
- Undated section titles such as
## Meeting notesor## Meetings - Headings with no parseable date (treated as dirty for the audit test)
De-duplication¶
- Every dated heading in a strategy file counts (including two headings on the same day)
- For meeting files, a heading is skipped if strategy already has the same
(org_id, meeting_date)
Persistence and refresh¶
Implementation: app/services/meeting_heading_metrics.py.
| Table | Role |
|---|---|
meeting_heading_events | One row per kept dated heading (org_id, meeting_date, source, source_path, heading_text, heading_line) |
meeting_metrics_meta | Singleton: last_evaluated_at, files_scanned, meetings_found |
Scan runs:
- On app startup (after
init_db; failures are logged and do not block boot) - On
POST /api/metrics/meetings/refresh(Metrics UI “Refresh meetings” button)
API responses for meeting time series include last_evaluated_at.
Frontend¶
Metrics dashboard (Metrics.vue):
- Summary card: meeting count for the selected day window
- Chart title: “Meetings by Month”
- Shows last evaluated timestamp
- Refresh meetings: calls refresh then reloads the dashboard
Auditing a notes root¶
CLI report (preferred for cleaning — per-org table, exit 1 when dirty):
cd backend
uv run python scripts/report_org_meetings.py /path/to/docs/notes
uv run python scripts/report_org_meetings.py /path/to/customers --dirty-only
Integration test (same filesystem rules; fails on dirty headings):
cd backend
MEETING_METRICS_AUDIT_ROOT=/path/to/docs/notes \
uv run pytest tests/it/test_meeting_heading_audit.py -m integration -s -v
Unit coverage for parsing and de-dupe: tests/ut/test_meeting_heading_metrics.py.
Frontend View¶
The Meetings view provides:
- Table listing with meeting ID, organization, project, file path, and date
- Filters by organization and project
- Create modal with markdown editor and live preview
- View modal for reading meeting content
- Edit modal for updating content and associations
- Delete confirmation
Best Practices¶
Meeting ID Convention¶
Use a consistent naming convention for meeting IDs:
- Format:
{type}-{date}-{description} - Examples:
mtg-2026-01-05-weekly-standupreview-2026-01-05-q1-planningonsite-2026-01-05-customer-kickoff
Organization¶
- Always link meetings to the relevant organization
- Link to projects when the meeting is project-specific
- Use descriptive meeting IDs for easy identification
Content Structure¶
- Use consistent heading structure
- For multi-meeting strategy notes, use dated headings (
## Meeting MM/DDor## Meeting MM/DD/YYYY) so metrics can count them - Include attendees for context
- Document action items with task checkboxes
- Add timestamps for key decisions