maarcadetweet: initial commit

AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit.

- PDS (Rust + axum + sqlx)
  - Auth: createAccount, createSession, refreshSession
  - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE)
  - Feed: feed.like.create, feed.repost.create
  - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos
  - Identity: resolveHandle
  - MST: spec-conformant (at-mst crate, 27 tests)
  - Repo: signed commits, TID counter (monotonic, 4096 wrap safe)

- AppView (Rust + axum + sqlx)
  - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed)
  - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration)
  - Handle-sync worker (did:plc + did:web)
  - JSONB embed storage + thread columns (migration 0003)
  - Like/repost counter cache (migration 0004)

- Tauri 2 + Svelte 5 Desktop Client
  - System tray (Show/Compose/Quit menu)
  - OS notifications (tauri-plugin-notification)
  - Auto-update (tauri-plugin-updater, placeholder endpoint)
  - Window-state (tauri-plugin-window-state)
  - 160-char compose with live counter
  - Image/Link embed rendering
  - LocalStorage-persisted like state
  - Timeline with poll (prepend new posts)
  - Custom TitleBar (transparent, no decorations)
  - Orange/IBM Plex Mono maarcade design

Tests: 231 Rust + 9 vitest = 240 passed.
This commit is contained in:
tomdebone
2026-07-05 20:01:31 +02:00
commit c586fd39c9
134 changed files with 35279 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
use serde::Deserialize;
/// Default polling interval for the handle-sync worker, in seconds.
/// Bumped to 5 minutes — handle changes are infrequent and a missing
/// `@handle` is purely cosmetic, so we don't need to hammer the PLC.
fn default_handle_sync_interval() -> u64 {
300
}
#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
pub pds_host: String,
pub pds_port: u16,
pub pds_public_url: String,
pub pds_handle_dns_zone: String,
pub pds_jwt_secret: String,
pub appview_host: String,
pub appview_port: u16,
pub appview_public_url: String,
pub jetstream_url: String,
pub jetstream_collections: Vec<String>,
pub database_url_pds: String,
pub database_url_appview: String,
pub s3_endpoint: String,
pub s3_region: String,
pub s3_access_key: String,
pub s3_secret_key: String,
pub s3_bucket_pds: String,
pub s3_bucket_appview: String,
pub plc_directory_url: String,
/// Optional shared secret for `POST /internal/ingest-commit`. If unset,
/// the endpoint accepts anonymous requests (dev mode). If set, callers
/// must send `X-Ingest-Secret: <value>`.
#[serde(default)]
pub appview_ingest_secret: Option<String>,
/// How often the handle-sync worker scans the `posts` table for rows
/// with an empty `handle` column and resolves them via the PLC
/// directory. Default: 300s (5 minutes).
#[serde(default = "default_handle_sync_interval")]
pub appview_handle_sync_interval_secs: u64,
}
impl AppConfig {
pub fn from_env() -> anyhow::Result<Self> {
let env = |k: &str| std::env::var(k).map_err(|_| anyhow::anyhow!("missing env: {k}"));
let collections: Vec<String> = env("JETSTREAM_COLLECTIONS")?
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(Self {
pds_host: env("PDS_HOST")?,
pds_port: env("PDS_PORT")?.parse()?,
pds_public_url: env("PDS_PUBLIC_URL")?,
pds_handle_dns_zone: env("PDS_HANDLE_DNS_ZONE")?,
pds_jwt_secret: env("PDS_JWT_SECRET")?,
appview_host: env("APPVIEW_HOST")?,
appview_port: env("APPVIEW_PORT")?.parse()?,
appview_public_url: env("APPVIEW_PUBLIC_URL")?,
jetstream_url: env("JETSTREAM_URL")?,
jetstream_collections: collections,
database_url_pds: env("DATABASE_URL_PDS")?,
database_url_appview: env("DATABASE_URL_APPVIEW")?,
s3_endpoint: env("S3_ENDPOINT")?,
s3_region: env("S3_REGION")?,
s3_access_key: env("S3_ACCESS_KEY")?,
s3_secret_key: env("S3_SECRET_KEY")?,
s3_bucket_pds: env("S3_BUCKET_PDS")?,
s3_bucket_appview: env("S3_BUCKET_APPVIEW")?,
plc_directory_url: env("PLC_DIRECTORY_URL")?,
appview_ingest_secret: std::env::var("APPVIEW_INGEST_SECRET").ok(),
appview_handle_sync_interval_secs: std::env::var("APPVIEW_HANDLE_SYNC_INTERVAL_SECS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_else(default_handle_sync_interval),
})
}
}