From 1494472e3eca8f9a8624beb8f037e3fcd7e14320 Mon Sep 17 00:00:00 2001 From: RTSDA Date: Sat, 28 Jun 2025 19:49:31 -0400 Subject: [PATCH] Update API integration from PocketBase to new church API - Replace PocketBase integration with new church API at api.rockvilletollandsda.church - Use /api/events/upcoming endpoint to eliminate client-side filtering - Update data structures to match new API response format - Simplify event fetching logic by leveraging server-side filtering - Increase image size limit to 2MB for better image support - Rename PocketbaseEvent to ApiEvent and PocketbaseClient to ApiClient - Update configuration to use api_url instead of pocketbase_url --- src/config.rs | 4 +-- src/main.rs | 67 ++++++++++++++++++++--------------------------- src/pocketbase.rs | 44 +++++++++++++++---------------- 3 files changed, 52 insertions(+), 63 deletions(-) diff --git a/src/config.rs b/src/config.rs index 7985795..d3e0b7f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,7 @@ use std::time::Duration; #[derive(Debug, Deserialize)] pub struct Settings { - pub pocketbase_url: String, + pub api_url: String, pub window_width: i32, pub window_height: i32, pub slide_interval_seconds: u64, @@ -40,7 +40,7 @@ impl Settings { impl Default for Settings { fn default() -> Self { Self { - pocketbase_url: String::from("https://pocketbase.rockvilletollandsda.church"), + api_url: String::from("https://api.rockvilletollandsda.church"), window_width: 1920, window_height: 1080, slide_interval_seconds: 10, diff --git a/src/main.rs b/src/main.rs index 210befd..085d9c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod config; mod pocketbase; -use crate::pocketbase::PocketbaseEvent; +use crate::pocketbase::ApiEvent; use iced::widget::{column, row, image, container, text}; use iced::{ window, Element, @@ -24,8 +24,8 @@ static SETTINGS: Lazy = Lazy::new(|| { }) }); -static POCKETBASE_CLIENT: Lazy = Lazy::new(|| { - pocketbase::PocketbaseClient::new(SETTINGS.pocketbase_url.clone()) +static API_CLIENT: Lazy = Lazy::new(|| { + pocketbase::ApiClient::new(SETTINGS.api_url.clone()) }); // Define some constants for styling @@ -41,7 +41,7 @@ const TIME_COLOR: Color = Color::from_rgb(0.8, 0.8, 0.95); // Soft purple-grey const LOCATION_ICON_COLOR: Color = Color::from_rgb(0.6, 0.4, 0.9); // Brighter purple const IMAGE_BG_COLOR: Color = Color::from_rgb(0.08, 0.08, 0.12); // Slightly lighter than background const LOADING_FRAMES: [&str; 4] = ["⠋", "⠙", "⠹", "⠸"]; -const MAX_IMAGE_SIZE: u64 = 500 * 1024; // 500KB limit +const MAX_IMAGE_SIZE: u64 = 2 * 1024 * 1024; // 2MB limit #[derive(Debug)] struct DigitalSign { @@ -160,6 +160,13 @@ impl IcedProgram for DigitalSign { tracing::info!("Cleared all existing images"); state.events = events; + + // Reset current event index if needed + if state.current_event_index >= state.events.len() && !state.events.is_empty() { + tracing::info!("Resetting current event index from {} to 0", state.current_event_index); + state.current_event_index = 0; + } + state.last_refresh = Instant::now(); state.is_fetching = false; @@ -398,50 +405,38 @@ impl Message { } async fn fetch_events() -> Result, anyhow::Error> { - tracing::info!("Starting to fetch events from Pocketbase"); - let pb_events = match POCKETBASE_CLIENT.fetch_events().await { + tracing::info!("Starting to fetch upcoming events from API"); + let api_events = match API_CLIENT.fetch_events().await { Ok(events) => { - tracing::info!("Successfully fetched {} events from Pocketbase", events.len()); + tracing::info!("Successfully fetched {} upcoming events from API", events.len()); events }, Err(e) => { - tracing::error!("Failed to fetch events from Pocketbase: {}", e); + tracing::error!("Failed to fetch events from API: {}", e); return Err(e); } }; - // Use a 12-hour window for filtering - let now = chrono::Utc::now() - chrono::Duration::hours(12); - let mut events: Vec = pb_events + // Convert API events to display events (no filtering needed since /upcoming endpoint handles it) + let mut events: Vec = api_events .into_iter() - .filter(|event| { - let is_current = event.end_time > now; - if !is_current { - tracing::info!( - "Filtering out event '{}' with end time {} (now is {})", - event.title, - event.end_time, - now - ); - } - is_current - }) .map(Event::from) .collect(); if events.is_empty() { - tracing::warn!("No current or future events found"); + tracing::warn!("No upcoming events found"); } else { tracing::info!( - "Found {} events, from {} to {}", + "Found {} upcoming events, from {} to {}", events.len(), events.first().map(|e| e.date.as_str()).unwrap_or("unknown"), events.last().map(|e| e.date.as_str()).unwrap_or("unknown") ); } + // Sort by start time (API should already provide them sorted, but ensure consistency) events.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); - tracing::info!("Processed and sorted {} current/future events", events.len()); + tracing::info!("Processed {} upcoming events", events.len()); Ok(events) } @@ -492,8 +487,8 @@ async fn load_image(url: String) -> image::Handle { } } -impl From for Event { - fn from(event: PocketbaseEvent) -> Self { +impl From for Event { + fn from(event: ApiEvent) -> Self { let clean_description = html2text::from_read(event.description.as_bytes(), 80) .replace('\n', " ") .split_whitespace() @@ -504,16 +499,10 @@ impl From for Event { let start_time = event.start_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string(); let end_time = event.end_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string(); - let image_url = event.image.map(|img| { - let url = format!( - "{}/api/files/events/{}/{}", - SETTINGS.pocketbase_url, - event.id, - img - ); - tracing::info!("Constructed image URL: {}", url); - url - }); + let image_url = event.image.clone(); + if let Some(ref url) = image_url { + tracing::info!("Using image URL: {}", url); + } Self { title: event.title, @@ -537,7 +526,7 @@ fn main() -> iced::Result { .init(); tracing::info!("Starting Beacon Digital Signage"); - tracing::info!("Pocketbase URL: {}", SETTINGS.pocketbase_url); + tracing::info!("API URL: {}", SETTINGS.api_url); // Load the icon file let icon_data = { diff --git a/src/pocketbase.rs b/src/pocketbase.rs index a0f4762..b04f092 100644 --- a/src/pocketbase.rs +++ b/src/pocketbase.rs @@ -3,10 +3,10 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::time::Duration; -const POCKETBASE_TIMEOUT: Duration = Duration::from_secs(10); +const API_TIMEOUT: Duration = Duration::from_secs(10); #[derive(Debug, Serialize, Deserialize)] -pub struct PocketbaseEvent { +pub struct ApiEvent { pub id: String, pub title: String, pub description: String, @@ -18,35 +18,29 @@ pub struct PocketbaseEvent { pub thumbnail: Option, pub category: String, pub is_featured: bool, - pub reoccuring: String, - pub created: DateTime, - pub updated: DateTime, + pub recurring_type: Option, + pub created_at: DateTime, + pub updated_at: DateTime, } #[derive(Debug, Clone)] -pub struct PocketbaseClient { +pub struct ApiClient { client: reqwest::Client, base_url: String, } -impl PocketbaseClient { +impl ApiClient { pub fn new(base_url: String) -> Self { let client = reqwest::Client::builder() - .timeout(POCKETBASE_TIMEOUT) + .timeout(API_TIMEOUT) .build() .expect("Failed to create HTTP client"); Self { client, base_url } } - pub async fn fetch_events(&self) -> Result> { - // Subtract 12 hours from now to include upcoming events - let now = (chrono::Utc::now() - chrono::Duration::hours(12)).to_rfc3339(); - let url = format!( - "{}/api/collections/events/records?filter=(end_time>='{}')", - self.base_url, - now - ); + pub async fn fetch_events(&self) -> Result> { + let url = format!("{}/api/events/upcoming", self.base_url); tracing::info!("Fetching events from URL: {}", url); let response = match self.client.get(&url) @@ -73,15 +67,21 @@ impl PocketbaseClient { }; #[derive(Deserialize)] - struct Response { - items: Vec, + struct ApiResponse { + success: bool, + data: Vec, } match response.json().await { - Ok(data) => { - let Response { items } = data; - tracing::info!("Successfully parsed {} events from response", items.len()); - Ok(items) + Ok(api_response) => { + let ApiResponse { success, data } = api_response; + if success { + tracing::info!("Successfully parsed {} events from response", data.len()); + Ok(data) + } else { + tracing::error!("API returned success: false"); + Err(anyhow::anyhow!("API request failed")) + } }, Err(e) => { tracing::error!("Failed to parse JSON response: {}", e);