Skip to content

refactor(app): standardize log file path resolution#84

Merged
K1ngst0m merged 1 commit intomainfrom
dev/refine-logging
Feb 6, 2026
Merged

refactor(app): standardize log file path resolution#84
K1ngst0m merged 1 commit intomainfrom
dev/refine-logging

Conversation

@K1ngst0m
Copy link
Copy Markdown
Collaborator

@K1ngst0m K1ngst0m commented Feb 6, 2026

User description

  • Introduce LoadedConfig startup flow that preserves config source path
  • Centralize logging.file path resolution policy in util config helpers
  • Apply log level, timestamp, and file sink consistently before app subsystem creation

PR Type

Enhancement


Description

  • Implement file logging sink with config-driven path resolution

  • Track config source path to resolve relative logging file paths

  • Apply log level, timestamp, and file sink before app subsystem creation

  • Add comprehensive tests and documentation for file logging behavior


Diagram Walkthrough

flowchart LR
  A["Config Load"] -->|"preserves source_path"| B["LoadedConfig"]
  B -->|"resolve_logging_file_path"| C["Resolved Log Path"]
  C -->|"set_log_file_path"| D["File Sink Attached"]
  D -->|"non-fatal on error"| E["Console Logging Active"]
  B -->|"apply_log_level"| F["Logger Configured"]
  B -->|"apply_log_timestamp"| F
Loading

File Walkthrough

Relevant files
Enhancement
5 files
main.cpp
Add LoadedConfig struct and file sink setup flow                 
+41/-5   
config.hpp
Add resolve_logging_file_path function declaration             
+11/-0   
config.cpp
Implement logging file path resolution logic                         
+19/-0   
logging.hpp
Add set_log_file_path API for file sink configuration       
+7/-0     
logging.cpp
Implement file sink management with pattern updates           
+64/-14 
Tests
2 files
test_config.cpp
Add tests for logging file path resolution                             
+46/-0   
test_logging.cpp
Add file sink write and error handling tests                         
+53/-0   
Documentation
6 files
goggles.template.toml
Document logging.file path resolution semantics                   
+4/-0     
project_policies.md
Clarify app logger scope and file logging policy                 
+3/-1     
proposal.md
Add change proposal for file logging feature                         
+46/-0   
design.md
Add design rationale and alternatives analysis                     
+58/-0   
spec.md
Add formal requirements for file logging behavior               
+48/-0   
tasks.md
Add implementation task checklist and verification             
+46/-0   

Summary by CodeRabbit

  • New Features

    • Added file-based logging support configurable via the logging.file setting in the configuration.
    • Empty paths log to console only; absolute paths are used as-is; relative paths resolve against the configuration file directory.
    • File sink setup failures do not interrupt startup and preserve console logging.
  • Documentation

    • Updated configuration template and policy documentation to describe logging file path behavior.

- Introduce LoadedConfig startup flow that preserves config source path
- Centralize logging.file path resolution policy in util config helpers
- Apply log level, timestamp, and file sink consistently before app subsystem creation
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

This PR introduces file-based logging configuration for the application. It tracks the loaded configuration's source path, adds path resolution logic for logging file destinations (empty=console, absolute=as-is, relative=against config directory), dynamically creates a file sink during startup if configured, and includes comprehensive design documentation and tests for the feature.

Changes

Cohort / File(s) Summary
Documentation & Specification
config/goggles.template.toml, docs/project_policies.md, openspec/changes/update-file-logging-path-policy/*
Added documentation for file logging configuration semantics, including clarifications on empty/absolute/relative path behavior and scope limitation to app-level logging (not capture layer).
Config Path Resolution
src/util/config.hpp, src/util/config.cpp
Introduced resolve_logging_file_path() function to resolve logging file paths relative to the configuration file directory, handling empty, absolute, and relative path cases.
Logging System Enhancements
src/util/logging.hpp, src/util/logging.cpp
Added set_log_file_path() public API for dynamic file sink creation, refactored sink pattern management to support multiple sinks (console and optional file), and added internal helpers for pattern synchronization and file sink removal.
Startup Flow & Config Tracking
src/app/main.cpp
Introduced LoadedConfig struct to pair configuration with its source path; updated load_config_for_cli() return type and added apply_log_file() function to activate file logging during startup with proper error handling.
Tests
tests/util/test_config.cpp, tests/util/test_logging.cpp
Added unit tests for path resolution (empty, absolute, relative, working-directory independence) and file sink functionality (successful creation, error handling with logger preservation).

Sequence Diagram(s)

sequenceDiagram
    participant App as Application Startup
    participant Config as Config Loading
    participant PathRes as Path Resolution
    participant Logging as Logging System
    participant FS as File System

    App->>Config: load_config_for_cli()
    Config->>Config: Parse TOML/return defaults
    Config-->>App: LoadedConfig{config, source_path}

    App->>PathRes: apply_log_file(config, source_path)
    PathRes->>PathRes: resolve_logging_file_path(logging.file, source_path)
    
    alt Empty logging.file
        PathRes-->>Logging: (empty path - skip)
    else Absolute Path
        PathRes->>Logging: set_log_file_path(absolute)
    else Relative Path
        PathRes->>PathRes: Resolve against source_path.parent()
        PathRes->>Logging: set_log_file_path(resolved)
    end

    Logging->>FS: Create/open log file
    alt File creation succeeds
        Logging->>Logging: Attach file sink + update patterns
        Logging-->>App: Success
    else File creation fails
        Logging->>Logging: Log warning, keep console sink
        Logging-->>App: Error (non-fatal, continue startup)
    end

    App->>App: Continue subsystem initialization
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Poem

🐰 A rabbit hops through config files with glee,
Tracking paths from directories with spree!
File sinks dance with logs so bright,
Error handling keeps the startup tight—
No broken starts, just warnings in sight! 📝✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing standardized log file path resolution through a new LoadedConfig structure, centralized path resolution policy, and consistent logging setup during startup.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev/refine-logging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Arbitrary file write

Description: The new file-logging sink allows config-driven paths to create directories and open/append
to an arbitrary filesystem location (set_log_file_path calls create_directories and then
opens basic_file_sink_mt on the provided path), which could enable unintended file writes
if an attacker can influence logging.file (including ../ traversal after
resolve_logging_file_path() just normalizes) or if the app runs with elevated privileges.
logging.cpp [88-122]

Referred Code
auto set_log_file_path(const std::filesystem::path& path) -> Result<void> {
    if (!g_logger) {
        initialize_logger();
    }

    if (path.empty()) {
        remove_file_sink();
        return {};
    }

    std::error_code ec;
    const auto parent = path.parent_path();
    if (!parent.empty()) {
        std::filesystem::create_directories(parent, ec);
        if (ec) {
            return make_error<void>(ErrorCode::file_write_failed,
                                    "Failed to create log directory '" + parent.string() +
                                        "': " + ec.message());
        }
    }



 ... (clipped 14 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured log format: The logger formatting patterns added for console and file sinks emit plain text (non-JSON)
logs, which violates the checklist requirement for structured logs suitable for
auditing/monitoring.

Referred Code
constexpr auto CONSOLE_PATTERN = "[%^%l%$] %v";
constexpr auto CONSOLE_PATTERN_TIMESTAMP = "[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v";
constexpr auto FILE_PATTERN = "[%l] %v";
constexpr auto FILE_PATTERN_TIMESTAMP = "[%Y-%m-%d %H:%M:%S.%e] [%l] %v";

auto remove_file_sink() -> void {
    if (!g_logger || !g_file_sink) {
        return;
    }

    auto& sinks = g_logger->sinks();
    sinks.erase(std::remove(sinks.begin(), sinks.end(), g_file_sink), sinks.end());
    g_file_sink.reset();
}

auto update_sink_patterns() -> void {
    if (!g_logger || !g_console_sink) {
        return;
    }

    g_console_sink->set_pattern(g_timestamp_enabled ? CONSOLE_PATTERN_TIMESTAMP : CONSOLE_PATTERN);


 ... (clipped 4 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Audit scope unclear: The PR changes logging sinks and configuration but does not demonstrate that
security-relevant actions are logged with user ID, timestamp, action, and outcome, which
cannot be verified from the diff alone.

Referred Code
constexpr auto CONSOLE_PATTERN = "[%^%l%$] %v";
constexpr auto CONSOLE_PATTERN_TIMESTAMP = "[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v";
constexpr auto FILE_PATTERN = "[%l] %v";
constexpr auto FILE_PATTERN_TIMESTAMP = "[%Y-%m-%d %H:%M:%S.%e] [%l] %v";

auto remove_file_sink() -> void {
    if (!g_logger || !g_file_sink) {
        return;
    }

    auto& sinks = g_logger->sinks();
    sinks.erase(std::remove(sinks.begin(), sinks.end(), g_file_sink), sinks.end());
    g_file_sink.reset();
}

auto update_sink_patterns() -> void {
    if (!g_logger || !g_console_sink) {
        return;
    }

    g_console_sink->set_pattern(g_timestamp_enabled ? CONSOLE_PATTERN_TIMESTAMP : CONSOLE_PATTERN);


 ... (clipped 5 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Path input policy: The new set_log_file_path accepts config-driven filesystem paths and creates
directories/writes files without an explicit allowlist or sandbox policy, which may be
acceptable but requires confirmation of the intended security model.

Referred Code
auto set_log_file_path(const std::filesystem::path& path) -> Result<void> {
    if (!g_logger) {
        initialize_logger();
    }

    if (path.empty()) {
        remove_file_sink();
        return {};
    }

    std::error_code ec;
    const auto parent = path.parent_path();
    if (!parent.empty()) {
        std::filesystem::create_directories(parent, ec);
        if (ec) {
            return make_error<void>(ErrorCode::file_write_failed,
                                    "Failed to create log directory '" + parent.string() +
                                        "': " + ec.message());
        }
    }



 ... (clipped 13 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Flush file sink on removal

Flush the g_file_sink in remove_file_sink before removing it to ensure all
buffered log messages are written to the file.

src/util/logging.cpp [22-30]

 auto remove_file_sink() -> void {
     if (!g_logger || !g_file_sink) {
         return;
     }
 
+    g_file_sink->flush();
     auto& sinks = g_logger->sinks();
     sinks.erase(std::remove(sinks.begin(), sinks.end(), g_file_sink), sinks.end());
     g_file_sink.reset();
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This is a valid improvement to prevent potential log message loss by ensuring the sink's buffer is flushed before it is removed and destroyed, which is particularly important if a new log file is configured.

Medium
Possible issue
Add mutex to prevent race conditions

Add a mutex to remove_file_sink to protect against race conditions when
modifying the logger's sinks from multiple threads.

src/util/logging.cpp [22-30]

+#include <mutex>
+...
+
+namespace {
+...
+std::shared_ptr<spdlog::sinks::sink> g_file_sink;
+bool g_timestamp_enabled = false;
+std::mutex g_sink_mutex;
+...
+}
+
 auto remove_file_sink() -> void {
+    std::lock_guard<std::mutex> lock(g_sink_mutex);
     if (!g_logger || !g_file_sink) {
         return;
     }
 
     auto& sinks = g_logger->sinks();
     sinks.erase(std::remove(sinks.begin(), sinks.end(), g_file_sink), sinks.end());
     g_file_sink.reset();
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential race condition when modifying logger sinks, as spdlog does not guarantee thread safety for logger configuration changes. While the current usage appears safe, this change improves the robustness of the public logging API.

Low
  • More

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@config/goggles.template.toml`:
- Around line 58-62: The comment alignment for the example "logs/goggles.log"->
is missing a space before the arrow; edit the comment block containing the file
logging target examples so the third line matches the others by adding at least
one space between the closing quote and the arrow (i.e., change
"logs/goggles.log"-> to "logs/goggles.log" ->) so the 'file' key comment is
consistently formatted.

In `@tests/util/test_logging.cpp`:
- Around line 162-186: The test leaves the file sink added by set_log_file_path
registered on the global logger (created via initialize_logger / accessible via
get_logger()), causing a dangling open file after remove_all(temp_dir); fix by
removing that file sink at the end of the test before deleting the temp dir:
flush the logger (get_logger()->flush()), locate the file sink instance
previously returned by set_log_file_path (or inspect get_logger()->sinks()),
remove/unregister that sink from the logger and/or call the appropriate
drop/erase routine so the file is closed, then proceed to
std::filesystem::remove_all(temp_dir).
🧹 Nitpick comments (3)
tests/util/test_config.cpp (1)

236-261: Solid CWD-independence test with proper RAII guard.

The CurrentPathGuard pattern ensures CWD is restored even when assertions fail. Two minor observations:

  1. Temp directories (goggles_config_cwd_a/b) use fixed names — parallel test runs could collide. Consider a unique suffix if parallelism is ever enabled.
  2. If a REQUIRE fails before lines 259–260, the temp dirs won't be cleaned up (though they're in the system temp dir, so the impact is negligible).

Neither is blocking.

openspec/changes/update-file-logging-path-policy/specs/app-window/spec.md (1)

11-16: Scenario step format deviates from coding guidelines.

The coding guidelines prescribe - **WHEN** [condition] - **THEN** [expected result] for scenario steps. The current format uses separate - **GIVEN**, - **WHEN**, - **THEN**, - **AND** lines. This pattern repeats across all scenarios in the file (lines 12–15, 18–21, 24–27, 34–37, 40–43, 46–48).

For example, the first scenario would become:

Suggested format per guidelines
 #### Scenario: Empty logging.file keeps console-only behavior
-- **GIVEN** `[logging].file = ""`
-- **WHEN** the application starts and applies logging configuration
-- **THEN** no file sink SHALL be attached
-- **AND** console logging SHALL remain active
+- **WHEN** `[logging].file = ""` and the application starts and applies logging configuration - **THEN** no file sink SHALL be attached and console logging SHALL remain active

As per coding guidelines, use format - **WHEN** [condition] - **THEN** [expected result] for scenario steps in requirements.

src/util/logging.cpp (1)

109-115: File sink opens in truncate mode — is this intentional?

basic_file_sink_mt(path.string(), true) passes true for the truncate parameter, so the log file is wiped on every application launch. If the intent is to preserve logs across runs (e.g., for post-mortem debugging), append mode (false) would be more appropriate. If truncate is deliberate, a brief comment here would clarify the choice for future readers.

If append is preferred:
-        new_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.string(), true);
+        new_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.string(), /*truncate=*/false);

Comment on lines +58 to 62
# File logging target:
# "" -> console only
# "/abs/path.log" -> use absolute path as-is
# "logs/goggles.log"-> resolved relative to the loaded config file directory
file = ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing space before -> on line 61.

The alignment arrow on line 61 is flush against the closing quote, unlike lines 59–60 which have padding. Add at least one space for readability.

Suggested fix
 # File logging target:
 #   ""                -> console only
 #   "/abs/path.log"   -> use absolute path as-is
-#   "logs/goggles.log"-> resolved relative to the loaded config file directory
+#   "logs/goggles.log" -> resolved relative to the loaded config file directory
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# File logging target:
# "" -> console only
# "/abs/path.log" -> use absolute path as-is
# "logs/goggles.log"-> resolved relative to the loaded config file directory
file = ""
# File logging target:
# "" -> console only
# "/abs/path.log" -> use absolute path as-is
# "logs/goggles.log" -> resolved relative to the loaded config file directory
file = ""
🤖 Prompt for AI Agents
In `@config/goggles.template.toml` around lines 58 - 62, The comment alignment for
the example "logs/goggles.log"-> is missing a space before the arrow; edit the
comment block containing the file logging target examples so the third line
matches the others by adding at least one space between the closing quote and
the arrow (i.e., change "logs/goggles.log"-> to "logs/goggles.log" ->) so the
'file' key comment is consistently formatted.

Comment on lines +162 to +186
TEST_CASE("set_log_file_path writes logs to file sink", "[logging]") {
initialize_logger("file_sink_test");
set_log_level(spdlog::level::info);

const auto temp_dir =
std::filesystem::temp_directory_path() / "goggles_logging_test" / "file_sink";
std::filesystem::remove_all(temp_dir);
std::filesystem::create_directories(temp_dir);
const auto log_path = temp_dir / "goggles.log";

auto sink_result = set_log_file_path(log_path);
REQUIRE(sink_result.has_value());

constexpr const char* FILE_TEST_MESSAGE = "file sink integration marker";
GOGGLES_LOG_INFO("{}", FILE_TEST_MESSAGE);
get_logger()->flush();

std::ifstream file(log_path);
REQUIRE(file.is_open());

std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
REQUIRE(content.find(FILE_TEST_MESSAGE) != std::string::npos);

std::filesystem::remove_all(temp_dir);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Good integration test for the file-sink happy path.

One concern: after remove_all(temp_dir) on line 185, the file sink attached on line 172 is still registered in the global logger. On Linux the open file descriptor keeps working, but on other platforms (or if the OS reclaims the inode) later test cases that trigger logging may hit unexpected I/O errors. Consider removing the file sink at the end of the test to leave the logger in a clean state:

Suggested cleanup
     REQUIRE(content.find(FILE_TEST_MESSAGE) != std::string::npos);

+    // Remove file sink before deleting the directory
+    set_log_file_path({});
     std::filesystem::remove_all(temp_dir);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TEST_CASE("set_log_file_path writes logs to file sink", "[logging]") {
initialize_logger("file_sink_test");
set_log_level(spdlog::level::info);
const auto temp_dir =
std::filesystem::temp_directory_path() / "goggles_logging_test" / "file_sink";
std::filesystem::remove_all(temp_dir);
std::filesystem::create_directories(temp_dir);
const auto log_path = temp_dir / "goggles.log";
auto sink_result = set_log_file_path(log_path);
REQUIRE(sink_result.has_value());
constexpr const char* FILE_TEST_MESSAGE = "file sink integration marker";
GOGGLES_LOG_INFO("{}", FILE_TEST_MESSAGE);
get_logger()->flush();
std::ifstream file(log_path);
REQUIRE(file.is_open());
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
REQUIRE(content.find(FILE_TEST_MESSAGE) != std::string::npos);
std::filesystem::remove_all(temp_dir);
}
TEST_CASE("set_log_file_path writes logs to file sink", "[logging]") {
initialize_logger("file_sink_test");
set_log_level(spdlog::level::info);
const auto temp_dir =
std::filesystem::temp_directory_path() / "goggles_logging_test" / "file_sink";
std::filesystem::remove_all(temp_dir);
std::filesystem::create_directories(temp_dir);
const auto log_path = temp_dir / "goggles.log";
auto sink_result = set_log_file_path(log_path);
REQUIRE(sink_result.has_value());
constexpr const char* FILE_TEST_MESSAGE = "file sink integration marker";
GOGGLES_LOG_INFO("{}", FILE_TEST_MESSAGE);
get_logger()->flush();
std::ifstream file(log_path);
REQUIRE(file.is_open());
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
REQUIRE(content.find(FILE_TEST_MESSAGE) != std::string::npos);
// Remove file sink before deleting the directory
set_log_file_path({});
std::filesystem::remove_all(temp_dir);
}
🤖 Prompt for AI Agents
In `@tests/util/test_logging.cpp` around lines 162 - 186, The test leaves the file
sink added by set_log_file_path registered on the global logger (created via
initialize_logger / accessible via get_logger()), causing a dangling open file
after remove_all(temp_dir); fix by removing that file sink at the end of the
test before deleting the temp dir: flush the logger (get_logger()->flush()),
locate the file sink instance previously returned by set_log_file_path (or
inspect get_logger()->sinks()), remove/unregister that sink from the logger
and/or call the appropriate drop/erase routine so the file is closed, then
proceed to std::filesystem::remove_all(temp_dir).

@K1ngst0m K1ngst0m merged commit 464a1c2 into main Feb 6, 2026
4 checks passed
@K1ngst0m K1ngst0m deleted the dev/refine-logging branch February 6, 2026 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant