From 0519c7ee1983db48ec16fb5d4464a3a876d305e2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:33:45 +0000 Subject: [PATCH 1/5] Design: Add recent activity & offline duration tracking feature This design document outlines a new feature to track player session history and show recent activity with offline durations. Key features: - Persistent session tracking in SQLite - New /oxeye recent command showing online and recently offline players - Enhanced /oxeye status with last seen information - 30-day session retention with automatic cleanup - Backward compatible, no breaking changes Co-authored-by: Adhitya Thirumala --- RECENT_ACTIVITY_DESIGN.md | 329 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 RECENT_ACTIVITY_DESIGN.md diff --git a/RECENT_ACTIVITY_DESIGN.md b/RECENT_ACTIVITY_DESIGN.md new file mode 100644 index 0000000..d5d15cd --- /dev/null +++ b/RECENT_ACTIVITY_DESIGN.md @@ -0,0 +1,329 @@ +# Recent Activity & Offline Duration Feature Design + +## Overview + +This feature extends Oxeye to track **recent player activity** and **offline duration**, enabling users to see who's been on their Minecraft servers recently and how long they've been offline. + +## Problem Statement + +Currently, Oxeye only tracks players who are **currently online**. Once a player disconnects, all information about their session is lost. Users want to: + +1. See which players have been on recently (even if they're currently offline) +2. Know how long a player has been offline +3. Understand recent server activity patterns + +## Design Decisions + +### 1. Data Storage Strategy + +**Choice: Persistent SQLite table for player sessions** + +We'll create a new `player_sessions` table to track historical join/leave events: + +```sql +CREATE TABLE player_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + api_key_hash TEXT NOT NULL, + player_name TEXT NOT NULL, + joined_at INTEGER NOT NULL, -- Unix timestamp + left_at INTEGER, -- NULL if still online + FOREIGN KEY (api_key_hash) REFERENCES servers(api_key_hash) ON DELETE CASCADE +); + +CREATE INDEX idx_sessions_server_time ON player_sessions(api_key_hash, left_at DESC); +CREATE INDEX idx_sessions_player ON player_sessions(player_name); +``` + +**Why persistent storage?** +- Recent activity is valuable historical data that should survive backend restarts +- Users expect to query "who was on yesterday" even after server maintenance +- Session history enables future analytics features + +### 2. Session Tracking + +**When to create/update sessions:** + +1. **Player joins** (`POST /join`) + - Create a new session record with `joined_at` timestamp + - `left_at` is NULL (still online) + +2. **Player leaves** (`POST /leave`) + - Update the session record, setting `left_at` to current timestamp + +3. **Server sync** (`POST /sync`) + - For players in the sync list who aren't in cache: create new sessions + - For players in cache who aren't in sync list: close their sessions (set `left_at`) + +4. **Server disconnection** + - Close all open sessions for that server (set `left_at` to current time) + +### 3. Data Retention + +**Retention policy: 30 days** + +Sessions older than 30 days should be automatically pruned to prevent unbounded growth: + +```sql +DELETE FROM player_sessions +WHERE left_at < ?1 + AND left_at IS NOT NULL; +``` + +This cleanup can run: +- On database initialization +- Periodically (e.g., daily via a background task) +- Before querying recent activity + +**Why 30 days?** +- Balances historical insight with database size +- Configurable via environment variable if users want more/less +- Most "recent activity" queries care about the last few days, not months + +### 4. Query Interface + +**New database methods:** + +```rust +impl Database { + /// Get recent sessions for a server (active or recently ended). + /// Returns sessions that ended within `window_secs` or are still active. + pub async fn get_recent_sessions( + &self, + api_key_hash: &str, + window_secs: i64, + now: i64 + ) -> Result>; + + /// Get all recent sessions across all servers in a guild. + pub async fn get_recent_sessions_for_guild( + &self, + guild_id: u64, + window_secs: i64, + now: i64 + ) -> Result>; + + /// Cleanup old sessions (called periodically). + pub async fn cleanup_old_sessions(&self, before: i64) -> Result; +} + +pub struct PlayerSession { + pub player_name: PlayerName, + pub joined_at: i64, + pub left_at: Option, // None if still online +} + +pub struct ServerRecentSessions { + pub server_name: String, + pub sessions: Vec, +} +``` + +### 5. Discord Commands + +**New command: `/oxeye recent [server] [timeframe]`** + +Shows players who have been online recently, including offline duration. + +**Parameters:** +- `server` (optional): Server name (defaults to all servers) +- `timeframe` (optional): How far back to look (default: 24h) + - Options: `1h`, `6h`, `12h`, `24h`, `7d`, `30d` + +**Example output:** + +``` +┌──────────────────────────────────────────────────┐ +│ 📊 Recent Activity - survival │ +├──────────────────────────────────────────────────┤ +│ 🟢 Currently Online (3) │ +│ • Steve (joined 2h ago) │ +│ • Alex (joined 45m ago) │ +│ • Notch (joined 5m ago) │ +│ │ +│ 🔴 Recently Offline (5) │ +│ • jeb_ (offline 30m) │ +│ • Dinnerbone (offline 2h) │ +│ • Herobrine (offline 5h) │ +│ • Dream (offline 18h) │ +│ • Technoblade (offline 23h) │ +│ │ +│ Oxeye │ +└──────────────────────────────────────────────────┘ +``` + +**Enhanced `/oxeye status` command:** + +Update the existing status command to show last seen time for offline players: + +``` +┌──────────────────────────────────────────────────┐ +│ 🟢 survival — 3 online │ +├──────────────────────────────────────────────────┤ +│ Steve (joined 2h ago) │ +│ Alex (joined 45m ago) │ +│ Notch (joined 5m ago) │ +│ │ +│ Last seen: │ +│ • jeb_ (30m ago) │ +│ • Dinnerbone (2h ago) │ +│ │ +│ Oxeye │ +└──────────────────────────────────────────────────┘ +``` + +### 6. API Changes + +No new HTTP endpoints needed. The existing endpoints will be enhanced to track sessions: + +**`POST /join`** - Create new session record +**`POST /leave`** - Close session record +**`POST /sync`** - Reconcile session records with current player list +**`POST /disconnect`** - Close all open sessions for the server + +### 7. Performance Considerations + +**Indexes:** +- `idx_sessions_server_time` - Enables fast queries for recent sessions by server +- `idx_sessions_player` - Enables fast lookups of a specific player's history + +**Query efficiency:** +```sql +-- Get recent sessions (last 24h) for a server +SELECT player_name, joined_at, left_at +FROM player_sessions +WHERE api_key_hash = ?1 + AND (left_at IS NULL OR left_at > ?2) +ORDER BY + CASE WHEN left_at IS NULL THEN 0 ELSE 1 END, -- Online first + COALESCE(left_at, joined_at) DESC +LIMIT 100; +``` + +**Expected overhead:** +- INSERT on player join: ~1ms +- UPDATE on player leave: ~1ms +- Storage: ~100 bytes per session record +- For 100 players with 10 sessions/day each: ~100KB/day, ~3MB/month + +This is negligible compared to the skin/image caching overhead already in the system. + +### 8. Time Formatting + +**Human-readable durations:** + +```rust +fn format_duration(seconds: i64) -> String { + match seconds { + s if s < 60 => format!("{}s", s), + s if s < 3600 => format!("{}m", s / 60), + s if s < 86400 => format!("{}h", s / 3600), + s => format!("{}d", s / 86400), + } +} +``` + +Examples: +- `30s` - 30 seconds +- `45m` - 45 minutes +- `2h` - 2 hours +- `5d` - 5 days + +### 9. Edge Cases + +1. **Server restart without sync** + - Open sessions remain open until next sync + - Status command shows "⚠️ Server not synced since backend restart" + +2. **Player rejoins before session timeout** + - Close previous session (set `left_at`) + - Create new session record + - Prevents inflated "online time" from single mega-session + +3. **Backend restart** + - All open sessions in DB show `left_at = NULL` + - On first sync per server, close old sessions + - Cache is rebuilt from server sync + +4. **Time zones** + - All timestamps are UTC (Unix timestamps) + - Duration calculations are timezone-agnostic + - Discord renders relative times ("2h ago") automatically + +## Implementation Plan + +### Phase 1: Database Layer (Core) +1. Add `player_sessions` table to schema migration +2. Implement session tracking methods: + - `create_session()` + - `close_session()` + - `get_recent_sessions()` + - `cleanup_old_sessions()` +3. Add database tests for session tracking + +### Phase 2: API Integration +1. Modify `POST /join` to create session records +2. Modify `POST /leave` to close session records +3. Modify `POST /sync` to reconcile sessions +4. Add cleanup task on database initialization + +### Phase 3: Discord Commands +1. Add `/oxeye recent` command +2. Enhance `/oxeye status` to show last seen +3. Add time formatting helper functions + +### Phase 4: Testing & Polish +1. Integration tests for session tracking +2. Test edge cases (disconnects, restarts, etc.) +3. Documentation updates + +## Configuration + +**New environment variables:** + +```bash +# Session retention period (days) +SESSION_RETENTION_DAYS=30 # default: 30 + +# Default recent activity window (hours) +RECENT_ACTIVITY_WINDOW_HOURS=24 # default: 24 +``` + +## Future Enhancements (Out of Scope) + +1. **Player activity graphs** + - Visualize peak hours + - Track daily/weekly active users + +2. **Player statistics** + - Total playtime per player + - Average session duration + - First seen / last seen timestamps + +3. **Activity webhooks** + - Notify when specific players join/leave + - Daily activity summary reports + +4. **Playtime leaderboard** + - Rank players by total time online + - Show most active players per week/month + +## Migration Strategy + +This is a **backward-compatible** addition: +- Existing functionality continues to work unchanged +- New table is created on next backend startup +- No breaking changes to existing commands or APIs +- Session tracking starts from deployment forward (no backfill) + +## Summary + +This design provides a robust foundation for tracking recent player activity: + +✅ **Persistent session tracking** with SQLite storage +✅ **Human-readable durations** in Discord embeds +✅ **Efficient queries** with proper indexing +✅ **Automatic cleanup** to prevent unbounded growth +✅ **Backward compatible** with existing functionality +✅ **Extensible** for future analytics features + +The feature enhances user visibility into server activity while maintaining the simplicity and performance of the current system. From 1b11dd323305b33d77411447a3924727ad95a4d7 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:25:09 +0000 Subject: [PATCH 2/5] Update design: Use in-memory tracking instead of persistent storage - Replace SQLite persistent storage with in-memory HashMap - Focus on configurable time window (default 24h) for recent activity - Track simple last seen timestamps instead of full session records - Remove database migrations and cleanup complexity - Lighter weight: ~40 bytes per player vs database overhead - No need to survive restarts per user feedback Co-authored-by: Adhitya Thirumala --- RECENT_ACTIVITY_DESIGN.md | 257 ++++++++++++++++++-------------------- 1 file changed, 122 insertions(+), 135 deletions(-) diff --git a/RECENT_ACTIVITY_DESIGN.md b/RECENT_ACTIVITY_DESIGN.md index d5d15cd..5b6ab57 100644 --- a/RECENT_ACTIVITY_DESIGN.md +++ b/RECENT_ACTIVITY_DESIGN.md @@ -2,119 +2,113 @@ ## Overview -This feature extends Oxeye to track **recent player activity** and **offline duration**, enabling users to see who's been on their Minecraft servers recently and how long they've been offline. +This feature extends Oxeye to track **recent player activity** and **last seen timestamps**, enabling users to see who's been on their Minecraft servers within a configurable time window (default: last 24 hours) and when they were last seen. ## Problem Statement -Currently, Oxeye only tracks players who are **currently online**. Once a player disconnects, all information about their session is lost. Users want to: +Currently, Oxeye only tracks players who are **currently online**. Once a player disconnects, all information about them is lost. Users want to: -1. See which players have been on recently (even if they're currently offline) -2. Know how long a player has been offline -3. Understand recent server activity patterns +1. See which players have been on recently (within the last day by default) +2. Know when each player was last seen +3. Understand recent server activity patterns without needing persistent history ## Design Decisions ### 1. Data Storage Strategy -**Choice: Persistent SQLite table for player sessions** +**Choice: In-memory tracking with configurable time window** -We'll create a new `player_sessions` table to track historical join/leave events: +We'll track recent player activity using an in-memory data structure within the existing player cache. Each player entry will include a `last_seen` timestamp: -```sql -CREATE TABLE player_sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - api_key_hash TEXT NOT NULL, - player_name TEXT NOT NULL, - joined_at INTEGER NOT NULL, -- Unix timestamp - left_at INTEGER, -- NULL if still online - FOREIGN KEY (api_key_hash) REFERENCES servers(api_key_hash) ON DELETE CASCADE -); +```rust +struct PlayerActivityCache { + // Existing player cache + players: HashMap>, -CREATE INDEX idx_sessions_server_time ON player_sessions(api_key_hash, left_at DESC); -CREATE INDEX idx_sessions_player ON player_sessions(player_name); + // New: Track when each player was last seen + last_seen: HashMap<(ApiKeyHash, PlayerName), i64>, // Unix timestamp +} ``` -**Why persistent storage?** -- Recent activity is valuable historical data that should survive backend restarts -- Users expect to query "who was on yesterday" even after server maintenance -- Session history enables future analytics features +**Why in-memory storage?** +- Simplicity: No database schema changes, migrations, or cleanup tasks +- Performance: Instant lookups with no database queries +- Sufficient for use case: Recent activity within a configurable window (default 1 day) +- No persistence needed: If backend restarts, we'll rebuild tracking from incoming server syncs +- Lightweight: Only stores timestamps, not full session records -### 2. Session Tracking +### 2. Activity Tracking -**When to create/update sessions:** +**When to update last seen timestamps:** 1. **Player joins** (`POST /join`) - - Create a new session record with `joined_at` timestamp - - `left_at` is NULL (still online) + - Update `last_seen` timestamp to current time + - Add player to active players cache 2. **Player leaves** (`POST /leave`) - - Update the session record, setting `left_at` to current timestamp + - Update `last_seen` timestamp to current time + - Remove player from active players cache + - Keep `last_seen` timestamp in memory for configured window 3. **Server sync** (`POST /sync`) - - For players in the sync list who aren't in cache: create new sessions - - For players in cache who aren't in sync list: close their sessions (set `left_at`) + - For all players in sync list: update their `last_seen` timestamps + - For players in cache but not in sync list: update `last_seen` and mark as offline 4. **Server disconnection** - - Close all open sessions for that server (set `left_at` to current time) + - Update `last_seen` for all active players on that server + - Clear from active players cache ### 3. Data Retention -**Retention policy: 30 days** - -Sessions older than 30 days should be automatically pruned to prevent unbounded growth: - -```sql -DELETE FROM player_sessions -WHERE left_at < ?1 - AND left_at IS NOT NULL; -``` +**Retention policy: Configurable time window (default 24 hours)** -This cleanup can run: -- On database initialization -- Periodically (e.g., daily via a background task) -- Before querying recent activity +Last seen entries are automatically filtered when querying: +- Only return players whose `last_seen` timestamp is within the configured window +- No explicit cleanup needed - entries naturally age out when queried +- Optional: Periodic cleanup to remove entries older than 2x the window to prevent memory growth -**Why 30 days?** -- Balances historical insight with database size -- Configurable via environment variable if users want more/less -- Most "recent activity" queries care about the last few days, not months +**Why 24 hours default?** +- Covers typical "who was on today" use case +- Lightweight memory footprint +- Configurable via environment variable for users who want longer history +- Short enough that in-memory storage is practical ### 4. Query Interface -**New database methods:** +**New cache methods:** ```rust -impl Database { - /// Get recent sessions for a server (active or recently ended). - /// Returns sessions that ended within `window_secs` or are still active. - pub async fn get_recent_sessions( +impl PlayerCache { + /// Get recent activity for a server (active or recently seen). + /// Returns players seen within `window_secs` from now. + pub fn get_recent_activity( &self, api_key_hash: &str, window_secs: i64, now: i64 - ) -> Result>; + ) -> Vec; - /// Get all recent sessions across all servers in a guild. - pub async fn get_recent_sessions_for_guild( + /// Get all recent activity across all servers in a guild. + pub fn get_recent_activity_for_guild( &self, guild_id: u64, window_secs: i64, now: i64 - ) -> Result>; + ) -> Vec; - /// Cleanup old sessions (called periodically). - pub async fn cleanup_old_sessions(&self, before: i64) -> Result; + /// Optional: Cleanup entries older than threshold to prevent memory growth. + pub fn cleanup_old_activity(&mut self, before: i64); } -pub struct PlayerSession { +pub struct PlayerActivity { pub player_name: PlayerName, - pub joined_at: i64, - pub left_at: Option, // None if still online + pub last_seen: i64, + pub is_online: bool, } -pub struct ServerRecentSessions { +pub struct ServerActivity { pub server_name: String, - pub sessions: Vec, + pub players: Vec, } ``` @@ -133,19 +127,19 @@ Shows players who have been online recently, including offline duration. ``` ┌──────────────────────────────────────────────────┐ -│ 📊 Recent Activity - survival │ +│ 📊 Recent Activity - survival (last 24h) │ ├──────────────────────────────────────────────────┤ │ 🟢 Currently Online (3) │ -│ • Steve (joined 2h ago) │ -│ • Alex (joined 45m ago) │ -│ • Notch (joined 5m ago) │ +│ • Steve (last seen: online now) │ +│ • Alex (last seen: online now) │ +│ • Notch (last seen: online now) │ │ │ │ 🔴 Recently Offline (5) │ -│ • jeb_ (offline 30m) │ -│ • Dinnerbone (offline 2h) │ -│ • Herobrine (offline 5h) │ -│ • Dream (offline 18h) │ -│ • Technoblade (offline 23h) │ +│ • jeb_ (last seen: 30m ago) │ +│ • Dinnerbone (last seen: 2h ago) │ +│ • Herobrine (last seen: 5h ago) │ +│ • Dream (last seen: 18h ago) │ +│ • Technoblade (last seen: 23h ago) │ │ │ │ Oxeye │ └──────────────────────────────────────────────────┘ @@ -173,39 +167,32 @@ Update the existing status command to show last seen time for offline players: ### 6. API Changes -No new HTTP endpoints needed. The existing endpoints will be enhanced to track sessions: +No new HTTP endpoints needed. The existing endpoints will be enhanced to update last seen timestamps: -**`POST /join`** - Create new session record -**`POST /leave`** - Close session record -**`POST /sync`** - Reconcile session records with current player list -**`POST /disconnect`** - Close all open sessions for the server +**`POST /join`** - Update player's last seen timestamp +**`POST /leave`** - Update player's last seen timestamp +**`POST /sync`** - Update last seen timestamps for all players in sync +**`POST /disconnect`** - Update last seen timestamps for all active players on server ### 7. Performance Considerations -**Indexes:** -- `idx_sessions_server_time` - Enables fast queries for recent sessions by server -- `idx_sessions_player` - Enables fast lookups of a specific player's history +**Memory overhead:** +- HashMap entry: ~(32 bytes key + 8 bytes timestamp) = ~40 bytes per player +- For 100 unique players in 24h: ~4KB total +- Negligible compared to player skin cache **Query efficiency:** -```sql --- Get recent sessions (last 24h) for a server -SELECT player_name, joined_at, left_at -FROM player_sessions -WHERE api_key_hash = ?1 - AND (left_at IS NULL OR left_at > ?2) -ORDER BY - CASE WHEN left_at IS NULL THEN 0 ELSE 1 END, -- Online first - COALESCE(left_at, joined_at) DESC -LIMIT 100; -``` +- O(1) timestamp lookup per player +- O(n) filtering for recent activity (where n = total tracked players) +- For typical server (100 players): < 1ms query time +- No database I/O required **Expected overhead:** -- INSERT on player join: ~1ms -- UPDATE on player leave: ~1ms -- Storage: ~100 bytes per session record -- For 100 players with 10 sessions/day each: ~100KB/day, ~3MB/month +- Timestamp update on join/leave: < 0.1ms (HashMap insert) +- Recent activity query: < 1ms (in-memory filtering) +- Memory: ~40 bytes per unique player tracked -This is negligible compared to the skin/image caching overhead already in the system. +This is significantly lighter than the previous database approach. ### 8. Time Formatting @@ -230,64 +217,62 @@ Examples: ### 9. Edge Cases -1. **Server restart without sync** - - Open sessions remain open until next sync - - Status command shows "⚠️ Server not synced since backend restart" +1. **Backend restart** + - In-memory cache is empty + - Last seen timestamps are lost (acceptable trade-off) + - Tracking resumes as servers sync and players join/leave + - Recent activity will be accurate within configured window after restart -2. **Player rejoins before session timeout** - - Close previous session (set `left_at`) - - Create new session record - - Prevents inflated "online time" from single mega-session +2. **Server disconnection** + - Update last seen for all active players before clearing + - Players show in recent activity list until they age out of window -3. **Backend restart** - - All open sessions in DB show `left_at = NULL` - - On first sync per server, close old sessions - - Cache is rebuilt from server sync +3. **Player rejoins multiple times** + - Last seen timestamp is simply updated each time + - No duplicate entries since we only store one timestamp per player 4. **Time zones** - All timestamps are UTC (Unix timestamps) - Duration calculations are timezone-agnostic - - Discord renders relative times ("2h ago") automatically + - Discord renders relative times ("30m ago") automatically ## Implementation Plan -### Phase 1: Database Layer (Core) -1. Add `player_sessions` table to schema migration -2. Implement session tracking methods: - - `create_session()` - - `close_session()` - - `get_recent_sessions()` - - `cleanup_old_sessions()` -3. Add database tests for session tracking +### Phase 1: Cache Layer (Core) +1. Add `last_seen` HashMap to player cache structure +2. Implement activity tracking methods: + - `update_last_seen()` + - `get_recent_activity()` + - `cleanup_old_activity()` (optional) +3. Add unit tests for activity tracking ### Phase 2: API Integration -1. Modify `POST /join` to create session records -2. Modify `POST /leave` to close session records -3. Modify `POST /sync` to reconcile sessions -4. Add cleanup task on database initialization +1. Modify `POST /join` to update last seen timestamps +2. Modify `POST /leave` to update last seen timestamps +3. Modify `POST /sync` to update last seen timestamps +4. Modify `POST /disconnect` to update last seen timestamps ### Phase 3: Discord Commands 1. Add `/oxeye recent` command -2. Enhance `/oxeye status` to show last seen +2. Enhance `/oxeye status` to show last seen info 3. Add time formatting helper functions ### Phase 4: Testing & Polish -1. Integration tests for session tracking -2. Test edge cases (disconnects, restarts, etc.) +1. Integration tests for activity tracking +2. Test edge cases (restarts, disconnects, etc.) 3. Documentation updates ## Configuration -**New environment variables:** +**New environment variable:** ```bash -# Session retention period (days) -SESSION_RETENTION_DAYS=30 # default: 30 - # Default recent activity window (hours) RECENT_ACTIVITY_WINDOW_HOURS=24 # default: 24 ``` +Users can also override this per-command using the `timeframe` parameter in `/oxeye recent`. + ## Future Enhancements (Out of Scope) 1. **Player activity graphs** @@ -311,19 +296,21 @@ RECENT_ACTIVITY_WINDOW_HOURS=24 # default: 24 This is a **backward-compatible** addition: - Existing functionality continues to work unchanged -- New table is created on next backend startup +- No database schema changes required - No breaking changes to existing commands or APIs -- Session tracking starts from deployment forward (no backfill) +- Activity tracking starts immediately upon deployment +- After backend restart, tracking resumes from first sync/join/leave events ## Summary -This design provides a robust foundation for tracking recent player activity: +This design provides a lightweight foundation for tracking recent player activity: -✅ **Persistent session tracking** with SQLite storage +✅ **In-memory tracking** with configurable time window (default 24h) +✅ **Simple last seen timestamps** for each player ✅ **Human-readable durations** in Discord embeds -✅ **Efficient queries** with proper indexing -✅ **Automatic cleanup** to prevent unbounded growth +✅ **No database changes** required +✅ **Minimal memory overhead** (~40 bytes per player) ✅ **Backward compatible** with existing functionality -✅ **Extensible** for future analytics features +✅ **Instant queries** with no I/O overhead -The feature enhances user visibility into server activity while maintaining the simplicity and performance of the current system. +The feature enhances user visibility into recent server activity with a simple, performant approach that doesn't require persistent storage. From a9407485618109ea4afb775a15f91c3eef7fe044 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:31:30 +0000 Subject: [PATCH 3/5] Simplify design: Remove /oxeye recent command, text-only status additions - Remove separate /oxeye recent command - Consolidate all functionality into /oxeye status - Clarify that offline players only get text additions (no picture changes) - Simplify API interface to focus on recently offline players only Co-authored-by: Adhitya Thirumala --- RECENT_ACTIVITY_DESIGN.md | 100 ++++++++++---------------------------- 1 file changed, 25 insertions(+), 75 deletions(-) diff --git a/RECENT_ACTIVITY_DESIGN.md b/RECENT_ACTIVITY_DESIGN.md index 5b6ab57..ebe4063 100644 --- a/RECENT_ACTIVITY_DESIGN.md +++ b/RECENT_ACTIVITY_DESIGN.md @@ -2,15 +2,11 @@ ## Overview -This feature extends Oxeye to track **recent player activity** and **last seen timestamps**, enabling users to see who's been on their Minecraft servers within a configurable time window (default: last 24 hours) and when they were last seen. +This feature extends Oxeye to track **last seen timestamps** for recently offline players, enabling users to see when players were last on their Minecraft servers within a configurable time window (default: last 24 hours). This information is displayed as text additions to the existing `/oxeye status` command. ## Problem Statement -Currently, Oxeye only tracks players who are **currently online**. Once a player disconnects, all information about them is lost. Users want to: - -1. See which players have been on recently (within the last day by default) -2. Know when each player was last seen -3. Understand recent server activity patterns without needing persistent history +Currently, Oxeye only tracks players who are **currently online**. Once a player disconnects, all information about them is lost. Users want to see when players were last online within the existing `/oxeye status` command, without needing a separate command or persistent history. ## Design Decisions @@ -79,92 +75,48 @@ Last seen entries are automatically filtered when querying: ```rust impl PlayerCache { - /// Get recent activity for a server (active or recently seen). - /// Returns players seen within `window_secs` from now. - pub fn get_recent_activity( + /// Get recently offline players for a server. + /// Returns offline players seen within `window_secs` from now. + pub fn get_recently_offline( &self, api_key_hash: &str, window_secs: i64, now: i64 - ) -> Vec; - - /// Get all recent activity across all servers in a guild. - pub fn get_recent_activity_for_guild( - &self, - guild_id: u64, - window_secs: i64, - now: i64 - ) -> Vec; + ) -> Vec; /// Optional: Cleanup entries older than threshold to prevent memory growth. pub fn cleanup_old_activity(&mut self, before: i64); } -pub struct PlayerActivity { +pub struct OfflinePlayerInfo { pub player_name: PlayerName, - pub last_seen: i64, - pub is_online: bool, -} - -pub struct ServerActivity { - pub server_name: String, - pub players: Vec, + pub last_seen: i64, // Unix timestamp } ``` ### 5. Discord Commands -**New command: `/oxeye recent [server] [timeframe]`** - -Shows players who have been online recently, including offline duration. +**Enhanced `/oxeye status` command:** -**Parameters:** -- `server` (optional): Server name (defaults to all servers) -- `timeframe` (optional): How far back to look (default: 24h) - - Options: `1h`, `6h`, `12h`, `24h`, `7d`, `30d` +Update the existing status command to show last seen time for recently offline players. The existing embedded image/picture showing online players remains unchanged - offline players are only shown as text additions below. **Example output:** ``` -┌──────────────────────────────────────────────────┐ -│ 📊 Recent Activity - survival (last 24h) │ -├──────────────────────────────────────────────────┤ -│ 🟢 Currently Online (3) │ -│ • Steve (last seen: online now) │ -│ • Alex (last seen: online now) │ -│ • Notch (last seen: online now) │ -│ │ -│ 🔴 Recently Offline (5) │ -│ • jeb_ (last seen: 30m ago) │ -│ • Dinnerbone (last seen: 2h ago) │ -│ • Herobrine (last seen: 5h ago) │ -│ • Dream (last seen: 18h ago) │ -│ • Technoblade (last seen: 23h ago) │ -│ │ -│ Oxeye │ -└──────────────────────────────────────────────────┘ -``` - -**Enhanced `/oxeye status` command:** +[Existing server status embed with player head images - unchanged] -Update the existing status command to show last seen time for offline players: - -``` -┌──────────────────────────────────────────────────┐ -│ 🟢 survival — 3 online │ -├──────────────────────────────────────────────────┤ -│ Steve (joined 2h ago) │ -│ Alex (joined 45m ago) │ -│ Notch (joined 5m ago) │ -│ │ -│ Last seen: │ -│ • jeb_ (30m ago) │ -│ • Dinnerbone (2h ago) │ -│ │ -│ Oxeye │ -└──────────────────────────────────────────────────┘ +Last seen (within 24h): +• jeb_ (30m ago) +• Dinnerbone (2h ago) +• Herobrine (5h ago) ``` +**Key points:** +- **No new commands** - all functionality integrated into existing `/oxeye status` +- **No picture/image changes** - player head images only shown for currently online players (existing behavior) +- **Text-only additions** - offline players shown as simple text list below the online players +- **Configurable window** - default 24h, configurable via environment variable + ### 6. API Changes No new HTTP endpoints needed. The existing endpoints will be enhanced to update last seen timestamps: @@ -252,10 +204,10 @@ Examples: 3. Modify `POST /sync` to update last seen timestamps 4. Modify `POST /disconnect` to update last seen timestamps -### Phase 3: Discord Commands -1. Add `/oxeye recent` command -2. Enhance `/oxeye status` to show last seen info -3. Add time formatting helper functions +### Phase 3: Discord Command Enhancement +1. Enhance `/oxeye status` to show recently offline players as text +2. Add time formatting helper functions +3. Ensure no picture/image changes for offline players (text only) ### Phase 4: Testing & Polish 1. Integration tests for activity tracking @@ -271,8 +223,6 @@ Examples: RECENT_ACTIVITY_WINDOW_HOURS=24 # default: 24 ``` -Users can also override this per-command using the `timeframe` parameter in `/oxeye recent`. - ## Future Enhancements (Out of Scope) 1. **Player activity graphs** From 8f99d7920df3340a5b5dd8d0c16edc6e14c99769 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:36:26 +0000 Subject: [PATCH 4/5] Use existing SCC cache instead of separate HashMap, add lazy cleanup Changes: - Extend ServerState with last_seen field instead of creating new cache - Leverage existing scc::HashMap for thread-safe concurrent access - Implement O(1) lazy cleanup strategy instead of O(n) periodic scans - Remove stale entries during reads/writes, not background tasks - Update implementation plan to reflect cache architecture Co-authored-by: Adhitya Thirumala --- RECENT_ACTIVITY_DESIGN.md | 206 ++++++++++++++++++++++++++------------ 1 file changed, 141 insertions(+), 65 deletions(-) diff --git a/RECENT_ACTIVITY_DESIGN.md b/RECENT_ACTIVITY_DESIGN.md index ebe4063..8125c2b 100644 --- a/RECENT_ACTIVITY_DESIGN.md +++ b/RECENT_ACTIVITY_DESIGN.md @@ -12,26 +12,37 @@ Currently, Oxeye only tracks players who are **currently online**. Once a player ### 1. Data Storage Strategy -**Choice: In-memory tracking with configurable time window** +**Choice: Extend existing SCC cache with last_seen tracking** -We'll track recent player activity using an in-memory data structure within the existing player cache. Each player entry will include a `last_seen` timestamp: +We'll track recent player activity by extending the existing `ServerState` struct within the current `scc::HashMap`-based online cache. No new data structures needed: ```rust -struct PlayerActivityCache { - // Existing player cache - players: HashMap>, +pub struct ServerState { + /// Online players with their join timestamps. + pub players: Vec<(PlayerName, i64)>, - // New: Track when each player was last seen - last_seen: HashMap<(ApiKeyHash, PlayerName), i64>, // Unix timestamp + /// NEW: Recently offline players with their last_seen timestamps. + /// Entries are lazily removed when older than the configured window. + pub last_seen: HashMap, // Unix timestamp + + /// Whether this server has synced since backend restart. + pub synced_since_boot: bool, } ``` -**Why in-memory storage?** -- Simplicity: No database schema changes, migrations, or cleanup tasks -- Performance: Instant lookups with no database queries -- Sufficient for use case: Recent activity within a configurable window (default 1 day) -- No persistence needed: If backend restarts, we'll rebuild tracking from incoming server syncs -- Lightweight: Only stores timestamps, not full session records +The existing cache structure remains: +```rust +pub type OnlineCache = scc::HashMap; // api_key_hash -> ServerState +``` + +**Why use the existing SCC cache?** +- Already thread-safe: `scc::HashMap` provides lock-free concurrent access +- Per-server isolation: `last_seen` is scoped to each server's `ServerState` +- No additional data structures: Extends existing architecture naturally +- Performance: Instant lookups with no database queries or additional locks +- Simplicity: No separate cache management logic needed +- Lightweight: Only stores timestamps (~8 bytes per player) +- No persistence needed: Data resets on restart (acceptable for "recent activity") ### 2. Activity Tracking @@ -54,38 +65,72 @@ struct PlayerActivityCache { - Update `last_seen` for all active players on that server - Clear from active players cache -### 3. Data Retention +### 3. Data Retention & TTL Cleanup **Retention policy: Configurable time window (default 24 hours)** -Last seen entries are automatically filtered when querying: -- Only return players whose `last_seen` timestamp is within the configured window -- No explicit cleanup needed - entries naturally age out when queried -- Optional: Periodic cleanup to remove entries older than 2x the window to prevent memory growth +**Lazy cleanup strategy (O(1) amortized, not O(n)):** + +Instead of scanning all entries periodically, we remove stale entries **as we access them**: + +1. **During queries** (`get_recently_offline()`): + - Remove `last_seen` entries older than threshold **while iterating** + - Only touches entries we're reading anyway + - O(k) where k = entries checked, not O(n) for all players + +2. **On player events** (join/leave/sync): + - When updating a player's `last_seen`, remove it if it's too old + - Natural cleanup during normal operation + +This approach: +- ✅ No O(n) scans of all cached players +- ✅ No background cleanup tasks needed +- ✅ Memory naturally bounded by activity rate +- ✅ Each operation removes stale data for players it touches **Why 24 hours default?** - Covers typical "who was on today" use case -- Lightweight memory footprint +- Lightweight memory footprint (~8 bytes per player) - Configurable via environment variable for users who want longer history -- Short enough that in-memory storage is practical +- Lazy cleanup keeps memory usage minimal ### 4. Query Interface -**New cache methods:** +**Extended ServerState methods:** ```rust -impl PlayerCache { - /// Get recently offline players for a server. - /// Returns offline players seen within `window_secs` from now. - pub fn get_recently_offline( - &self, - api_key_hash: &str, - window_secs: i64, - now: i64 - ) -> Vec; - - /// Optional: Cleanup entries older than threshold to prevent memory growth. - pub fn cleanup_old_activity(&mut self, before: i64); +impl ServerState { + /// Update last_seen timestamp for a player. + /// Also removes the entry if it's older than the cutoff (lazy cleanup). + pub fn update_last_seen(&mut self, player_name: PlayerName, now: i64, cutoff: i64) { + // Remove if too old (lazy cleanup) + if now < cutoff { + self.last_seen.remove(&player_name); + return; + } + self.last_seen.insert(player_name, now); + } + + /// Get recently offline players (not currently online). + /// Removes stale entries older than cutoff during iteration (lazy cleanup). + pub fn get_recently_offline(&mut self, cutoff: i64) -> Vec { + let online_players: HashSet<_> = self.players.iter().map(|(n, _)| n).collect(); + + // Collect recent offline players and remove stale entries + let mut result = Vec::new(); + self.last_seen.retain(|name, &mut last_seen| { + if last_seen < cutoff { + false // Remove stale entry (lazy cleanup) + } else if !online_players.contains(name) { + result.push(OfflinePlayerInfo { player_name: *name, last_seen }); + true // Keep entry + } else { + true // Keep entry (player is online) + } + }); + + result + } } pub struct OfflinePlayerInfo { @@ -94,6 +139,11 @@ pub struct OfflinePlayerInfo { } ``` +**Key optimization:** +- `update_last_seen()` performs lazy cleanup on single entries +- `get_recently_offline()` uses `HashMap::retain()` to remove stale entries during iteration +- No separate O(n) cleanup needed + ### 5. Discord Commands **Enhanced `/oxeye status` command:** @@ -129,22 +179,29 @@ No new HTTP endpoints needed. The existing endpoints will be enhanced to update ### 7. Performance Considerations **Memory overhead:** -- HashMap entry: ~(32 bytes key + 8 bytes timestamp) = ~40 bytes per player -- For 100 unique players in 24h: ~4KB total +- `last_seen` HashMap entry: ~(16 bytes PlayerName + 8 bytes timestamp) = ~24 bytes per player +- For 100 unique players in 24h: ~2.4KB per server +- Stored within existing `ServerState` in SCC cache +- Lazy cleanup prevents unbounded growth - Negligible compared to player skin cache **Query efficiency:** -- O(1) timestamp lookup per player -- O(n) filtering for recent activity (where n = total tracked players) -- For typical server (100 players): < 1ms query time +- Timestamp update: O(1) HashMap insert +- Recent activity query: O(k) where k = `last_seen` entries (bounded by lazy cleanup) - No database I/O required +- No global locks (SCC HashMap is lock-free) +- For typical server (100 players): < 1ms query time **Expected overhead:** - Timestamp update on join/leave: < 0.1ms (HashMap insert) -- Recent activity query: < 1ms (in-memory filtering) -- Memory: ~40 bytes per unique player tracked +- Recent activity query with lazy cleanup: < 1ms (in-memory filtering + retain) +- Memory: ~24 bytes per unique player tracked -This is significantly lighter than the previous database approach. +**Lazy cleanup efficiency:** +- No O(n) scans across all servers +- Stale entries removed during normal reads/writes +- Memory bounded by: `(activity_rate) × (window_duration)` +- Example: 100 players/day × 24h window = ~2.4KB per server ### 8. Time Formatting @@ -191,28 +248,44 @@ Examples: ## Implementation Plan ### Phase 1: Cache Layer (Core) -1. Add `last_seen` HashMap to player cache structure -2. Implement activity tracking methods: - - `update_last_seen()` - - `get_recent_activity()` - - `cleanup_old_activity()` (optional) -3. Add unit tests for activity tracking - -### Phase 2: API Integration -1. Modify `POST /join` to update last seen timestamps -2. Modify `POST /leave` to update last seen timestamps -3. Modify `POST /sync` to update last seen timestamps -4. Modify `POST /disconnect` to update last seen timestamps - -### Phase 3: Discord Command Enhancement -1. Enhance `/oxeye status` to show recently offline players as text -2. Add time formatting helper functions -3. Ensure no picture/image changes for offline players (text only) - -### Phase 4: Testing & Polish +1. **Extend `ServerState` struct** (oxeye-db/src/cache.rs): + - Add `last_seen: HashMap` field + - Implement `update_last_seen()` with lazy cleanup + - Implement `get_recently_offline()` with lazy cleanup via `retain()` +2. **Update `ServerState::new()`** to initialize empty `last_seen` HashMap +3. **Add unit tests** for: + - Last seen tracking + - Lazy cleanup during queries + - Stale entry removal + +### Phase 2: Database Layer Integration +1. **Add helper method** to `Database` (oxeye-db/src/lib.rs): + - `get_recently_offline(api_key_hash, window_secs) -> Vec` + - Calls `ServerState::get_recently_offline()` on the cached state +2. **Modify existing methods** to call `update_last_seen()`: + - `player_join()` - update last_seen before adding to online list + - `player_leave()` - update last_seen after removing from online list + - `sync_players()` - update last_seen for all players in sync + - `delete_server_by_api_key()` - last_seen is cleared when server cache entry is removed + +### Phase 3: API Integration +1. **Configuration**: Add `RECENT_ACTIVITY_WINDOW_HOURS` env var to backend +2. **Modify routes** (oxeye-backend/src/routes.rs): + - `POST /join`, `/leave`, `/sync` already call DB methods, which now update last_seen + - No route changes needed (logic is in DB layer) + +### Phase 4: Discord Command Enhancement +1. **Enhance `/oxeye status`** (oxeye-bot): + - Call `db.get_recently_offline()` after getting online players + - Format as text list: "Last seen (within 24h): • player (Xm ago)" +2. **Add time formatting helper** for human-readable durations +3. **Ensure no picture changes** for offline players (text only) + +### Phase 5: Testing & Polish 1. Integration tests for activity tracking -2. Test edge cases (restarts, disconnects, etc.) -3. Documentation updates +2. Test lazy cleanup efficiency +3. Test edge cases (restarts, disconnects, etc.) +4. Documentation updates ## Configuration @@ -255,12 +328,15 @@ This is a **backward-compatible** addition: This design provides a lightweight foundation for tracking recent player activity: -✅ **In-memory tracking** with configurable time window (default 24h) +✅ **Uses existing SCC cache** - extends `ServerState` struct, no new data structures +✅ **Lock-free concurrency** - leverages existing `scc::HashMap` thread safety +✅ **Lazy cleanup (not O(n))** - removes stale entries during reads/writes, not periodic scans +✅ **Per-server isolation** - `last_seen` scoped to each server's state +✅ **Minimal memory overhead** (~24 bytes per player, bounded by lazy cleanup) ✅ **Simple last seen timestamps** for each player ✅ **Human-readable durations** in Discord embeds ✅ **No database changes** required -✅ **Minimal memory overhead** (~40 bytes per player) ✅ **Backward compatible** with existing functionality ✅ **Instant queries** with no I/O overhead -The feature enhances user visibility into recent server activity with a simple, performant approach that doesn't require persistent storage. +The feature enhances user visibility into recent server activity by naturally extending the existing cache architecture with efficient lazy cleanup. From f70b624cc5803b3981258ebcef3b7ea1ebf7bd79 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:51:49 +0000 Subject: [PATCH 5/5] Replace HashMap with proper PlayerActivity struct in design - Define PlayerActivity struct with player_name, last_seen, joined_at, is_online - Replace ServerState.last_seen HashMap with player_activity Vec - Update query methods to work with single source of truth - Cleaner data model: all player state in one struct - Simpler operations: Vec filtering instead of HashMap lookups - Better cache locality with contiguous Vec storage - Updated implementation plan and performance analysis Co-authored-by: Adhitya Thirumala --- RECENT_ACTIVITY_DESIGN.md | 246 +++++++++++++++++++++++--------------- 1 file changed, 148 insertions(+), 98 deletions(-) diff --git a/RECENT_ACTIVITY_DESIGN.md b/RECENT_ACTIVITY_DESIGN.md index 8125c2b..34fad07 100644 --- a/RECENT_ACTIVITY_DESIGN.md +++ b/RECENT_ACTIVITY_DESIGN.md @@ -17,13 +17,20 @@ Currently, Oxeye only tracks players who are **currently online**. Once a player We'll track recent player activity by extending the existing `ServerState` struct within the current `scc::HashMap`-based online cache. No new data structures needed: ```rust -pub struct ServerState { - /// Online players with their join timestamps. - pub players: Vec<(PlayerName, i64)>, +/// Tracks player activity state including online status and timestamps. +#[derive(Clone, Debug)] +pub struct PlayerActivity { + pub player_name: PlayerName, + pub last_seen: i64, // Unix timestamp of last activity + pub joined_at: i64, // Unix timestamp when player joined (0 if offline) + pub is_online: bool, // Current online status +} - /// NEW: Recently offline players with their last_seen timestamps. +pub struct ServerState { + /// All tracked players with their activity state. + /// Includes both online and recently offline players. /// Entries are lazily removed when older than the configured window. - pub last_seen: HashMap, // Unix timestamp + pub player_activity: Vec, /// Whether this server has synced since backend restart. pub synced_since_boot: bool, @@ -35,62 +42,65 @@ The existing cache structure remains: pub type OnlineCache = scc::HashMap; // api_key_hash -> ServerState ``` -**Why use the existing SCC cache?** +**Why use the existing SCC cache with a proper struct?** - Already thread-safe: `scc::HashMap` provides lock-free concurrent access -- Per-server isolation: `last_seen` is scoped to each server's `ServerState` -- No additional data structures: Extends existing architecture naturally -- Performance: Instant lookups with no database queries or additional locks -- Simplicity: No separate cache management logic needed -- Lightweight: Only stores timestamps (~8 bytes per player) +- Per-server isolation: `player_activity` is scoped to each server's `ServerState` +- Clean data model: Single `PlayerActivity` struct instead of separate collections +- Easy filtering: Vec allows simple iteration for online/offline queries +- No redundant data: One source of truth for each player's state +- Performance: Linear scan through Vec is fast for typical server sizes (<100 players) +- Simplicity: No HashMap lookups or synchronization between separate structures +- Lightweight: ~33 bytes per player (PlayerName + 3×i64 + bool) - No persistence needed: Data resets on restart (acceptable for "recent activity") ### 2. Activity Tracking -**When to update last seen timestamps:** +**When to update player activity:** 1. **Player joins** (`POST /join`) - - Update `last_seen` timestamp to current time - - Add player to active players cache + - Find existing `PlayerActivity` or create new one + - Set `is_online = true`, `joined_at = now`, `last_seen = now` 2. **Player leaves** (`POST /leave`) - - Update `last_seen` timestamp to current time - - Remove player from active players cache - - Keep `last_seen` timestamp in memory for configured window + - Find player's `PlayerActivity` entry + - Set `is_online = false`, `joined_at = 0`, `last_seen = now` + - Entry stays in Vec for configured window (lazy cleanup removes later) 3. **Server sync** (`POST /sync`) - - For all players in sync list: update their `last_seen` timestamps - - For players in cache but not in sync list: update `last_seen` and mark as offline + - For all players in sync list: update their activity (mark online if not already) + - For tracked players not in sync: mark as offline (`is_online = false`, `last_seen = now`) 4. **Server disconnection** - - Update `last_seen` for all active players on that server - - Clear from active players cache + - Mark all tracked players as offline + - Set `last_seen = now` for all + - Entries stay in Vec until lazy cleanup removes them ### 3. Data Retention & TTL Cleanup **Retention policy: Configurable time window (default 24 hours)** -**Lazy cleanup strategy (O(1) amortized, not O(n)):** +**Lazy cleanup strategy:** Instead of scanning all entries periodically, we remove stale entries **as we access them**: -1. **During queries** (`get_recently_offline()`): - - Remove `last_seen` entries older than threshold **while iterating** - - Only touches entries we're reading anyway - - O(k) where k = entries checked, not O(n) for all players +1. **During queries** (`get_online_players()`, `get_recently_offline()`): + - Filter out `PlayerActivity` entries where `last_seen < threshold` + - Use `Vec::retain()` to remove stale entries during iteration + - O(n) where n = entries in this server's Vec (typically <100 players) -2. **On player events** (join/leave/sync): - - When updating a player's `last_seen`, remove it if it's too old - - Natural cleanup during normal operation +2. **On player updates** (join/leave/sync): + - When updating a player, check if `last_seen` is too old + - Remove stale entry before adding updated one This approach: -- ✅ No O(n) scans of all cached players - ✅ No background cleanup tasks needed -- ✅ Memory naturally bounded by activity rate -- ✅ Each operation removes stale data for players it touches +- ✅ Memory naturally bounded by activity rate × window duration +- ✅ Cleanup happens during normal operations +- ✅ Linear scan is fast for typical server sizes (<100 players) **Why 24 hours default?** - Covers typical "who was on today" use case -- Lightweight memory footprint (~8 bytes per player) +- Lightweight memory footprint (~33 bytes per player) - Configurable via environment variable for users who want longer history - Lazy cleanup keeps memory usage minimal @@ -100,49 +110,70 @@ This approach: ```rust impl ServerState { - /// Update last_seen timestamp for a player. - /// Also removes the entry if it's older than the cutoff (lazy cleanup). - pub fn update_last_seen(&mut self, player_name: PlayerName, now: i64, cutoff: i64) { - // Remove if too old (lazy cleanup) - if now < cutoff { - self.last_seen.remove(&player_name); - return; + /// Update or create player activity entry. + /// Performs lazy cleanup by removing stale entries during update. + pub fn update_player_activity( + &mut self, + player_name: PlayerName, + is_online: bool, + now: i64, + cutoff: i64, + ) { + // Lazy cleanup: remove stale entries while searching + self.player_activity.retain(|p| p.last_seen >= cutoff); + + // Find existing player or add new entry + if let Some(activity) = self.player_activity.iter_mut().find(|p| p.player_name == player_name) { + activity.last_seen = now; + activity.is_online = is_online; + activity.joined_at = if is_online { now } else { 0 }; + } else { + self.player_activity.push(PlayerActivity { + player_name, + last_seen: now, + joined_at: if is_online { now } else { 0 }, + is_online, + }); } - self.last_seen.insert(player_name, now); + } + + /// Get currently online players. + /// Performs lazy cleanup by removing stale entries. + pub fn get_online_players(&mut self, cutoff: i64) -> Vec { + // Lazy cleanup while collecting + self.player_activity.retain(|p| p.last_seen >= cutoff); + self.player_activity.iter() + .filter(|p| p.is_online) + .cloned() + .collect() } /// Get recently offline players (not currently online). - /// Removes stale entries older than cutoff during iteration (lazy cleanup). - pub fn get_recently_offline(&mut self, cutoff: i64) -> Vec { - let online_players: HashSet<_> = self.players.iter().map(|(n, _)| n).collect(); - - // Collect recent offline players and remove stale entries - let mut result = Vec::new(); - self.last_seen.retain(|name, &mut last_seen| { - if last_seen < cutoff { - false // Remove stale entry (lazy cleanup) - } else if !online_players.contains(name) { - result.push(OfflinePlayerInfo { player_name: *name, last_seen }); - true // Keep entry - } else { - true // Keep entry (player is online) - } - }); - - result + /// Performs lazy cleanup by removing stale entries. + pub fn get_recently_offline(&mut self, cutoff: i64) -> Vec { + // Lazy cleanup while collecting + self.player_activity.retain(|p| p.last_seen >= cutoff); + self.player_activity.iter() + .filter(|p| !p.is_online) + .cloned() + .collect() } -} -pub struct OfflinePlayerInfo { - pub player_name: PlayerName, - pub last_seen: i64, // Unix timestamp + /// Get all recent activity (online + offline). + pub fn get_all_activity(&mut self, cutoff: i64) -> Vec { + // Lazy cleanup + self.player_activity.retain(|p| p.last_seen >= cutoff); + self.player_activity.clone() + } } ``` -**Key optimization:** -- `update_last_seen()` performs lazy cleanup on single entries -- `get_recently_offline()` uses `HashMap::retain()` to remove stale entries during iteration -- No separate O(n) cleanup needed +**Key benefits:** +- Single source of truth: `PlayerActivity` struct contains all player state +- Simple Vec operations: No HashMap lookups or separate data structures +- Lazy cleanup: `Vec::retain()` removes stale entries during queries +- Easy filtering: `is_online` flag makes online/offline queries straightforward +- Clone-friendly: Small struct (~33 bytes) is cheap to copy ### 5. Discord Commands @@ -179,29 +210,36 @@ No new HTTP endpoints needed. The existing endpoints will be enhanced to update ### 7. Performance Considerations **Memory overhead:** -- `last_seen` HashMap entry: ~(16 bytes PlayerName + 8 bytes timestamp) = ~24 bytes per player -- For 100 unique players in 24h: ~2.4KB per server +- `PlayerActivity` struct: ~33 bytes (PlayerName:16 + last_seen:8 + joined_at:8 + is_online:1) +- For 100 unique players in 24h: ~3.3KB per server - Stored within existing `ServerState` in SCC cache - Lazy cleanup prevents unbounded growth - Negligible compared to player skin cache **Query efficiency:** -- Timestamp update: O(1) HashMap insert -- Recent activity query: O(k) where k = `last_seen` entries (bounded by lazy cleanup) +- Update player activity: O(n) linear search through Vec (n = players on this server) +- Recent activity query: O(n) where n = tracked players (typically <100) - No database I/O required - No global locks (SCC HashMap is lock-free) - For typical server (100 players): < 1ms query time **Expected overhead:** -- Timestamp update on join/leave: < 0.1ms (HashMap insert) -- Recent activity query with lazy cleanup: < 1ms (in-memory filtering + retain) -- Memory: ~24 bytes per unique player tracked +- Player update on join/leave: < 0.5ms (linear search + lazy cleanup) +- Recent activity query with lazy cleanup: < 1ms (Vec filter + retain) +- Memory: ~33 bytes per unique player tracked + +**Why Vec over HashMap:** +- Simpler: No key management, single data structure +- Faster for small n: Linear scan of <100 entries is ~microseconds +- Better cache locality: Contiguous memory layout +- Less memory: No hash table overhead +- Lazy cleanup efficiency: `Vec::retain()` is highly optimized **Lazy cleanup efficiency:** - No O(n) scans across all servers -- Stale entries removed during normal reads/writes +- Stale entries removed during normal reads/writes per-server - Memory bounded by: `(activity_rate) × (window_duration)` -- Example: 100 players/day × 24h window = ~2.4KB per server +- Example: 100 players/day × 24h window = ~3.3KB per server ### 8. Time Formatting @@ -248,25 +286,33 @@ Examples: ## Implementation Plan ### Phase 1: Cache Layer (Core) -1. **Extend `ServerState` struct** (oxeye-db/src/cache.rs): - - Add `last_seen: HashMap` field - - Implement `update_last_seen()` with lazy cleanup - - Implement `get_recently_offline()` with lazy cleanup via `retain()` -2. **Update `ServerState::new()`** to initialize empty `last_seen` HashMap -3. **Add unit tests** for: - - Last seen tracking +1. **Define `PlayerActivity` struct** (oxeye-db/src/cache.rs): + - Fields: `player_name`, `last_seen`, `joined_at`, `is_online` + - Derive `Clone`, `Debug` traits +2. **Update `ServerState` struct** (oxeye-db/src/cache.rs): + - Replace separate player tracking with `player_activity: Vec` + - Implement `update_player_activity()` with lazy cleanup + - Implement `get_online_players()`, `get_recently_offline()`, `get_all_activity()` +3. **Update `ServerState::new()`** to initialize empty `player_activity` Vec +4. **Add unit tests** for: + - Player activity tracking (join/leave transitions) + - Online/offline filtering - Lazy cleanup during queries - Stale entry removal ### Phase 2: Database Layer Integration -1. **Add helper method** to `Database` (oxeye-db/src/lib.rs): - - `get_recently_offline(api_key_hash, window_secs) -> Vec` - - Calls `ServerState::get_recently_offline()` on the cached state -2. **Modify existing methods** to call `update_last_seen()`: - - `player_join()` - update last_seen before adding to online list - - `player_leave()` - update last_seen after removing from online list - - `sync_players()` - update last_seen for all players in sync - - `delete_server_by_api_key()` - last_seen is cleared when server cache entry is removed +1. **Add helper methods** to `Database` (oxeye-db/src/lib.rs): + - `get_online_players(api_key_hash, window_secs) -> Vec` + - `get_recently_offline(api_key_hash, window_secs) -> Vec` + - Both call corresponding `ServerState` methods +2. **Refactor existing player tracking methods** to use `PlayerActivity`: + - `player_join()` - call `update_player_activity(name, true, now, cutoff)` + - `player_leave()` - call `update_player_activity(name, false, now, cutoff)` + - `sync_players()` - update all synced players as online, mark missing as offline + - `get_server_state()` - return online players from `player_activity` Vec +3. **Migration strategy**: + - Existing `players: Vec<(PlayerName, i64)>` is replaced by filtering `player_activity` + - Backward compatible: Online player queries filter `is_online == true` ### Phase 3: API Integration 1. **Configuration**: Add `RECENT_ACTIVITY_WINDOW_HOURS` env var to backend @@ -276,10 +322,12 @@ Examples: ### Phase 4: Discord Command Enhancement 1. **Enhance `/oxeye status`** (oxeye-bot): - - Call `db.get_recently_offline()` after getting online players - - Format as text list: "Last seen (within 24h): • player (Xm ago)" + - Call `db.get_online_players()` for online list (replaces existing call) + - Call `db.get_recently_offline()` for offline list + - Format offline players as text list: "Last seen (within 24h): • player (Xm ago)" 2. **Add time formatting helper** for human-readable durations 3. **Ensure no picture changes** for offline players (text only) +4. **Update existing status display** to work with `PlayerActivity` struct ### Phase 5: Testing & Polish 1. Integration tests for activity tracking @@ -329,14 +377,16 @@ This is a **backward-compatible** addition: This design provides a lightweight foundation for tracking recent player activity: ✅ **Uses existing SCC cache** - extends `ServerState` struct, no new data structures +✅ **Clean data model** - single `PlayerActivity` struct with all player state ✅ **Lock-free concurrency** - leverages existing `scc::HashMap` thread safety -✅ **Lazy cleanup (not O(n))** - removes stale entries during reads/writes, not periodic scans -✅ **Per-server isolation** - `last_seen` scoped to each server's state -✅ **Minimal memory overhead** (~24 bytes per player, bounded by lazy cleanup) -✅ **Simple last seen timestamps** for each player +✅ **Lazy cleanup** - removes stale entries during reads/writes, not periodic scans +✅ **Per-server isolation** - `player_activity` scoped to each server's state +✅ **Minimal memory overhead** (~33 bytes per player, bounded by lazy cleanup) +✅ **Simple Vec operations** - no HashMap overhead, better cache locality +✅ **Complete player state** - timestamps, online status, join time in one struct ✅ **Human-readable durations** in Discord embeds ✅ **No database changes** required ✅ **Backward compatible** with existing functionality ✅ **Instant queries** with no I/O overhead -The feature enhances user visibility into recent server activity by naturally extending the existing cache architecture with efficient lazy cleanup. +The feature enhances user visibility into recent server activity by naturally extending the existing cache architecture with a clean struct-based design and efficient lazy cleanup.