feat: add plan step

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-03-15 22:38:18 +01:00
parent 7eb6ae7cbb
commit 04e452ecc3
71 changed files with 1059 additions and 319 deletions

View File

@@ -11,6 +11,7 @@ mod templates;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use forage_core::session::{FileSessionStore, SessionStore};
use forage_db::PgSessionStore;
@@ -54,8 +55,8 @@ fn init_telemetry() {
)
.build();
let otel_layer = tracing_opentelemetry::layer()
.with_tracer(tracer_provider.tracer("forage-server"));
let otel_layer =
tracing_opentelemetry::layer().with_tracer(tracer_provider.tracer("forage-server"));
tracing_subscriber::registry()
.with(env_filter)
@@ -119,7 +120,10 @@ async fn main() -> anyhow::Result<()> {
let mut mad = notmad::Mad::builder();
// Session store + integration store: PostgreSQL if DATABASE_URL is set
let (sessions, integration_store): (Arc<dyn SessionStore>, Option<Arc<dyn forage_core::integrations::IntegrationStore>>);
let (sessions, integration_store): (
Arc<dyn SessionStore>,
Option<Arc<dyn forage_core::integrations::IntegrationStore>>,
);
if let Ok(database_url) = std::env::var("DATABASE_URL") {
tracing::info!("using PostgreSQL session store");
@@ -129,12 +133,16 @@ async fn main() -> anyhow::Result<()> {
let pg_store = Arc::new(PgSessionStore::new(pool.clone()));
// Integration store (uses same pool)
let encryption_key = std::env::var("INTEGRATION_ENCRYPTION_KEY")
.unwrap_or_else(|_| {
tracing::warn!("INTEGRATION_ENCRYPTION_KEY not set — using default key (not safe for production)");
"forage-dev-key-not-for-production!!".to_string()
});
let pg_integrations = Arc::new(forage_db::PgIntegrationStore::new(pool, encryption_key.into_bytes()));
let encryption_key = std::env::var("INTEGRATION_ENCRYPTION_KEY").unwrap_or_else(|_| {
tracing::warn!(
"INTEGRATION_ENCRYPTION_KEY not set — using default key (not safe for production)"
);
"forage-dev-key-not-for-production!!".to_string()
});
let pg_integrations = Arc::new(forage_db::PgIntegrationStore::new(
pool,
encryption_key.into_bytes(),
));
// Session reaper component
mad.add(session_reaper::PgSessionReaper {
@@ -143,11 +151,15 @@ async fn main() -> anyhow::Result<()> {
});
sessions = pg_store;
integration_store = Some(pg_integrations as Arc<dyn forage_core::integrations::IntegrationStore>);
integration_store =
Some(pg_integrations as Arc<dyn forage_core::integrations::IntegrationStore>);
} else {
let session_dir = std::env::var("SESSION_DIR").unwrap_or_else(|_| "target/sessions".into());
tracing::info!("using file session store at {session_dir} (set DATABASE_URL for PostgreSQL)");
let file_store = Arc::new(FileSessionStore::new(&session_dir).expect("failed to create session dir"));
tracing::info!(
"using file session store at {session_dir} (set DATABASE_URL for PostgreSQL)"
);
let file_store =
Arc::new(FileSessionStore::new(&session_dir).expect("failed to create session dir"));
// File session reaper component
mad.add(session_reaper::FileSessionReaper {
@@ -159,8 +171,13 @@ async fn main() -> anyhow::Result<()> {
};
let forest_client = Arc::new(forest_client);
let mut state = AppState::new(template_engine, forest_client.clone(), forest_client.clone(), sessions)
.with_grpc_client(forest_client.clone());
let mut state = AppState::new(
template_engine,
forest_client.clone(),
forest_client.clone(),
sessions,
)
.with_grpc_client(forest_client.clone());
// Slack OAuth config (optional, enables "Add to Slack" button)
if let (Ok(client_id), Ok(client_secret)) = (
@@ -220,7 +237,9 @@ async fn main() -> anyhow::Result<()> {
});
} else {
// Fallback: direct dispatch (no durability)
tracing::warn!("NATS_URL not set — using direct notification dispatch (no durability)");
tracing::warn!(
"NATS_URL not set — using direct notification dispatch (no durability)"
);
mad.add(notification_worker::NotificationListener {
grpc: forest_client,
store: store.clone(),
@@ -234,12 +253,11 @@ async fn main() -> anyhow::Result<()> {
}
// HTTP server component
mad.add(serve_http::ServeHttp {
addr,
state,
});
mad.add(serve_http::ServeHttp { addr, state });
mad.run().await?;
mad.cancellation(Some(Duration::from_secs(10)))
.run()
.await?;
Ok(())
}