feat: Add API endpoint for listing oneliners

This commit introduces a new API endpoint `/api/oneliners` that allows
authenticated users to retrieve a list of their recent oneliners.

The endpoint supports conditional GET requests using the `ETag` header
for efficient caching. It also allows specifying a time window for the
oneliners via the `hours` query parameter, with sensible defaults and
clamping.

The implementation includes:
- A new route handler `handle_oneliners`.
- A new type `OnelinerWatermark` for tracking user-specific timestamps.
- Logic for scanning user data directories, filtering by time, and
  handling deleted cases.
- Test cases to cover various scenarios, including caching, time
  windowing, and edge cases.
This commit is contained in:
2026-04-18 09:27:23 +02:00
parent 6fbb549ddf
commit 94392e72d8
10 changed files with 658 additions and 16 deletions
+12 -2
View File
@@ -2,12 +2,13 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::{error, info, warn};
use super::{ffmpeg, ollama, whisper, TranscribeReceiver};
use crate::config::{Config, WhisperUserSettings};
use crate::gazetteer::Gazetteer;
use crate::{BusyGuard, WorkerBusy};
use crate::{BusyGuard, OnelinerWatermark, WorkerBusy};
const ONELINER_TIMEOUT: Duration = Duration::from_secs(60);
@@ -19,6 +20,7 @@ pub async fn run(
client: reqwest::Client,
worker_busy: WorkerBusy,
vocab: Arc<Gazetteer>,
watermark: OnelinerWatermark,
) {
info!("Transcription worker started");
let timeout = Duration::from_secs(config.whisper_timeout_seconds);
@@ -88,7 +90,7 @@ pub async fn run(
if let Some(case_dir) = audio_path.parent()
&& !has_pending_recordings(case_dir).await
{
update_oneliner(case_dir, &client, &config, &vocab).await;
update_oneliner(case_dir, &job.user_slug, &client, &config, &vocab, &watermark).await;
}
}
@@ -127,9 +129,11 @@ async fn mark_failed(audio_path: &Path) {
/// no-op — next transcript write retries.
pub(crate) async fn update_oneliner(
case_dir: &Path,
user_slug: &str,
client: &reqwest::Client,
config: &Config,
vocab: &Gazetteer,
watermark: &OnelinerWatermark,
) {
let path = case_dir.join("oneliner.txt");
@@ -154,6 +158,12 @@ pub(crate) async fn update_oneliner(
error!(path = %path.display(), error = %e, "writing oneliner failed");
} else {
info!(path = %path.display(), chars = line.chars().count(), "Oneliner updated");
// Bump the per-user watermark so the next `/api/oneliners`
// poll sees a fresh ETag and fetches the new content.
watermark
.write()
.await
.insert(user_slug.to_string(), OffsetDateTime::now_utc());
}
}
Err(e) => {