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
This commit is contained in:
RTSDA 2025-06-28 19:49:31 -04:00
parent b8f11b6e10
commit 1494472e3e
3 changed files with 52 additions and 63 deletions

View file

@ -5,7 +5,7 @@ use std::time::Duration;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Settings { pub struct Settings {
pub pocketbase_url: String, pub api_url: String,
pub window_width: i32, pub window_width: i32,
pub window_height: i32, pub window_height: i32,
pub slide_interval_seconds: u64, pub slide_interval_seconds: u64,
@ -40,7 +40,7 @@ impl Settings {
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Self { Self {
pocketbase_url: String::from("https://pocketbase.rockvilletollandsda.church"), api_url: String::from("https://api.rockvilletollandsda.church"),
window_width: 1920, window_width: 1920,
window_height: 1080, window_height: 1080,
slide_interval_seconds: 10, slide_interval_seconds: 10,

View file

@ -1,7 +1,7 @@
mod config; mod config;
mod pocketbase; mod pocketbase;
use crate::pocketbase::PocketbaseEvent; use crate::pocketbase::ApiEvent;
use iced::widget::{column, row, image, container, text}; use iced::widget::{column, row, image, container, text};
use iced::{ use iced::{
window, Element, window, Element,
@ -24,8 +24,8 @@ static SETTINGS: Lazy<config::Settings> = Lazy::new(|| {
}) })
}); });
static POCKETBASE_CLIENT: Lazy<pocketbase::PocketbaseClient> = Lazy::new(|| { static API_CLIENT: Lazy<pocketbase::ApiClient> = Lazy::new(|| {
pocketbase::PocketbaseClient::new(SETTINGS.pocketbase_url.clone()) pocketbase::ApiClient::new(SETTINGS.api_url.clone())
}); });
// Define some constants for styling // 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 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 IMAGE_BG_COLOR: Color = Color::from_rgb(0.08, 0.08, 0.12); // Slightly lighter than background
const LOADING_FRAMES: [&str; 4] = ["", "", "", ""]; 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)] #[derive(Debug)]
struct DigitalSign { struct DigitalSign {
@ -160,6 +160,13 @@ impl IcedProgram for DigitalSign {
tracing::info!("Cleared all existing images"); tracing::info!("Cleared all existing images");
state.events = events; 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.last_refresh = Instant::now();
state.is_fetching = false; state.is_fetching = false;
@ -398,50 +405,38 @@ impl Message {
} }
async fn fetch_events() -> Result<Vec<Event>, anyhow::Error> { async fn fetch_events() -> Result<Vec<Event>, anyhow::Error> {
tracing::info!("Starting to fetch events from Pocketbase"); tracing::info!("Starting to fetch upcoming events from API");
let pb_events = match POCKETBASE_CLIENT.fetch_events().await { let api_events = match API_CLIENT.fetch_events().await {
Ok(events) => { Ok(events) => {
tracing::info!("Successfully fetched {} events from Pocketbase", events.len()); tracing::info!("Successfully fetched {} upcoming events from API", events.len());
events events
}, },
Err(e) => { Err(e) => {
tracing::error!("Failed to fetch events from Pocketbase: {}", e); tracing::error!("Failed to fetch events from API: {}", e);
return Err(e); return Err(e);
} }
}; };
// Use a 12-hour window for filtering // Convert API events to display events (no filtering needed since /upcoming endpoint handles it)
let now = chrono::Utc::now() - chrono::Duration::hours(12); let mut events: Vec<Event> = api_events
let mut events: Vec<Event> = pb_events
.into_iter() .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) .map(Event::from)
.collect(); .collect();
if events.is_empty() { if events.is_empty() {
tracing::warn!("No current or future events found"); tracing::warn!("No upcoming events found");
} else { } else {
tracing::info!( tracing::info!(
"Found {} events, from {} to {}", "Found {} upcoming events, from {} to {}",
events.len(), events.len(),
events.first().map(|e| e.date.as_str()).unwrap_or("unknown"), events.first().map(|e| e.date.as_str()).unwrap_or("unknown"),
events.last().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)); 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) Ok(events)
} }
@ -492,8 +487,8 @@ async fn load_image(url: String) -> image::Handle {
} }
} }
impl From<PocketbaseEvent> for Event { impl From<ApiEvent> for Event {
fn from(event: PocketbaseEvent) -> Self { fn from(event: ApiEvent) -> Self {
let clean_description = html2text::from_read(event.description.as_bytes(), 80) let clean_description = html2text::from_read(event.description.as_bytes(), 80)
.replace('\n', " ") .replace('\n', " ")
.split_whitespace() .split_whitespace()
@ -504,16 +499,10 @@ impl From<PocketbaseEvent> for Event {
let start_time = event.start_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string(); 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 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 image_url = event.image.clone();
let url = format!( if let Some(ref url) = image_url {
"{}/api/files/events/{}/{}", tracing::info!("Using image URL: {}", url);
SETTINGS.pocketbase_url, }
event.id,
img
);
tracing::info!("Constructed image URL: {}", url);
url
});
Self { Self {
title: event.title, title: event.title,
@ -537,7 +526,7 @@ fn main() -> iced::Result {
.init(); .init();
tracing::info!("Starting Beacon Digital Signage"); tracing::info!("Starting Beacon Digital Signage");
tracing::info!("Pocketbase URL: {}", SETTINGS.pocketbase_url); tracing::info!("API URL: {}", SETTINGS.api_url);
// Load the icon file // Load the icon file
let icon_data = { let icon_data = {

View file

@ -3,10 +3,10 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
const POCKETBASE_TIMEOUT: Duration = Duration::from_secs(10); const API_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct PocketbaseEvent { pub struct ApiEvent {
pub id: String, pub id: String,
pub title: String, pub title: String,
pub description: String, pub description: String,
@ -18,35 +18,29 @@ pub struct PocketbaseEvent {
pub thumbnail: Option<String>, pub thumbnail: Option<String>,
pub category: String, pub category: String,
pub is_featured: bool, pub is_featured: bool,
pub reoccuring: String, pub recurring_type: Option<String>,
pub created: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated: DateTime<Utc>, pub updated_at: DateTime<Utc>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PocketbaseClient { pub struct ApiClient {
client: reqwest::Client, client: reqwest::Client,
base_url: String, base_url: String,
} }
impl PocketbaseClient { impl ApiClient {
pub fn new(base_url: String) -> Self { pub fn new(base_url: String) -> Self {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.timeout(POCKETBASE_TIMEOUT) .timeout(API_TIMEOUT)
.build() .build()
.expect("Failed to create HTTP client"); .expect("Failed to create HTTP client");
Self { client, base_url } Self { client, base_url }
} }
pub async fn fetch_events(&self) -> Result<Vec<PocketbaseEvent>> { pub async fn fetch_events(&self) -> Result<Vec<ApiEvent>> {
// Subtract 12 hours from now to include upcoming events let url = format!("{}/api/events/upcoming", self.base_url);
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
);
tracing::info!("Fetching events from URL: {}", url); tracing::info!("Fetching events from URL: {}", url);
let response = match self.client.get(&url) let response = match self.client.get(&url)
@ -73,15 +67,21 @@ impl PocketbaseClient {
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct Response { struct ApiResponse {
items: Vec<PocketbaseEvent>, success: bool,
data: Vec<ApiEvent>,
} }
match response.json().await { match response.json().await {
Ok(data) => { Ok(api_response) => {
let Response { items } = data; let ApiResponse { success, data } = api_response;
tracing::info!("Successfully parsed {} events from response", items.len()); if success {
Ok(items) 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) => { Err(e) => {
tracing::error!("Failed to parse JSON response: {}", e); tracing::error!("Failed to parse JSON response: {}", e);