Files
marstemedia/ARCHITECTURE.md
Bytemalte 0d531d5b7c docs: add ARCHITECTURE.md and AGENTS.md, split CSS, fix justfile
- 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>
2026-03-16 15:02:36 +01:00

175 lines
6.6 KiB
Markdown

# 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 via `use_route` to highlight the active link.
- **`Home`** (`src/pages/home.rs`) — Nostr feed. Manages hashtag filter state, invokes `fetch_nostr_posts` on load and on user interaction, renders posts as `PostCard` components.
- **`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 as `NewsCard` with Markdown support via `pulldown-cmark`.
### How the frontend talks to the backend
Every backend call uses the `invoke` wasm-bindgen extern:
```rust
#[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.
1. Generates a fresh, random throwaway keypair on every call (privacy by design — no persistent identity).
2. Creates an `EasyNostr` client using the temp key (via the local `easy-nostr` library).
3. Connects to three relays: `relay.damus.io`, `nos.lol`, `relay.snort.social`.
4. Fetches posts by hashtag list, or random posts if no tags are given.
5. Maps results to the serializable `LocalPost` struct 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:
1. Clones `easy-nostr` as a private submodule.
2. Installs Rust with four Android targets + `wasm32-unknown-unknown`.
3. Installs Trunk (binary) and `tauri-cli` (cargo).
4. Sets up Android SDK + NDK 25.
5. 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
```