- Add ARCHITECTURE.md with full system overview (data flow, modules, build) - Add AGENTS.md as a guide for future AI agents working on this project - Split monolithic styles.css into focused files under assets/: variables, animations, layout, navbar, header, cards, config, filters, utils, greet - Fix justfile: replace `cargo run tauri dev/build` with `cargo tauri dev/build` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.6 KiB
Architecture: marstemedia
marstemedia is a desktop (and Android) application built with Tauri v2. The UI is written in Rust compiled to WebAssembly using Yew, and the backend logic runs as native Rust code inside the Tauri host process.
Big Picture
┌─────────────────────────────────────────────────────────┐
│ Tauri Desktop App │
│ │
│ ┌──────────────────────┐ IPC (invoke) ┌──────────┐ │
│ │ Frontend (Wasm) │ ──────────────► │ Backend │ │
│ │ Yew / Rust │ ◄────────────── │ Rust │ │
│ │ src/ │ JSON results │ src-tauri│ │
│ └──────────────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
▲ renders inside a webview (WebKit/Edge)
The frontend never touches the network directly for heavy operations — it sends named commands via window.__TAURI__.core.invoke() to the backend, which does the actual networking, file I/O, and cryptography.
Frontend (src/)
Built with Yew 0.21 (React-style component model for Rust/Wasm) and yew-router for client-side routing.
Entry point
| File | Role |
|---|---|
src/main.rs |
Bootstraps the Wasm app, mounts the App component |
src/app.rs |
Defines the Route enum and the top-level layout (router + navbar) |
Routes
| Path | Component | File |
|---|---|---|
/ |
Home |
src/pages/home.rs |
/news |
News |
src/pages/news.rs |
/greet |
Greet |
src/pages/greet.rs |
Components
Navbar(src/navbar/navbar.rs) — Fixed bottom dock with links to all three routes. Reads the current route viause_routeto highlight the active link.Home(src/pages/home.rs) — Nostr feed. Manages hashtag filter state, invokesfetch_nostr_postson load and on user interaction, renders posts asPostCardcomponents.News(src/pages/news.rs) — Dual-mode news page. Toggles between AI mode (Groq / OpenRouter) and RSS mode. Has a collapsible config panel for API keys and RSS feed management. Renders articles asNewsCardwith Markdown support viapulldown-cmark.
How the frontend talks to the backend
Every backend call uses the invoke wasm-bindgen extern:
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)]
async fn invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;
Arguments are serialized with serde-wasm-bindgen and results are deserialized back into typed Rust structs.
Backend (src-tauri/src/)
Native Rust code that runs in the Tauri host process. Registered commands are the only API surface the frontend can call.
Entry point
| File | Role |
|---|---|
src-tauri/src/main.rs |
Desktop executable entry point, calls run() |
src-tauri/src/lib.rs |
Configures Tauri: registers NewsState, registers all commands |
Modules
home.rs — Nostr
Handles the fetch_nostr_posts command.
- Generates a fresh, random throwaway keypair on every call (privacy by design — no persistent identity).
- Creates an
EasyNostrclient using the temp key (via the localeasy-nostrlibrary). - Connects to three relays:
relay.damus.io,nos.lol,relay.snort.social. - Fetches posts by hashtag list, or random posts if no tags are given.
- Maps results to the serializable
LocalPoststruct and returns JSON.
news.rs — RSS & AI News
Manages NewsState (held in Tauri's managed state, shared across calls):
NewsState {
openrouter_key: Mutex<String>
groq_key: Mutex<String>
rss_config: Mutex<RssConfig>
}
Commands:
| Command | What it does |
|---|---|
load_rss_config |
Reads rss.ron from the app config dir; writes 16 default feeds if the file is missing or has fewer than 3 entries |
save_rss_urls |
Serializes the feed list to rss.ron and updates in-memory state |
save_openrouter_key |
Stores the key in NewsState.openrouter_key (in-memory only, not persisted to disk) |
save_groq_key |
Same for the Groq key |
fetch_ai_news |
POSTs to Groq or OpenRouter chat completions API; asks for a short tech news summary in German Markdown |
fetch_rss_news |
Iterates the in-memory feed list, fetches each feed with reqwest, parses with feed-rs, takes up to 3 articles per feed, returns Vec<NewsArticle> |
Local library: easy-nostr
Located at src-tauri/easy-nostr/ (path dependency). Wraps nostr-sdk to provide high-level helpers like get_random_posts() and get_posts_by_hashtags(). Written by the same author (Bytemalte).
Configuration & Persistence
| What | Where | Format |
|---|---|---|
| RSS feed list | <app_config_dir>/rss.ron |
RON |
| API keys | In-memory (NewsState) — lost on restart |
— |
| App window config | src-tauri/tauri.conf.json |
JSON |
API keys are intentionally not persisted to disk. The user re-enters them each session via the config panel.
Build System
| Tool | Role |
|---|---|
trunk |
Builds and bundles the Yew/Wasm frontend (trunk build / trunk serve) |
tauri-cli |
Wraps the full build and produces the native app bundle |
Trunk.toml |
Trunk configuration (output dir dist/, used by Tauri) |
justfile |
Developer shortcuts |
The tauri.conf.json wires them together:
beforeDevCommand: trunk serve (starts dev server on :1420)
beforeBuildCommand: trunk build (builds Wasm)
frontendDist: ../dist (Tauri embeds this)
CI / Android
.gitea/workflows/android.yaml runs on push to main or version tags. It:
- Clones
easy-nostras a private submodule. - Installs Rust with four Android targets +
wasm32-unknown-unknown. - Installs Trunk (binary) and
tauri-cli(cargo). - Sets up Android SDK + NDK 25.
- Runs
cargo tauri android build.
Data Flow Summary
User action
│
▼
Yew component (state update)
│ invoke("command_name", args_json)
▼
Tauri IPC bridge
│
▼
Backend Rust function
│ network / file I/O
▼
External service (Nostr relay / RSS feed / AI API / disk)
│ serialized result
▼
Yew component re-renders