M1 step 3: Log writing and Storage Clean-up

- lib/storage_model (host-tested): FAT-safe names, daily Log paths,
  dates from Log/Capture file names, CleanupPlan by category and age,
  byte formatting
- StorageService does all card I/O on its task: queued Log lines are
  written in batches each second (dropped while Logs are paused),
  plus file listing and deletion jobs
- Settings > Storage moves into StoragePage: usage, Clean up
  (category, age with size preview, confirmation), Erase SD card
- Clock: local date for Log names
- Serial: log <text>, sd list

Verified on the device: lines land in /irc/dev/#test/2026-10-02.log
with folders created as needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-02 14:56:06 +02:00
co-authored by Claude Opus 5.5
parent 1a0baf4ac7
commit d3948ccc3a
18 changed files with 785 additions and 74 deletions
@@ -0,0 +1,54 @@
#include <unity.h>
#include "storage_paths.h"
using namespace roro;
void setUp() {}
void tearDown() {}
void test_sanitize_replaces_characters_fat_forbids() {
TEST_ASSERT_EQUAL_STRING("a_b_c_d_e_f_g_h_i", storage::sanitize("a\\b/c:d*e?f\"g<h>i").c_str());
TEST_ASSERT_EQUAL_STRING("#roro", storage::sanitize("#roro").c_str());
TEST_ASSERT_EQUAL_STRING("Cl\xC3\xA9ment", storage::sanitize("Cl\xC3\xA9ment").c_str());
}
void test_sanitize_never_returns_empty_or_dot_names() {
TEST_ASSERT_EQUAL_STRING("_", storage::sanitize("").c_str());
TEST_ASSERT_EQUAL_STRING("_", storage::sanitize(".").c_str());
TEST_ASSERT_EQUAL_STRING("_.", storage::sanitize("..").c_str());
}
void test_daily_log_path() {
TEST_ASSERT_EQUAL_STRING("/irc/libera/#roro/2026-10-02.log",
storage::dailyLogPath({"irc", "libera", "#roro"}, "2026-10-02").c_str());
}
void test_daily_log_path_sanitizes_each_part() {
TEST_ASSERT_EQUAL_STRING("/irc/irc.libera.chat_6697/nick_name/undated.log",
storage::dailyLogPath({"irc", "irc.libera.chat:6697", "nick/name"}, "").c_str());
}
void test_parse_date_from_log_and_capture_names() {
TEST_ASSERT_EQUAL(20728, storage::fileDay("2026-10-02.log")); // days since 1970-01-01
TEST_ASSERT_EQUAL(20728, storage::fileDay("20261002-144500.pcap"));
TEST_ASSERT_EQUAL(-1, storage::fileDay("undated.log"));
TEST_ASSERT_EQUAL(-1, storage::fileDay("notes.txt"));
TEST_ASSERT_EQUAL(-1, storage::fileDay("2026-13-02.log"));
}
void test_day_from_utc_seconds() {
TEST_ASSERT_EQUAL(20728, storage::dayFromUtc(1790942400)); // 2026-10-02 12:00Z
TEST_ASSERT_EQUAL(0, storage::dayFromUtc(0));
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_sanitize_replaces_characters_fat_forbids);
RUN_TEST(test_sanitize_never_returns_empty_or_dot_names);
RUN_TEST(test_daily_log_path);
RUN_TEST(test_daily_log_path_sanitizes_each_part);
RUN_TEST(test_parse_date_from_log_and_capture_names);
RUN_TEST(test_day_from_utc_seconds);
return UNITY_END();
}