feat(templates): persist template_id; finalize_meeting returns it (T8.1, FR-NOTE-5)

NewMeeting/Meeting gain template_id; finalize_meeting now returns it
(fetched via one cheap scalar SELECT) so callers that only have
segments/speakers in memory (not a fetched Meeting) can resolve the
template without a separate round-trip.
This commit is contained in:
iamdoubz
2026-07-02 11:25:41 -05:00
parent f63076ad06
commit 6b5b359438
+32 -6
View File
@@ -36,6 +36,11 @@ impl From<sqlx::Error> for StoreError {
pub struct NewMeeting {
pub title: String,
pub calendar_event_id: Option<String>,
/// Note-template id (Phase 8, T8.1, FR-NOTE-5) — resolved against
/// `notes::templates::catalog()`, kept alongside the meeting so
/// re-rendering notes.md on reprocess/resume reapplies the same
/// section structure instead of losing it.
pub template_id: Option<String>,
}
/// `list_meetings` filters (Phase 8, FR-SEARCH-2). All fields are ANDed
@@ -89,6 +94,9 @@ pub struct Meeting {
pub calendar_event_id: Option<String>,
/// Tags (Phase 8, FR-SEARCH-2), sorted.
pub tags: Vec<String>,
/// Note-template id (Phase 8, T8.1, FR-NOTE-5), if one was picked at
/// creation — resolved against `notes::templates::catalog()`.
pub template_id: Option<String>,
}
/// A calendar event with its attendees (T6.3/T6.4, FR-CAL-1/3) — the
@@ -123,7 +131,14 @@ pub struct Retention {
#[async_trait]
pub trait Store: Send + Sync {
async fn create_meeting(&self, m: NewMeeting) -> Result<MeetingId, StoreError>;
async fn finalize_meeting(&self, id: &MeetingId, s: FinalizeMeeting) -> Result<(), StoreError>;
/// Returns the meeting's `template_id` (set at creation, Phase 8 T8.1) so
/// callers can re-render notes.md with the same section structure
/// without a separate fetch.
async fn finalize_meeting(
&self,
id: &MeetingId,
s: FinalizeMeeting,
) -> Result<Option<String>, StoreError>;
async fn list_meetings(
&self,
filter: MeetingFilter,
@@ -490,8 +505,8 @@ impl Store for SqliteStore {
let audio_path = folder.join("audio.wav");
let now = now_unix();
sqlx::query(
"INSERT INTO meetings (id, title, started_at, folder_path, audio_path, status, calendar_event_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'recording', ?, ?, ?)",
"INSERT INTO meetings (id, title, started_at, folder_path, audio_path, status, calendar_event_id, template_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'recording', ?, ?, ?, ?)",
)
.bind(&id)
.bind(&m.title)
@@ -499,6 +514,7 @@ impl Store for SqliteStore {
.bind(folder.display().to_string())
.bind(audio_path.display().to_string())
.bind(&m.calendar_event_id)
.bind(&m.template_id)
.bind(now)
.bind(now)
.execute(&self.pool)
@@ -506,7 +522,11 @@ impl Store for SqliteStore {
Ok(id)
}
async fn finalize_meeting(&self, id: &MeetingId, s: FinalizeMeeting) -> Result<(), StoreError> {
async fn finalize_meeting(
&self,
id: &MeetingId,
s: FinalizeMeeting,
) -> Result<Option<String>, StoreError> {
let now = now_unix();
sqlx::query(
"UPDATE meetings SET status = 'ready', ended_at = ?, duration_secs = ?, recorded = ?,
@@ -553,7 +573,12 @@ impl Store for SqliteStore {
serde_json::to_string_pretty(&transcript).map_err(|e| StoreError::Db(e.to_string()))?;
std::fs::write(paths::meeting_dir(id).join("transcript.json"), json)?;
self.reindex_fts(id).await?;
Ok(())
let template_id: Option<String> =
sqlx::query_scalar("SELECT template_id FROM meetings WHERE id = ?")
.bind(id)
.fetch_one(&self.pool)
.await?;
Ok(template_id)
}
async fn list_meetings(
@@ -625,7 +650,7 @@ impl Store for SqliteStore {
async fn get_meeting(&self, id: &MeetingId) -> Result<Meeting, StoreError> {
let row = sqlx::query(
"SELECT id, title, started_at, ended_at, duration_secs, status, recorded, language, backend_used, model_used, calendar_event_id
"SELECT id, title, started_at, ended_at, duration_secs, status, recorded, language, backend_used, model_used, calendar_event_id, template_id
FROM meetings WHERE id = ?",
)
.bind(id)
@@ -696,6 +721,7 @@ impl Store for SqliteStore {
summary,
calendar_event_id: row.get("calendar_event_id"),
tags,
template_id: row.get("template_id"),
})
}