Ctrl K
ComunidadGuías…

server/src/confighttp_playnite.cpp

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * @file src/confighttp_playnite.cpp
 * @brief Playnite-specific HTTP endpoints and helpers (Windows-only).
 */

#ifdef _WIN32

  // standard includes
  #include <algorithm>
  #include <array>
  #include <cctype>
  #include <chrono>
  #include <cstdint>
  #include <cwctype>
  #include <filesystem>
  #include <fstream>
  #include <iterator>
  #include <limits>
  #include <map>
  #include <memory>
  #include <mutex>
  #include <optional>
  #include <regex>
  #include <sstream>
  #include <string>
  #include <string_view>
  #include <unordered_map>
  #include <unordered_set>
  #include <vector>

  // third-party includes
  #include <boost/property_tree/json_parser.hpp>
  #include <boost/property_tree/ptree.hpp>
  #include <nlohmann/json.hpp>
  #include <Simple-Web-Server/server_https.hpp>
  #include <zlib.h>

  // local includes
  #include "config_playnite.h"
  #include "confighttp.h"
  #include "httpcommon.h"
  #include "logging.h"
  #include "log_export.h"
  #include "src/platform/windows/ipc/misc_utils.h"
  #include "src/platform/windows/playnite_integration.h"
  #include "state_storage.h"
  #include "uuid.h"

  // Windows headers
  #include <KnownFolders.h>
  #include <ShlObj.h>
  #include <windows.h>
  #include <ws2tcpip.h>

  // boost
  #include <boost/crc.hpp>

namespace confighttp {

  // Bring request/response types into scope to match confighttp.cpp usage
  using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
  using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;

  // Forward declarations for helpers defined in confighttp.cpp
  bool authenticate(resp_https_t response, req_https_t request);
  void print_req(const req_https_t &request);
  void send_response(resp_https_t response, const nlohmann::json &output_tree);
  void bad_request(resp_https_t response, req_https_t request, const std::string &error_message = "Bad Request");
  void conflict(resp_https_t response, const std::string &error_message);
  bool check_content_type(resp_https_t response, req_https_t request, const std::string_view &contentType);

  struct playnite_install_state_t {
    std::optional<bool> installed;
    std::filesystem::path extensions_dir;
  };

  // Helper: determine whether the Playnite plugin is installed.
  // An active IPC connection is authoritative proof that the plugin is loaded,
  // even if the current service context cannot resolve the user's extension path.
  static playnite_install_state_t query_plugin_install_state(bool active) {
    playnite_install_state_t state;
    try {
      std::string destPath;
      if (platf::playnite::get_extension_target_dir(destPath)) {
        state.extensions_dir = destPath;
        state.installed =
          std::filesystem::exists(state.extensions_dir / "extension.yaml") &&
          std::filesystem::exists(state.extensions_dir / "SunshinePlaynite.psm1");
      } else if (active) {
        state.installed = true;
      }
    } catch (...) {
      if (active) {
        state.installed = true;
      }
    }
    return state;
  }

  // Enhance app JSON with a Playnite-derived cover path when applicable.
  void enhance_app_with_playnite_cover(nlohmann::json &input_tree) {
    try {
      if ((!input_tree.contains("image-path") || (input_tree["image-path"].is_string() && input_tree["image-path"].get<std::string>().empty())) &&
          input_tree.contains("playnite-id") && input_tree["playnite-id"].is_string()) {
        std::string cover;
        if (platf::playnite::get_cover_png_for_playnite_game(input_tree["playnite-id"].get<std::string>(), cover)) {
          input_tree["image-path"] = cover;
        }
      }
    } catch (...) {
      // Best-effort only
    }
  }

  void enhance_app_with_playnite_icon(nlohmann::json &input_tree) {
    try {
      if ((!input_tree.contains("playnite-icon-path") || (input_tree["playnite-icon-path"].is_string() && input_tree["playnite-icon-path"].get<std::string>().empty())) &&
          input_tree.contains("playnite-id") && input_tree["playnite-id"].is_string()) {
        std::string icon;
        if (platf::playnite::get_icon_png_for_playnite_game(input_tree["playnite-id"].get<std::string>(), icon)) {
          input_tree["playnite-icon-path"] = icon;
        }
      }
    } catch (...) {
      // Best-effort only
    }
  }

  // No longer needed: old fallback path resolver removed with AssocQueryString-based detection

  void getPlayniteStatus(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    if (!config::playnite.enabled) {
      send_response(response, nlohmann::json{{"active", false}, {"enabled", false}, {"available", false}, {"installed", false}, {"update_available", false}});
      return;
    }
    // Keep the Playnite IPC client alive when the UI refreshes status.
    // This updates the inactivity timer and ensures a fresh connection.
    platf::playnite::ensure_client_for_api();
    nlohmann::json out;
    // Active reflects current pipe/server connection only
    const bool active = platf::playnite::is_active();
    out["active"] = active;
    // Deprecated fields removed: playnite_running, installed_unknown
    // Session requirement removed: IPC is available during RDP/lock; rely on
    // active IPC first, then fall back to per-user extension path resolution.
    const auto install_state = query_plugin_install_state(active);
    const auto &dest = install_state.extensions_dir;
    if (install_state.installed.has_value()) {
      out["installed"] = *install_state.installed;
    } else {
      out["installed"] = nullptr;
    }
    out["extensions_dir"] = dest.string();
    // Version info and update flag
    auto normalize_ver = [](std::string s) {
      // strip leading 'v' and whitespace
      while (!s.empty() && (s[0] == ' ' || s[0] == '\t')) {
        s.erase(s.begin());
      }
      if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) {
        s.erase(s.begin());
      }
      return s;
    };
    auto semver_cmp = [&](const std::string &a, const std::string &b) {
      auto to_parts = [](const std::string &s) {
        std::vector<int> parts;
        int cur = 0;
        bool have = false;
        for (size_t i = 0; i <= s.size(); ++i) {
          if (i == s.size() || s[i] == '.') {
            parts.push_back(have ? cur : 0);
            cur = 0;
            have = false;
          } else if (s[i] >= '0' && s[i] <= '9') {
            have = true;
            cur = cur * 10 + (s[i] - '0');
          } else {
            // stop at first non-digit/non-dot
            break;
          }
        }
        while (!parts.empty() && parts.back() == 0) {
          parts.pop_back();
        }
        return parts;
      };
      auto pa = to_parts(normalize_ver(a));
      auto pb = to_parts(normalize_ver(b));
      size_t n = std::max(pa.size(), pb.size());
      pa.resize(n, 0);
      pb.resize(n, 0);
      for (size_t i = 0; i < n; ++i) {
        if (pa[i] < pb[i]) {
          return -1;
        }
        if (pa[i] > pb[i]) {
          return 1;
        }
      }
      return 0;
    };
    std::string installed_ver, packaged_ver;
    bool have_installed = platf::playnite::get_installed_plugin_version(installed_ver);
    bool have_packaged = platf::playnite::get_packaged_plugin_version(packaged_ver);
    if (have_installed) {
      out["installed_version"] = installed_ver;
    }
    if (have_packaged) {
      out["packaged_version"] = packaged_ver;
    }
    bool update_available = false;
    if (out["installed"].is_boolean() && out["installed"].get<bool>() && have_installed && have_packaged) {
      update_available = semver_cmp(installed_ver, packaged_ver) < 0;
    }
    out["update_available"] = update_available;
    // No session readiness flag; IPC works through RDP/lock. Frontend derives readiness from installed/active.
    // Reduce verbosity: this endpoint can be polled frequently by the UI.
    // Log at debug level instead of info to avoid log spam while still
    // keeping the line available when debugging.
    BOOST_LOG(debug) << "Playnite status: active=" << out["active"]
                     << ", dir=" << (dest.empty() ? std::string("(unknown)") : dest.string())
                     << ", installed_version=" << (have_installed ? installed_ver : std::string(""))
                     << ", packaged_version=" << (have_packaged ? packaged_ver : std::string(""))
                     << ", update_available=" << (update_available ? "true" : "false");
    send_response(response, out);
  }

  void getPlayniteGames(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    try {
      if (!query_plugin_install_state(platf::playnite::is_active()).installed.value_or(false)) {
        SimpleWeb::CaseInsensitiveMultimap headers;
        headers.emplace("Content-Type", "application/json");
        headers.emplace("X-Frame-Options", "DENY");
        headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
        response->write(SimpleWeb::StatusCode::success_ok, "[]", headers);
        return;
      }
      std::string json;
      if (!platf::playnite::get_games_list_json(json)) {
        // return empty array if not available
        json = "[]";
      }
      BOOST_LOG(debug) << "Playnite games: json length=" << json.size();
      SimpleWeb::CaseInsensitiveMultimap headers;
      headers.emplace("Content-Type", "application/json");
      headers.emplace("X-Frame-Options", "DENY");
      headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
      response->write(SimpleWeb::StatusCode::success_ok, json, headers);
    } catch (std::exception &e) {
      bad_request(response, request, e.what());
    }
  }

  void getPlayniteCategories(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    try {
      if (!query_plugin_install_state(platf::playnite::is_active()).installed.value_or(false)) {
        SimpleWeb::CaseInsensitiveMultimap headers;
        headers.emplace("Content-Type", "application/json");
        headers.emplace("X-Frame-Options", "DENY");
        headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
        response->write(SimpleWeb::StatusCode::success_ok, "[]", headers);
        return;
      }
      std::string json;
      if (!platf::playnite::get_categories_list_json(json)) {
        // return empty array if not available
        json = "[]";
      }
      BOOST_LOG(debug) << "Playnite categories: json length=" << json.size();
      SimpleWeb::CaseInsensitiveMultimap headers;
      headers.emplace("Content-Type", "application/json");
      headers.emplace("X-Frame-Options", "DENY");
      headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
      response->write(SimpleWeb::StatusCode::success_ok, json, headers);
    } catch (std::exception &e) {
      bad_request(response, request, e.what());
    }
  }

  void getPlaynitePlugins(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    try {
      if (!query_plugin_install_state(platf::playnite::is_active()).installed.value_or(false)) {
        SimpleWeb::CaseInsensitiveMultimap headers;
        headers.emplace("Content-Type", "application/json");
        headers.emplace("X-Frame-Options", "DENY");
        headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
        response->write(SimpleWeb::StatusCode::success_ok, "[]", headers);
        return;
      }
      std::string json;
      if (!platf::playnite::get_plugins_list_json(json)) {
        json = "[]";
      }
      BOOST_LOG(debug) << "Playnite plugins: json length=" << json.size();
      SimpleWeb::CaseInsensitiveMultimap headers;
      headers.emplace("Content-Type", "application/json");
      headers.emplace("X-Frame-Options", "DENY");
      headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
      response->write(SimpleWeb::StatusCode::success_ok, json, headers);
    } catch (std::exception &e) {
      bad_request(response, request, e.what());
    }
  }

  void installPlaynite(resp_https_t response, req_https_t request) {
    if (!check_content_type(response, request, "application/json")) {
      return;
    }
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    std::string err;
    nlohmann::json out;
    bool request_restart = false;
    try {
      std::stringstream ss;
      ss << request->content.rdbuf();
      if (ss.rdbuf()->in_avail() > 0) {
        auto in = nlohmann::json::parse(ss);
        request_restart = in.value("restart", false);
      }
    } catch (...) {
      // ignore body parse errors; treat as no-restart
    }
    // Prefer same resolved dir as status
    std::string target;
    bool have_target = platf::playnite::get_extension_target_dir(target);
    bool ok = false;
    if (have_target) {
      ok = platf::playnite::install_plugin_to(target, err);
    } else {
      ok = platf::playnite::install_plugin(err);
    }
    std::ostringstream log_msg;
    log_msg << "Playnite install: " << (ok ? "success" : "failed");
    if (have_target) {
      log_msg << " target=" << target;
    }
    log_msg << " restart=" << (request_restart ? "true" : "false");
    if (!ok && !err.empty()) {
      log_msg << " error=" << err;
    }
    BOOST_LOG(info) << log_msg.str();
    out["status"] = ok;
    if (!ok) {
      out["error"] = err;
    }
    // Optionally close and restart Playnite to pick up the new plugin
    if (ok && request_restart) {
      bool restarted = platf::playnite::restart_playnite();
      out["restarted"] = restarted;
    }
    send_response(response, out);
  }

  void uninstallPlaynite(resp_https_t response, req_https_t request) {
    if (!check_content_type(response, request, "application/json")) {
      return;
    }
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    std::string err;
    nlohmann::json out;
    bool request_restart = false;
    try {
      std::stringstream ss;
      ss << request->content.rdbuf();
      if (ss.rdbuf()->in_avail() > 0) {
        auto in = nlohmann::json::parse(ss);
        request_restart = in.value("restart", false);
      }
    } catch (...) {
      // ignore body parse errors; treat as no-restart
    }
    bool ok = platf::playnite::uninstall_plugin(err);
    {
      std::ostringstream log_msg;
      log_msg << "Playnite uninstall: " << (ok ? "success" : "failed")
              << " restart=" << (request_restart ? "true" : "false");
      if (!ok && !err.empty()) {
        log_msg << " error=" << err;
      }
      BOOST_LOG(info) << log_msg.str();
    }
    out["status"] = ok;
    if (!ok) {
      out["error"] = err;
    }
    if (ok && request_restart) {
      bool restarted = platf::playnite::restart_playnite();
      out["restarted"] = restarted;
    }
    send_response(response, out);
  }

  void postPlayniteForceSync(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    if (!config::playnite.enabled) {
      send_response(response, nlohmann::json{{"status", false}, {"error", "Playnite integration is disabled"}});
      return;
    }
    nlohmann::json out;
    bool ok = platf::playnite::force_sync();
    out["status"] = ok;
    send_response(response, out);
  }

  void postPlayniteCover(resp_https_t response, req_https_t request) {
    if (!check_content_type(response, request, "application/json")) {
      return;
    }
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);

    try {
      std::stringstream stream;
      stream << request->content.rdbuf();
      const auto input = nlohmann::json::parse(stream);
      const auto playnite_id = input.value("playnite_id", "");
      const auto cover_key = input.value("cover_key", "");
      if (playnite_id.empty() || cover_key.empty()) {
        bad_request(response, request, "Playnite game ID and cover key are required");
        return;
      }

      const auto cover_path = platf::appdata() / "covers" / (http::url_escape(cover_key) + ".png");
      std::error_code error;
      if (!std::filesystem::is_regular_file(cover_path, error) || error) {
        bad_request(response, request, "Uploaded cover was not found");
        return;
      }
      if (!platf::playnite::set_game_cover(playnite_id, cover_path.generic_string())) {
        bad_request(response, request, "Playnite did not confirm the cover metadata update");
        return;
      }
      if (!platf::playnite::force_sync()) {
        bad_request(response, request, "Playnite did not confirm a refreshed metadata snapshot");
        return;
      }

      const nlohmann::json output {
        {"status", true},
        {"path", cover_path.generic_string()}
      };
      send_response(response, output);
    } catch (const std::exception &e) {
      BOOST_LOG(warning) << "SetPlayniteCover: " << e.what();
      bad_request(response, request, e.what());
    }
  }

  void postPlayniteLaunch(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    nlohmann::json out;
    // Use unified restart path: will start Playnite if not running
    bool ok = platf::playnite::restart_playnite();
    out["status"] = ok;
    send_response(response, out);
  }

  using namespace log_export;

  namespace {
    namespace fs = std::filesystem;
    namespace pt = boost::property_tree;

    pt::ptree &ensure_pt_root(pt::ptree &tree) {
      auto it = tree.find("root");
      if (it == tree.not_found()) {
        auto inserted = tree.insert(tree.end(), std::make_pair(std::string("root"), pt::ptree {}));
        return inserted->second;
      }
      return it->second;
    }

    struct CrashDismissalState {
      std::string filename;
      std::string captured_at;
      std::string dismissed_at;
    };

    std::optional<CrashDismissalState> load_crash_dismissal_state() {
      statefile::migrate_recent_state_keys();
      const std::string &path_str = statefile::vibeshine_state_path();
      if (path_str.empty()) {
        return std::nullopt;
      }
      std::lock_guard<std::mutex> lock(statefile::state_mutex());
      fs::path path(path_str);
      pt::ptree tree;
      try {
        if (statefile::load_json(path.string(), tree) != statefile::json_load_result_e::loaded) {
          return std::nullopt;
        }
      } catch (const std::exception &e) {
        BOOST_LOG(warning) << "Crash dismissal: failed to read state file: " << e.what();
        return std::nullopt;
      }
      auto root_it = tree.find("root");
      if (root_it == tree.not_found()) {
        return std::nullopt;
      }
      auto dismissal_it = root_it->second.find("crashdump_dismissal");
      if (dismissal_it == root_it->second.not_found()) {
        return std::nullopt;
      }
      CrashDismissalState state;
      state.filename = dismissal_it->second.get<std::string>("filename", "");
      state.captured_at = dismissal_it->second.get<std::string>("captured_at", "");
      state.dismissed_at = dismissal_it->second.get<std::string>("dismissed_at", "");
      if (state.filename.empty()) {
        return std::nullopt;
      }
      return state;
    }

    bool save_crash_dismissal_state(const CrashDismissalState &state) {
      statefile::migrate_recent_state_keys();
      const std::string &path_str = statefile::vibeshine_state_path();
      if (path_str.empty()) {
        return false;
      }
      std::lock_guard<std::mutex> lock(statefile::state_mutex());
      fs::path path(path_str);
      pt::ptree tree;
      if (!statefile::load_json_for_update(path.string(), tree)) {
        return false;
      }
      auto &root = ensure_pt_root(tree);
      pt::ptree node;
      node.put("filename", state.filename);
      node.put("captured_at", state.captured_at);
      node.put("dismissed_at", state.dismissed_at);
      root.put_child("crashdump_dismissal", node);
      try {
        fs::path dir = path.parent_path();
        if (!dir.empty() && !fs::exists(dir)) {
          fs::create_directories(dir);
        }
      } catch (const std::exception &e) {
        BOOST_LOG(error) << "Crash dismissal: failed to prepare directories: " << e.what();
      }
      try {
        statefile::write_json_atomic(path.string(), tree);
      } catch (const std::exception &e) {
        BOOST_LOG(error) << "Crash dismissal: failed to write state file: " << e.what();
        return false;
      }
      return true;
    }
  }  // namespace

  static bool read_file_if_exists(const std::filesystem::path &p, std::string &out, std::optional<std::filesystem::file_time_type> *write_time = nullptr) {
    std::error_code ec {};
    if (!std::filesystem::exists(p, ec) || std::filesystem::is_directory(p, ec)) {
      return false;
    }
    try {
      if (write_time) {
        auto mtime = std::filesystem::last_write_time(p, ec);
        if (!ec) {
          *write_time = mtime;
        } else {
          *write_time = std::nullopt;
        }
      }
      std::ifstream f(p, std::ios::binary);
      if (!f) {
        return false;
      }
      std::ostringstream ss;
      ss << f.rdbuf();
      out = ss.str();
      return true;
    } catch (...) {
      return false;
    }
  }

  namespace {
    struct log_candidate_t {
      std::filesystem::path path;
      std::filesystem::file_time_type mtime;
    };

    void add_log_candidate(const std::filesystem::path &path, std::vector<log_candidate_t> &out) {
      std::error_code ec;
      if (!std::filesystem::exists(path, ec) || std::filesystem::is_directory(path, ec)) {
        return;
      }
      auto mtime = std::filesystem::last_write_time(path, ec);
      if (ec) {
        return;
      }
      out.push_back({path, mtime});
    }

    void add_session_log_candidates(const std::filesystem::path &dir, const std::string &prefix, std::vector<log_candidate_t> &out) {
      std::error_code ec;
      if (!std::filesystem::exists(dir, ec) || !std::filesystem::is_directory(dir, ec)) {
        return;
      }
      for (std::filesystem::directory_iterator it(dir, ec); it != std::filesystem::directory_iterator(); ++it) {
        if (ec) {
          break;
        }
        std::error_code file_ec;
        if (!it->is_regular_file(file_ec)) {
          continue;
        }
        const auto filename = it->path().filename().string();
        if (filename.rfind(prefix, 0) != 0) {
          continue;
        }
        add_log_candidate(it->path(), out);
      }
    }

    std::optional<std::filesystem::path> pick_latest_log(const std::vector<log_candidate_t> &candidates) {
      if (candidates.empty()) {
        return std::nullopt;
      }
      const auto it = std::max_element(candidates.begin(), candidates.end(), [](const auto &a, const auto &b) {
        return a.mtime < b.mtime;
      });
      if (it == candidates.end()) {
        return std::nullopt;
      }
      return it->path;
    }

    void add_helper_log_roots(const std::filesystem::path &root, const std::string &base_name, std::vector<log_candidate_t> &candidates, bool include_temp = true) {
      if (root.empty()) {
        return;
      }
      const auto sunshine_base = root / L"Sunshine";
      add_session_log_candidates(sunshine_base / L"logs", base_name + "-", candidates);
      add_log_candidate(sunshine_base / (base_name + ".log"), candidates);

      if (include_temp) {
        const auto temp_base = root / L"Temp";
        add_session_log_candidates(temp_base / L"Sunshine" / L"logs", base_name + "-", candidates);
        add_log_candidate(temp_base / (base_name + ".log"), candidates);
      }
    }

    std::optional<std::filesystem::path> find_latest_helper_log(const std::string &base_name) {
      std::vector<log_candidate_t> candidates;
      try {
        platf::dxgi::safe_token user_token;
        user_token.reset(platf::dxgi::retrieve_users_token(false));

        auto add_known_folder = [&](REFKNOWNFOLDERID id) {
          PWSTR baseW = nullptr;
          if (SUCCEEDED(SHGetKnownFolderPath(id, 0, user_token.get(), &baseW)) && baseW) {
            std::filesystem::path base = baseW;
            CoTaskMemFree(baseW);
            add_helper_log_roots(base, base_name, candidates);
          }
        };

        add_known_folder(FOLDERID_RoamingAppData);
        add_known_folder(FOLDERID_LocalAppData);
      } catch (...) {}

      auto add_csidl_root = [&](int csidl) {
        wchar_t baseW[MAX_PATH] = {};
        if (!SUCCEEDED(SHGetFolderPathW(nullptr, csidl, nullptr, SHGFP_TYPE_CURRENT, baseW))) {
          return;
        }
        std::filesystem::path base = std::filesystem::path(baseW);
        add_helper_log_roots(base, base_name, candidates);
      };

      add_csidl_root(CSIDL_APPDATA);
      add_csidl_root(CSIDL_LOCAL_APPDATA);

      try {
        wchar_t tmpPathW[MAX_PATH] = {};
        DWORD n = GetTempPathW(_countof(tmpPathW), tmpPathW);
        if (n > 0 && n < _countof(tmpPathW)) {
          std::filesystem::path temp_base = std::filesystem::path(tmpPathW);
          add_helper_log_roots(temp_base, base_name, candidates, false);
        }
      } catch (...) {}

      return pick_latest_log(candidates);
    }
  }  // namespace

  bool is_helper_log_source(const std::string &source) {
    return source == "display_helper" || source == "playnite" || source == "playnite_launcher" || source == "wgc";
  }

  bool read_helper_log(const std::string &source, std::string &out) {
    std::string base_name;
    if (source == "display_helper") {
      base_name = "sunshine_display_helper";
    } else if (source == "playnite") {
      base_name = "sunshine_playnite";
    } else if (source == "playnite_launcher") {
      base_name = "sunshine_playnite_launcher";
    } else if (source == "wgc") {
      base_name = "sunshine_wgc_helper";
    } else {
      return false;
    }

    auto latest = find_latest_helper_log(base_name);
    if (!latest) {
      return false;
    }
    return read_file_if_exists(*latest, out);
  }

  constexpr std::uint64_t kCrashBundleMaxBytes = 30ull * 1024ull * 1024ull;
  static std::uint64_t estimate_zip_entry_size(std::size_t name_len, std::uint64_t data_size);

  static std::vector<ZipDataEntry> collect_support_logs() {
    std::vector<ZipDataEntry> entries;
    export_log_sanitizer_t sanitizer;

    auto add_recent_logs = [&](const std::filesystem::path &dir, const std::string &prefix, const std::string &suffix, std::size_t limit, const std::string &zip_prefix) {
      std::vector<log_candidate_t> candidates;
      std::error_code ec;
      if (!std::filesystem::exists(dir, ec) || !std::filesystem::is_directory(dir, ec)) {
        return;
      }
      for (std::filesystem::directory_iterator it(dir, ec); it != std::filesystem::directory_iterator(); ++it) {
        if (ec) {
          break;
        }
        std::error_code file_ec;
        if (!it->is_regular_file(file_ec)) {
          continue;
        }
        const auto filename = it->path().filename().string();
        if (filename.rfind(prefix, 0) != 0) {
          continue;
        }
        if (filename.size() < suffix.size() || filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) != 0) {
          continue;
        }
        add_log_candidate(it->path(), candidates);
      }
      std::sort(candidates.begin(), candidates.end(), [](const auto &a, const auto &b) {
        return a.mtime > b.mtime;
      });
      if (candidates.size() > limit) {
        candidates.resize(limit);
      }
      for (const auto &candidate : candidates) {
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(candidate.path, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, zip_prefix + candidate.path.filename().string(), std::move(data), mtime));
        }
      }
    };

    // ArtLight log directory (session logging). Retention keeps up to ~30
    // sessions x 5 rollover files here; exporting all of them made the bundle
    // hundreds of megabytes and blew past the HTTP content deadline. Newest
    // files win, bounded by both a file count and a total byte budget.
    constexpr std::size_t k_max_session_export_files = 32;
    constexpr std::uintmax_t k_max_session_export_bytes = 64ull * 1024ull * 1024ull;
    try {
      bool collected_directory = false;
      if (auto log_dir = logging::session_log_directory()) {
        std::vector<log_candidate_t> candidates;
        std::error_code ec;
        for (std::filesystem::directory_iterator it(*log_dir, ec); it != std::filesystem::directory_iterator(); ++it) {
          if (ec) {
            break;
          }
          std::error_code file_ec;
          if (!it->is_regular_file(file_ec)) {
            continue;
          }
          add_log_candidate(it->path(), candidates);
        }
        std::sort(candidates.begin(), candidates.end(), [](const auto &a, const auto &b) {
          return a.mtime > b.mtime;
        });
        std::uintmax_t budget_used = 0;
        std::size_t files_added = 0;
        for (const auto &candidate : candidates) {
          if (files_added >= k_max_session_export_files) {
            break;
          }
          std::error_code size_ec;
          const auto size = std::filesystem::file_size(candidate.path, size_ec);
          if (size_ec || size > k_max_session_export_bytes - budget_used) {
            continue;
          }
          std::string data;
          std::optional<std::filesystem::file_time_type> mtime;
          if (read_file_if_exists(candidate.path, data, &mtime)) {
            if (data.size() > k_max_session_export_bytes - budget_used) {
              continue;
            }
            budget_used += data.size();
            ++files_added;
            entries.push_back(make_export_log_entry(sanitizer, candidate.path.filename().string(), std::move(data), mtime));
            collected_directory = true;
          }
        }
      }
      if (!collected_directory) {
        auto current_log = logging::current_log_file();
        if (!current_log.empty()) {
          std::string data;
          std::optional<std::filesystem::file_time_type> mtime;
          if (read_file_if_exists(current_log, data, &mtime)) {
            entries.push_back(make_export_log_entry(sanitizer, current_log.filename().string(), std::move(data), mtime));
          }
        }
      }
    } catch (...) {}

    // Playnite plugin log (Roaming\Sunshine\sunshine_playnite.log)
    try {
      platf::dxgi::safe_token user_token;
      user_token.reset(platf::dxgi::retrieve_users_token(false));
      PWSTR roamingW = nullptr;
      if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, user_token.get(), &roamingW)) && roamingW) {
        std::filesystem::path p = std::filesystem::path(roamingW) / L"Sunshine" / L"sunshine_playnite.log";
        CoTaskMemFree(roamingW);
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
        }
      }
    } catch (...) {}

    // Plugin fallback log: try user's LocalAppData\Temp then process TEMP
    try {
      platf::dxgi::safe_token user_token;
      user_token.reset(platf::dxgi::retrieve_users_token(false));
      PWSTR localW = nullptr;
      if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, user_token.get(), &localW)) && localW) {
        std::filesystem::path p = std::filesystem::path(localW) / L"Temp" / L"sunshine_playnite.log";
        CoTaskMemFree(localW);
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
        }
      }
    } catch (...) {}
    try {
      wchar_t tmpPathW[MAX_PATH] = {};
      DWORD n = GetTempPathW(_countof(tmpPathW), tmpPathW);
      if (n > 0 && n < _countof(tmpPathW)) {
        std::filesystem::path p = std::filesystem::path(tmpPathW) / L"sunshine_playnite.log";
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
        }
      }
    } catch (...) {}

    auto add_playnite_from_base = [&](const std::filesystem::path &base) {
      bool any = false;
      {
        std::string data;
        auto p = base / L"playnite.log";
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
          any = true;
        }
      }
      {
        std::string data;
        auto p = base / L"extensions.log";
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
          any = true;
        }
      }
      {
        std::string data;
        auto p = base / L"launcher.log";
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
          any = true;
        }
      }
      return any;
    };

    bool got_playnite_logs = false;
    try {
      platf::dxgi::safe_token user_token;
      user_token.reset(platf::dxgi::retrieve_users_token(false));
      auto add_from_known = [&](REFKNOWNFOLDERID id) {
        PWSTR pathW = nullptr;
        if (SUCCEEDED(SHGetKnownFolderPath(id, 0, user_token.get(), &pathW)) && pathW) {
          std::filesystem::path base = std::filesystem::path(pathW) / L"Playnite";
          CoTaskMemFree(pathW);
          if (add_playnite_from_base(base)) {
            got_playnite_logs = true;
          }
        }
      };
      add_from_known(FOLDERID_RoamingAppData);
      add_from_known(FOLDERID_LocalAppData);
    } catch (...) {}

    if (!got_playnite_logs) {
      try {
        wchar_t buf[MAX_PATH] = {};
        if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, buf))) {
          got_playnite_logs |= add_playnite_from_base(std::filesystem::path(buf) / L"Playnite");
        }
      } catch (...) {}
      try {
        wchar_t buf[MAX_PATH] = {};
        if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, buf))) {
          got_playnite_logs |= add_playnite_from_base(std::filesystem::path(buf) / L"Playnite");
        }
      } catch (...) {}
    }

    // One scan per directory, newest few files per helper prefix. The previous
    // shape re-enumerated the directory once per prefix and read every session
    // file ever rotated, which multiplied into the export slowdown.
    constexpr std::size_t k_max_helper_logs_per_prefix = 6;
    auto add_session_logs_with_prefixes = [&](const std::filesystem::path &dir, const std::vector<std::string> &prefixes) {
      std::map<std::string, std::vector<log_candidate_t>> per_prefix;
      std::error_code ec;
      for (std::filesystem::directory_iterator it(dir, ec); it != std::filesystem::directory_iterator(); ++it) {
        if (ec) {
          break;
        }
        std::error_code file_ec;
        if (!it->is_regular_file(file_ec)) {
          continue;
        }
        const auto filename = it->path().filename().string();
        for (const auto &prefix : prefixes) {
          if (filename.rfind(prefix, 0) == 0) {
            add_log_candidate(it->path(), per_prefix[prefix]);
            break;
          }
        }
      }
      for (auto &[prefix, candidates] : per_prefix) {
        std::sort(candidates.begin(), candidates.end(), [](const auto &a, const auto &b) {
          return a.mtime > b.mtime;
        });
        if (candidates.size() > k_max_helper_logs_per_prefix) {
          candidates.resize(k_max_helper_logs_per_prefix);
        }
        for (const auto &candidate : candidates) {
          std::string data;
          std::optional<std::filesystem::file_time_type> mtime;
          if (read_file_if_exists(candidate.path, data, &mtime)) {
            entries.push_back(make_export_log_entry(sanitizer, candidate.path.filename().string(), std::move(data), mtime));
          }
        }
      }
    };

    // The known-folder and CSIDL passes below resolve to the same directories
    // when ArtLight runs in a user session; without dedup every helper log was
    // read from disk and regex-sanitized twice.
    std::unordered_set<std::wstring> visited_helper_bases;
    auto mark_base_visited = [&](const std::filesystem::path &base) {
      std::error_code ec;
      auto canonical = std::filesystem::weakly_canonical(base, ec);
      std::wstring key = (ec ? base : canonical).native();
      std::transform(key.begin(), key.end(), key.begin(), ::towlower);
      return visited_helper_bases.insert(key).second;
    };

    auto add_user_helper_logs = [&](const std::filesystem::path &base) {
      if (!mark_base_visited(base)) {
        return;
      }
      // Legacy single-file helper logs (kept for backwards compatibility).
      {
        std::filesystem::path p = base / L"sunshine_playnite.log";
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
        }
      }
      {
        std::filesystem::path p = base / L"sunshine_playnite_launcher.log";
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
        }
      }
      {
        std::filesystem::path p = base / L"sunshine_launcher.log";
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
        }
      }
      {
        std::filesystem::path p = base / L"sunshine_display_helper.log";
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
        }
      }
      {
        std::filesystem::path p = base / L"sunshine_wgc_helper.log";
        std::string data;
        std::optional<std::filesystem::file_time_type> mtime;
        if (read_file_if_exists(p, data, &mtime)) {
          entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
        }
      }

      // Session-mode helper logs live under Roaming/LocalAppData\\Sunshine\\logs.
      static const std::vector<std::string> helper_log_prefixes {
        "sunshine_playnite_launcher-",
        "sunshine_playnite-",
        "sunshine_launcher-",
        "sunshine_display_helper-",
        "sunshine_wgc_helper-",
      };
      add_session_logs_with_prefixes(base / L"logs", helper_log_prefixes);
    };

    try {
      auto add_user_sunshine_logs = [&](REFKNOWNFOLDERID id) {
        platf::dxgi::safe_token user_token;
        user_token.reset(platf::dxgi::retrieve_users_token(false));
        PWSTR baseW = nullptr;
        if (SUCCEEDED(SHGetKnownFolderPath(id, 0, user_token.get(), &baseW)) && baseW) {
          std::filesystem::path base = std::filesystem::path(baseW) / L"Sunshine";
          CoTaskMemFree(baseW);
          add_user_helper_logs(base);
        }
      };
      add_user_sunshine_logs(FOLDERID_RoamingAppData);
      add_user_sunshine_logs(FOLDERID_LocalAppData);
    } catch (...) {}

    auto try_add_sunshine_logs = [&](int csidl) {
      wchar_t baseW[MAX_PATH] = {};
      if (!SUCCEEDED(SHGetFolderPathW(nullptr, csidl, nullptr, SHGFP_TYPE_CURRENT, baseW))) {
        return;
      }
      std::filesystem::path base = std::filesystem::path(baseW) / L"Sunshine";
      add_user_helper_logs(base);
    };

    try_add_sunshine_logs(CSIDL_APPDATA);
    try_add_sunshine_logs(CSIDL_LOCAL_APPDATA);

    try {
      std::filesystem::path cfg = platf::appdata();
      std::filesystem::path p = cfg / "sunshine_launcher.log";
      std::string data;
      std::optional<std::filesystem::file_time_type> mtime;
      if (read_file_if_exists(p, data, &mtime)) {
        entries.push_back(make_export_log_entry(sanitizer, p.filename().string(), std::move(data), mtime));
      }
    } catch (...) {}

    // Installer logs persisted by the bootstrapper, plus recent MSI logs still in TEMP.
    try {
      std::filesystem::path install_log_dir = platf::appdata() / "logs" / "installer";
      add_recent_logs(install_log_dir, "vibeshine_", ".log", 12, "installer/");
    } catch (...) {}
    try {
      wchar_t tmpPathW[MAX_PATH] = {};
      DWORD n = GetTempPathW(_countof(tmpPathW), tmpPathW);
      if (n > 0 && n < _countof(tmpPathW)) {
        std::filesystem::path temp_dir = std::filesystem::path(tmpPathW);
        add_recent_logs(temp_dir, "vibeshine_install_", ".log", 8, "installer/temp/");
        add_recent_logs(temp_dir, "vibeshine_preinstall_remove_", ".log", 8, "installer/temp/");
        add_recent_logs(temp_dir, "vibeshine_uninstall_", ".log", 4, "installer/temp/");
        add_recent_logs(temp_dir, "vibeshine_uninstall_remove_", ".log", 4, "installer/temp/");
        add_recent_logs(temp_dir, "vibeshine_cli", ".log", 4, "installer/temp/");
      }
    } catch (...) {}

    {
      std::vector<ZipDataEntry> dedup;
      std::unordered_set<std::string> seen;
      dedup.reserve(entries.size());
      for (auto &e : entries) {
        if (seen.insert(e.name).second) {
          dedup.emplace_back(std::move(e));
        }
      }
      entries.swap(dedup);
    }

    // A crash-bundle part is capped at 30 MiB. Keep its in-memory log corpus
    // within that exact uncompressed ZIP estimate, so logs never force an
    // oversized first part before crash dumps are considered.
    constexpr std::uint64_t kZipEndOfCentralDirectorySize = 22;
    std::uint64_t estimated_log_size = kZipEndOfCentralDirectorySize;
    std::size_t omitted_logs = 0;
    std::vector<ZipDataEntry> bounded;
    bounded.reserve(entries.size());
    for (auto &entry : entries) {
      const auto entry_size = estimate_zip_entry_size(entry.name.size(), static_cast<std::uint64_t>(entry.data.size()));
      if (entry_size > kCrashBundleMaxBytes - estimated_log_size) {
        ++omitted_logs;
        continue;
      }
      estimated_log_size += entry_size;
      bounded.emplace_back(std::move(entry));
    }
    if (omitted_logs != 0) {
      BOOST_LOG(warning) << "Crash bundle omitted " << omitted_logs << " log entries to keep each part below 30 MiB.";
    }
    entries.swap(bounded);

    return entries;
  }

  void downloadPlayniteLogs(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    try {
      auto entries = collect_support_logs();
      std::string zip = build_zip_from_entries(entries);

      char fname[64];
      std::time_t tt = std::time(nullptr);
      std::tm tm {};
      localtime_s(&tm, &tt);
      std::snprintf(fname, sizeof(fname), "vibeshine_logs-%04d%02d%02d-%02d%02d%02d.zip", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);

      SimpleWeb::CaseInsensitiveMultimap headers;
      headers.emplace("Content-Type", "application/zip");
      std::string cd = std::string("attachment; filename=\"") + fname + "\"";
      headers.emplace("Content-Disposition", cd);
      headers.emplace("X-Frame-Options", "DENY");
      headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
      response->write(SimpleWeb::StatusCode::success_ok, zip, headers);
    } catch (std::exception &e) {
      bad_request(response, request, e.what());
    }
  }

  struct CrashDumpInfo {
    std::string process;
    std::filesystem::path path;
    std::filesystem::file_time_type write_time;
    std::uint64_t size;
  };

  struct ZipFileEntry {
    std::string name;
    std::filesystem::path path;
    std::filesystem::file_time_type write_time;
    std::uint64_t size;
  };

  struct CrashBundlePartPlan {
    std::vector<ZipFileEntry> files;
    bool include_logs = false;
    std::string filename;
    std::uint64_t estimated_size = 0;
  };

  struct CrashDumpTarget {
    std::string process;
    std::wstring prefix;
  };

  static const std::array<CrashDumpTarget, 4> kCrashDumpTargets = {{
    {"sunshine.exe", L"sunshine.exe."},
    {"sunshine_display_helper.exe", L"sunshine_display_helper.exe."},
    {"sunshine_wgc_capture.exe", L"sunshine_wgc_capture.exe."},
    {"playnite-launcher.exe", L"playnite-launcher.exe."},
  }};

  constexpr std::uint64_t kMinCrashDumpSunshineBytes = 10ull * 1024ull * 1024ull;

  static std::wstring to_lower_wstring(std::wstring value) {
    std::transform(value.begin(), value.end(), value.begin(), [](wchar_t ch) {
      return static_cast<wchar_t>(std::towlower(ch));
    });
    return value;
  }

  static std::string to_lower_ascii(std::string value) {
    std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
      return static_cast<char>(std::tolower(ch));
    });
    return value;
  }

  static bool has_dump_file_extension(const std::wstring &ext_lower) {
    return ext_lower == L".dmp" || ext_lower == L".mdmp" || ext_lower == L".hdmp";
  }

  static bool ends_with_ascii(std::string_view value, std::string_view suffix) {
    return value.size() >= suffix.size() && value.substr(value.size() - suffix.size()) == suffix;
  }

  static std::string trim_copy(std::string_view value);
  static std::string filename_from_path(std::string value);

  static bool has_numeric_instance_suffix(std::wstring_view value, std::size_t open_pos, std::size_t close_pos) {
    if (open_pos == std::wstring_view::npos || close_pos == std::wstring_view::npos || close_pos <= open_pos + 1) {
      return false;
    }
    for (std::size_t i = open_pos + 1; i < close_pos; ++i) {
      if (!std::iswdigit(value[i])) {
        return false;
      }
    }
    return true;
  }

  static std::string normalize_process_name_for_matching(std::string value) {
    value = to_lower_ascii(filename_from_path(trim_copy(value)));
    if (value.empty()) {
      return {};
    }

    const std::size_t close = value.rfind(')');
    const std::size_t open = value.rfind('(');
    if (open != std::string::npos && close != std::string::npos && close > open + 1) {
      bool numeric_suffix = true;
      for (std::size_t i = open + 1; i < close; ++i) {
        if (!std::isdigit(static_cast<unsigned char>(value[i]))) {
          numeric_suffix = false;
          break;
        }
      }
      if (numeric_suffix && close == value.size() - 1) {
        value.erase(open);
      }
    }

    return value;
  }

  static const CrashDumpTarget *find_crash_dump_target_by_process(std::string process_name) {
    process_name = normalize_process_name_for_matching(std::move(process_name));
    if (process_name.empty()) {
      return nullptr;
    }

    const bool has_exe = ends_with_ascii(process_name, ".exe");
    for (const auto &target : kCrashDumpTargets) {
      if (process_name == target.process) {
        return &target;
      }
      if (!has_exe) {
        if ((process_name + ".exe") == target.process) {
          return &target;
        }
      } else if (process_name.size() > 4) {
        const std::string without_ext = process_name.substr(0, process_name.size() - 4);
        if (without_ext == target.process.substr(0, target.process.size() - 4)) {
          return &target;
        }
      }
    }
    return nullptr;
  }

  static void add_unique_path(std::vector<std::filesystem::path> &paths, std::unordered_set<std::wstring> &seen, const std::filesystem::path &path) {
    if (path.empty()) {
      return;
    }
    std::filesystem::path normalized = path.lexically_normal();
    std::wstring key = to_lower_wstring(normalized.wstring());
    if (!seen.insert(key).second) {
      return;
    }
    paths.push_back(normalized);
  }

  static std::vector<std::filesystem::path> crash_dump_roots() {
    std::vector<std::filesystem::path> roots;
    std::unordered_set<std::wstring> seen;

    wchar_t sysDir[MAX_PATH] = {};
    UINT len = GetSystemDirectoryW(sysDir, _countof(sysDir));
    if (len > 0 && len < _countof(sysDir)) {
      std::filesystem::path base(sysDir);
      add_unique_path(roots, seen, base / L"config" / L"systemprofile" / L"AppData" / L"Local" / L"CrashDumps");
    }

    try {
      platf::dxgi::safe_token user_token;
      user_token.reset(platf::dxgi::retrieve_users_token(false));
      PWSTR localW = nullptr;
      if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, user_token.get(), &localW)) && localW) {
        std::filesystem::path base(localW);
        CoTaskMemFree(localW);
        add_unique_path(roots, seen, base / L"CrashDumps");
      }
    } catch (...) {}

    wchar_t localBaseW[MAX_PATH] = {};
    if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, localBaseW))) {
      add_unique_path(roots, seen, std::filesystem::path(localBaseW) / L"CrashDumps");
    }

    return roots;
  }

  static std::vector<std::filesystem::path> wer_roots() {
    std::vector<std::filesystem::path> roots;
    std::unordered_set<std::wstring> seen;

    auto add_wer_paths = [&](const std::filesystem::path &base) {
      add_unique_path(roots, seen, base / L"Microsoft" / L"Windows" / L"WER" / L"ReportQueue");
      add_unique_path(roots, seen, base / L"Microsoft" / L"Windows" / L"WER" / L"ReportArchive");
    };

    try {
      platf::dxgi::safe_token user_token;
      user_token.reset(platf::dxgi::retrieve_users_token(false));
      PWSTR localW = nullptr;
      if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, user_token.get(), &localW)) && localW) {
        std::filesystem::path base(localW);
        CoTaskMemFree(localW);
        add_wer_paths(base);
      }
    } catch (...) {}

    wchar_t localBaseW[MAX_PATH] = {};
    if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, localBaseW))) {
      add_wer_paths(std::filesystem::path(localBaseW));
    }

    return roots;
  }

  static std::string to_iso8601(const std::chrono::system_clock::time_point &tp) {
    std::time_t tt = std::chrono::system_clock::to_time_t(tp);
    std::tm tm {};
    gmtime_s(&tm, &tt);
    char buf[32];
    if (std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm) == 0) {
      return {};
    }
    return std::string(buf);
  }

  static const CrashDumpTarget *match_crash_dump_target(const std::wstring &filename_lower) {
    for (const auto &target : kCrashDumpTargets) {
      if (filename_lower.rfind(target.prefix, 0) == 0) {
        return &target;
      }
      if (!target.prefix.empty()) {
        std::wstring base = target.prefix.substr(0, target.prefix.size() - 1);
        if (filename_lower.rfind(base + L"(", 0) == 0) {
          const std::size_t open = base.size();
          const std::size_t close = filename_lower.find(L')', open + 1);
          if (has_numeric_instance_suffix(filename_lower, open, close) &&
              close + 1 < filename_lower.size() &&
              filename_lower[close + 1] == L'.') {
            return &target;
          }
        }
      }
    }
    return nullptr;
  }

  static bool is_sunshine_process(const std::string &process_lower) {
    return process_lower == "sunshine.exe";
  }

  static bool read_text_file_best_effort(const std::filesystem::path &p, std::string &out_utf8) {
    std::string raw;
    if (!read_file_if_exists(p, raw)) {
      return false;
    }
    if (raw.size() >= 2) {
      const unsigned char b0 = static_cast<unsigned char>(raw[0]);
      const unsigned char b1 = static_cast<unsigned char>(raw[1]);
      const bool le_bom = (b0 == 0xFF && b1 == 0xFE);
      const bool be_bom = (b0 == 0xFE && b1 == 0xFF);
      if (le_bom || be_bom) {
        const size_t byte_count = raw.size() - 2;
        const size_t len16 = byte_count / 2;
        std::wstring wide;
        wide.resize(len16);
        const unsigned char *data = reinterpret_cast<const unsigned char *>(raw.data() + 2);
        for (size_t i = 0; i < len16; ++i) {
          uint16_t code = 0;
          if (be_bom) {
            code = static_cast<uint16_t>((data[i * 2] << 8) | data[i * 2 + 1]);
          } else {
            code = static_cast<uint16_t>(data[i * 2] | (data[i * 2 + 1] << 8));
          }
          wide[i] = static_cast<wchar_t>(code);
        }
        while (!wide.empty() && wide.back() == L'\0') {
          wide.pop_back();
        }
        if (!wide.empty()) {
          int needed = WideCharToMultiByte(CP_UTF8, 0, wide.data(), static_cast<int>(wide.size()), nullptr, 0, nullptr, nullptr);
          if (needed > 0) {
            out_utf8.assign(static_cast<size_t>(needed), '\0');
            WideCharToMultiByte(CP_UTF8, 0, wide.data(), static_cast<int>(wide.size()), out_utf8.data(), needed, nullptr, nullptr);
            while (!out_utf8.empty() && out_utf8.back() == '\0') {
              out_utf8.pop_back();
            }
            return true;
          }
        }
      }
    }
    out_utf8 = raw;
    return true;
  }

  static std::string trim_copy(std::string_view value) {
    size_t start = 0;
    while (start < value.size() && std::isspace(static_cast<unsigned char>(value[start]))) {
      ++start;
    }
    size_t end = value.size();
    while (end > start && std::isspace(static_cast<unsigned char>(value[end - 1]))) {
      --end;
    }
    std::string out(value.substr(start, end - start));
    if (out.size() >= 2 && out.front() == '"' && out.back() == '"') {
      out = out.substr(1, out.size() - 2);
    }
    return out;
  }

  static std::string filename_from_path(std::string value) {
    const size_t pos = value.find_last_of("\\/");
    if (pos != std::string::npos) {
      value = value.substr(pos + 1);
    }
    return value;
  }

  static std::optional<std::string> wer_report_app_name(const std::string &content) {
    std::string app_path;
    std::string app_name;
    std::string_view view(content);
    size_t pos = 0;
    while (pos < view.size()) {
      size_t end = view.find_first_of("\r\n", pos);
      if (end == std::string_view::npos) {
        end = view.size();
      }
      std::string_view line = view.substr(pos, end - pos);
      pos = end + 1;
      if (end + 1 < view.size() && view[end] == '\r' && view[end + 1] == '\n') {
        pos = end + 2;
      }
      const size_t eq = line.find('=');
      if (eq == std::string_view::npos) {
        continue;
      }
      std::string key = to_lower_ascii(trim_copy(line.substr(0, eq)));
      std::string value = trim_copy(line.substr(eq + 1));
      if (key == "apppath") {
        app_path = value;
      } else if (key == "appname") {
        app_name = value;
      }
    }
    if (!app_path.empty()) {
      return normalize_process_name_for_matching(app_path);
    }
    if (!app_name.empty()) {
      return normalize_process_name_for_matching(app_name);
    }
    return std::nullopt;
  }

  static std::optional<CrashDumpInfo> newest_dmp_in_dir(const std::filesystem::path &dir, const std::string &process_lower) {
    std::error_code ec {};
    if (!std::filesystem::exists(dir, ec) || !std::filesystem::is_directory(dir, ec)) {
      return std::nullopt;
    }
    CrashDumpInfo best {};
    std::filesystem::file_time_type best_time {};
    bool have = false;
    for (const auto &entry : std::filesystem::directory_iterator(dir, ec)) {
      if (ec) {
        break;
      }
      if (!entry.is_regular_file(ec)) {
        continue;
      }
      auto ext = to_lower_wstring(entry.path().extension().wstring());
      if (!has_dump_file_extension(ext)) {
        continue;
      }
      auto write_time = entry.last_write_time(ec);
      if (ec) {
        ec.clear();
        continue;
      }
      std::error_code size_ec {};
      auto file_size_raw = std::filesystem::file_size(entry.path(), size_ec);
      if (size_ec) {
        continue;
      }
      if (file_size_raw > std::numeric_limits<std::uint64_t>::max()) {
        continue;
      }
      const std::uint64_t file_size = static_cast<std::uint64_t>(file_size_raw);
      if (!have || write_time > best_time) {
        best_time = write_time;
        best.process = process_lower;
        best.path = entry.path();
        best.write_time = write_time;
        best.size = file_size;
        have = true;
      }
    }
    if (!have) {
      return std::nullopt;
    }
    return best;
  }

  static bool is_recent_dump(const std::filesystem::file_time_type &write_time, std::chrono::hours max_age) {
    auto sys_time = file_time_to_system_clock(write_time);
    auto now = std::chrono::system_clock::now();
    return sys_time + max_age >= now;
  }

  static bool should_accept_dump(const CrashDumpInfo &info) {
    if (is_sunshine_process(info.process)) {
      return info.size >= kMinCrashDumpSunshineBytes;
    }
    return true;
  }

  static std::vector<CrashDumpInfo> find_recent_crash_dumps(std::chrono::hours max_age) {
    std::unordered_map<std::string, CrashDumpInfo> best;

    auto consider = [&](CrashDumpInfo info) {
      if (!is_recent_dump(info.write_time, max_age)) {
        return;
      }
      if (!should_accept_dump(info)) {
        return;
      }
      auto it = best.find(info.process);
      if (it == best.end() || info.write_time > it->second.write_time) {
        best[info.process] = std::move(info);
      }
    };

    for (const auto &root : crash_dump_roots()) {
      std::error_code ec {};
      if (!std::filesystem::exists(root, ec) || !std::filesystem::is_directory(root, ec)) {
        continue;
      }
      for (const auto &entry : std::filesystem::directory_iterator(root, ec)) {
        if (ec) {
          break;
        }
        if (!entry.is_regular_file(ec)) {
          continue;
        }
        auto filename = entry.path().filename().wstring();
        std::wstring lower = to_lower_wstring(filename);
        const CrashDumpTarget *target = match_crash_dump_target(lower);
        if (!target) {
          continue;
        }
        auto ext = to_lower_wstring(entry.path().extension().wstring());
        if (!has_dump_file_extension(ext)) {
          continue;
        }
        auto write_time = entry.last_write_time(ec);
        if (ec) {
          ec.clear();
          continue;
        }
        std::error_code size_ec {};
        auto file_size_raw = std::filesystem::file_size(entry.path(), size_ec);
        if (size_ec) {
          continue;
        }
        if (file_size_raw > std::numeric_limits<std::uint64_t>::max()) {
          continue;
        }
        CrashDumpInfo info;
        info.process = target->process;
        info.path = entry.path();
        info.write_time = write_time;
        info.size = static_cast<std::uint64_t>(file_size_raw);
        consider(std::move(info));
      }
    }

    for (const auto &root : wer_roots()) {
      std::error_code ec {};
      if (!std::filesystem::exists(root, ec) || !std::filesystem::is_directory(root, ec)) {
        continue;
      }
      for (const auto &entry : std::filesystem::directory_iterator(root, ec)) {
        if (ec) {
          break;
        }
        if (!entry.is_directory(ec)) {
          continue;
        }
        std::filesystem::path report_path = entry.path() / L"Report.wer";
        std::string report;
        if (!read_text_file_best_effort(report_path, report)) {
          continue;
        }
        auto app_name = wer_report_app_name(report);
        if (!app_name) {
          continue;
        }
        const CrashDumpTarget *target = find_crash_dump_target_by_process(*app_name);
        if (!target) {
          continue;
        }
        auto dump = newest_dmp_in_dir(entry.path(), target->process);
        if (!dump) {
          continue;
        }
        consider(std::move(*dump));
      }
    }

    std::vector<CrashDumpInfo> results;
    results.reserve(best.size());
    for (auto &kv : best) {
      results.push_back(std::move(kv.second));
    }
    std::sort(results.begin(), results.end(), [](const CrashDumpInfo &a, const CrashDumpInfo &b) {
      return a.write_time > b.write_time;
    });
    return results;
  }

  static std::uint64_t estimate_zip_entry_size(std::size_t name_len, std::uint64_t data_size) {
    constexpr std::uint64_t kLocalHeaderSize = 30;
    constexpr std::uint64_t kCentralHeaderSize = 46;
    return kLocalHeaderSize + kCentralHeaderSize + (static_cast<std::uint64_t>(name_len) * 2) + data_size;
  }

  static std::uint64_t estimate_zip_size(const std::vector<ZipDataEntry> &data_entries, const std::vector<ZipFileEntry> &file_entries) {
    constexpr std::uint64_t kEndOfCentralDirectory = 22;
    std::uint64_t total = kEndOfCentralDirectory;
    for (const auto &entry : data_entries) {
      total += estimate_zip_entry_size(entry.name.size(), static_cast<std::uint64_t>(entry.data.size()));
    }
    for (const auto &entry : file_entries) {
      total += estimate_zip_entry_size(entry.name.size(), entry.size);
    }
    return total;
  }

  static std::string crash_bundle_base_name() {
    char fname[80];
    std::time_t tt = std::time(nullptr);
    std::tm tm {};
    localtime_s(&tm, &tt);
    std::snprintf(fname, sizeof(fname), "sunshine_crashbundle-%04d%02d%02d-%02d%02d%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
    return std::string(fname);
  }

  static std::string crash_bundle_filename(const std::string &base, std::size_t part_index, std::size_t part_count) {
    if (part_count <= 1) {
      return base + ".zip";
    }
    return base + "-part" + std::to_string(part_index) + ".zip";
  }

  static std::vector<CrashBundlePartPlan> build_crash_bundle_plan(const std::vector<ZipDataEntry> &logs, const std::vector<CrashDumpInfo> &dumps) {
    std::vector<CrashBundlePartPlan> parts;
    const auto base = crash_bundle_base_name();
    CrashBundlePartPlan current;
    current.include_logs = true;
    current.estimated_size = estimate_zip_size(logs, {});
    parts.push_back(current);

    auto add_new_part = [&](bool include_logs) {
      CrashBundlePartPlan part;
      part.include_logs = include_logs;
      part.estimated_size = include_logs ? estimate_zip_size(logs, {}) : estimate_zip_size({}, {});
      parts.push_back(part);
    };

    for (const auto &dump : dumps) {
      ZipFileEntry entry {dump.path.filename().string(), dump.path, dump.write_time, dump.size};
      const std::uint64_t entry_est = estimate_zip_entry_size(entry.name.size(), entry.size);
      if (entry_est > kCrashBundleMaxBytes - estimate_zip_size({}, {})) {
        BOOST_LOG(warning) << "Crash bundle skipped oversized dump " << entry.name << " to keep each part below 30 MiB.";
        continue;
      }
      CrashBundlePartPlan *part = &parts.back();
      const bool part_has_payload = (part->include_logs && !logs.empty()) || !part->files.empty();
      if (part_has_payload && part->estimated_size > kCrashBundleMaxBytes - entry_est) {
        add_new_part(false);
        part = &parts.back();
      }
      part->files.push_back(entry);
      part->estimated_size += entry_est;
    }

    for (std::size_t i = 0; i < parts.size(); ++i) {
      parts[i].filename = crash_bundle_filename(base, i + 1, parts.size());
    }
    return parts;
  }

  struct CrashBundleSnapshot {
    std::vector<ZipDataEntry> logs;
    std::vector<CrashDumpInfo> dumps;
    std::vector<CrashBundlePartPlan> plan;
    std::chrono::steady_clock::time_point expires_at {};
    std::string id;
  };

  struct CrashBundleSnapshotCache {
    std::mutex mutex;
    std::unordered_map<std::string, std::shared_ptr<const CrashBundleSnapshot>> snapshots;
  };

  constexpr auto kCrashBundleSnapshotTtl = std::chrono::minutes {5};
  constexpr std::size_t kMaxCrashBundleSnapshots = 4;

  static CrashBundleSnapshotCache &crash_bundle_snapshot_cache() {
    static CrashBundleSnapshotCache cache;
    return cache;
  }

  static void prune_crash_bundle_snapshots(CrashBundleSnapshotCache &cache, std::chrono::steady_clock::time_point now) {
    for (auto it = cache.snapshots.begin(); it != cache.snapshots.end();) {
      if (it->second->expires_at <= now) {
        it = cache.snapshots.erase(it);
      } else {
        ++it;
      }
    }
  }

  // The manifest freezes the sanitized logs, discovered dumps, part plan, and
  // filenames. Download requests refer to its opaque id, so a newly-created
  // dump cannot shift a later part onto different content.
  static std::shared_ptr<const CrashBundleSnapshot> create_crash_bundle_snapshot() {
    auto snapshot = std::make_shared<CrashBundleSnapshot>();
    snapshot->logs = collect_support_logs();
    snapshot->dumps = find_recent_crash_dumps(std::chrono::hours(24 * 7));
    snapshot->plan = build_crash_bundle_plan(snapshot->logs, snapshot->dumps);
    snapshot->id = uuid_util::uuid_t::generate().string();
    snapshot->expires_at = std::chrono::steady_clock::now() + kCrashBundleSnapshotTtl;

    auto &cache = crash_bundle_snapshot_cache();
    std::lock_guard lock(cache.mutex);
    const auto now = std::chrono::steady_clock::now();
    prune_crash_bundle_snapshots(cache, now);
    while (cache.snapshots.size() >= kMaxCrashBundleSnapshots) {
      const auto oldest = std::min_element(cache.snapshots.begin(), cache.snapshots.end(), [](const auto &left, const auto &right) {
        return left.second->expires_at < right.second->expires_at;
      });
      cache.snapshots.erase(oldest);
    }
    cache.snapshots.insert_or_assign(snapshot->id, snapshot);
    return snapshot;
  }

  static std::shared_ptr<const CrashBundleSnapshot> find_crash_bundle_snapshot(std::string_view id) {
    auto &cache = crash_bundle_snapshot_cache();
    std::lock_guard lock(cache.mutex);
    const auto now = std::chrono::steady_clock::now();
    prune_crash_bundle_snapshots(cache, now);
    const auto it = cache.snapshots.find(std::string(id));
    return it == cache.snapshots.end() ? nullptr : it->second;
  }

  static bool crash_bundle_plan_has_dump(const CrashBundleSnapshot &snapshot) {
    return std::any_of(snapshot.plan.begin(), snapshot.plan.end(), [](const auto &part) {
      return !part.files.empty();
    });
  }

  static inline void write_le16(std::ostream &out, uint16_t v) {
    out.put(static_cast<char>(v & 0xFF));
    out.put(static_cast<char>((v >> 8) & 0xFF));
  }

  static inline void write_le32(std::ostream &out, uint32_t v) {
    out.put(static_cast<char>(v & 0xFF));
    out.put(static_cast<char>((v >> 8) & 0xFF));
    out.put(static_cast<char>((v >> 16) & 0xFF));
    out.put(static_cast<char>((v >> 24) & 0xFF));
  }

  static bool write_zip_bundle_to_path(const std::filesystem::path &dest, const std::vector<ZipDataEntry> &data_entries, const std::vector<ZipFileEntry> &file_entries, std::string &error) {
    std::ofstream out(dest, std::ios::binary | std::ios::trunc);
    if (!out) {
      error = "Failed to create crash bundle";
      return false;
    }

    struct CdEntry {
      std::string name;
      uint32_t crc;
      uint32_t comp_size;
      uint32_t uncomp_size;
      uint32_t offset;
      uint16_t method;
      uint16_t dostime;
      uint16_t dosdate;
    };

    std::vector<CdEntry> cd;

    auto add_entry = [&](const std::string &name, uint16_t method, uint32_t crc, uint32_t comp_size, uint32_t uncomp_size, uint16_t dostime, uint16_t dosdate, const std::string &payload) -> bool {
      auto pos = out.tellp();
      if (pos < 0 || static_cast<unsigned long long>(pos) > std::numeric_limits<uint32_t>::max()) {
        error = "ZIP entry offset overflow";
        return false;
      }
      uint32_t offset = static_cast<uint32_t>(pos);
      write_le32(out, 0x04034b50u);
      write_le16(out, 20);
      write_le16(out, 0);
      write_le16(out, method);
      write_le16(out, dostime);
      write_le16(out, dosdate);
      write_le32(out, crc);
      write_le32(out, comp_size);
      write_le32(out, uncomp_size);
      write_le16(out, static_cast<uint16_t>(name.size()));
      write_le16(out, 0);
      out.write(name.data(), name.size());
      out.write(payload.data(), payload.size());
      if (!out) {
        error = "Failed writing ZIP entry";
        return false;
      }
      cd.push_back(CdEntry {name, crc, comp_size, uncomp_size, offset, method, dostime, dosdate});
      return true;
    };

    for (const auto &entry : data_entries) {
      const std::string &name = entry.name;
      const std::string &data = entry.data;
      boost::crc_32_type crc;
      crc.process_bytes(data.data(), data.size());
      uint32_t checksum = crc.checksum();
      std::string compressed;
      bool use_deflate = deflate_buffer(data.data(), data.size(), compressed) && compressed.size() < data.size();
      const std::string &payload = use_deflate ? compressed : data;
      uint16_t method = use_deflate ? 8 : 0;
      uint32_t comp_size = static_cast<uint32_t>(payload.size());
      uint32_t uncomp_size = static_cast<uint32_t>(data.size());
      uint16_t dostime = 0, dosdate = 0;
      if (entry.write_time) {
        to_dos_datetime(file_time_to_system_clock(*entry.write_time), dostime, dosdate);
      } else {
        current_dos_datetime(dostime, dosdate);
      }
      if (!add_entry(name, method, checksum, comp_size, uncomp_size, dostime, dosdate, payload)) {
        return false;
      }
    }

    for (const auto &entry : file_entries) {
      std::error_code ec {};
      if (!std::filesystem::exists(entry.path, ec) || !std::filesystem::is_regular_file(entry.path, ec)) {
        error = "Crash dump no longer exists";
        return false;
      }
      std::error_code metadata_ec {};
      const auto current_size = std::filesystem::file_size(entry.path, metadata_ec);
      if (metadata_ec) {
        error = "Crash dump changed since the manifest was created";
        return false;
      }
      const auto current_write_time = std::filesystem::last_write_time(entry.path, metadata_ec);
      if (metadata_ec || current_size != entry.size || current_write_time != entry.write_time) {
        error = "Crash dump changed since the manifest was created";
        return false;
      }
      if (entry.size > std::numeric_limits<uint32_t>::max()) {
        error = "Crash dump too large (over 4 GiB)";
        return false;
      }
      std::ifstream in(entry.path, std::ios::binary);
      if (!in) {
        error = "Failed to open crash dump";
        return false;
      }
      std::string raw;
      raw.resize(static_cast<std::size_t>(entry.size));
      in.read(raw.data(), static_cast<std::streamsize>(raw.size()));
      if (in.gcount() != static_cast<std::streamsize>(raw.size())) {
        error = "Failed to read crash dump";
        return false;
      }
      boost::crc_32_type crc;
      crc.process_bytes(raw.data(), raw.size());
      uint32_t checksum = crc.checksum();
      std::string compressed;
      bool use_deflate = deflate_buffer(raw.data(), raw.size(), compressed) && compressed.size() < raw.size();
      const std::string &payload = use_deflate ? compressed : raw;
      uint16_t method = use_deflate ? 8 : 0;
      uint32_t comp_size = static_cast<uint32_t>(payload.size());
      uint32_t uncomp_size = static_cast<uint32_t>(raw.size());
      uint16_t dostime = 0, dosdate = 0;
      to_dos_datetime(file_time_to_system_clock(entry.write_time), dostime, dosdate);
      if (!add_entry(entry.name, method, checksum, comp_size, uncomp_size, dostime, dosdate, payload)) {
        return false;
      }
    }

    auto cd_start_pos = out.tellp();
    if (cd_start_pos < 0 || static_cast<unsigned long long>(cd_start_pos) > std::numeric_limits<uint32_t>::max()) {
      error = "ZIP central directory offset overflow";
      return false;
    }
    uint32_t cd_start = static_cast<uint32_t>(cd_start_pos);

    for (const auto &e : cd) {
      write_le32(out, 0x02014b50u);
      write_le16(out, 20);
      write_le16(out, 20);
      write_le16(out, 0);
      write_le16(out, e.method);
      write_le16(out, e.dostime);
      write_le16(out, e.dosdate);
      write_le32(out, e.crc);
      write_le32(out, e.comp_size);
      write_le32(out, e.uncomp_size);
      write_le16(out, static_cast<uint16_t>(e.name.size()));
      write_le16(out, 0);
      write_le16(out, 0);
      write_le16(out, 0);
      write_le16(out, 0);
      write_le32(out, 0);
      write_le32(out, e.offset);
      out.write(e.name.data(), e.name.size());
      if (!out) {
        error = "Failed writing ZIP central directory";
        return false;
      }
    }

    auto cd_end_pos = out.tellp();
    if (cd_end_pos < 0 || static_cast<unsigned long long>(cd_end_pos) > std::numeric_limits<uint32_t>::max()) {
      error = "ZIP central directory size overflow";
      return false;
    }
    uint32_t cd_size = static_cast<uint32_t>(cd_end_pos - cd_start_pos);

    write_le32(out, 0x06054b50u);
    write_le16(out, 0);
    write_le16(out, 0);
    write_le16(out, static_cast<uint16_t>(cd.size()));
    write_le16(out, static_cast<uint16_t>(cd.size()));
    write_le32(out, cd_size);
    write_le32(out, cd_start);
    write_le16(out, 0);
    out.flush();
    if (!out.good()) {
      error = "Failed finalizing crash bundle";
      return false;
    }
    return true;
  }

  void getCrashDumpStatus(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    try {
      auto dumps = find_recent_crash_dumps(std::chrono::hours(24 * 7));
      nlohmann::json out;
      if (dumps.empty()) {
        out["available"] = false;
        out["dismissed"] = false;
      } else {
        const auto &info = dumps.front();
        auto captured = file_time_to_system_clock(info.write_time);
        std::string captured_iso = to_iso8601(captured);
        out["available"] = true;
        out["process"] = info.process;
        out["path"] = info.path.string();
        out["filename"] = info.path.filename().string();
        out["size_bytes"] = info.size;
        out["captured_at"] = captured_iso;
        auto age = std::chrono::system_clock::now() - captured;
        out["age_seconds"] = std::chrono::duration_cast<std::chrono::seconds>(age).count();
        out["age_hours"] = std::chrono::duration_cast<std::chrono::hours>(age).count();
        if (auto dismissal = load_crash_dismissal_state()) {
          bool matches = dismissal->filename == info.path.filename().string();
          out["dismissed"] = matches;
          if (matches && !dismissal->dismissed_at.empty()) {
            out["dismissed_at"] = dismissal->dismissed_at;
          }
          if (matches && dismissal->captured_at.empty()) {
            CrashDismissalState updated = *dismissal;
            updated.captured_at = captured_iso;
            save_crash_dismissal_state(updated);
          }
        } else {
          out["dismissed"] = false;
        }
      }
      send_response(response, out);
    } catch (const std::exception &e) {
      bad_request(response, request, e.what());
    }
  }

  void postCrashDumpDismiss(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    try {
      const std::string body = request->content.string();
      if (body.empty()) {
        bad_request(response, request, "Missing request body");
        return;
      }
      auto payload = nlohmann::json::parse(body, nullptr, true, true);
      std::string filename = payload.value("filename", "");
      std::string captured_at = payload.value("captured_at", "");
      if (filename.empty()) {
        bad_request(response, request, "Missing filename");
        return;
      }
      auto dumps = find_recent_crash_dumps(std::chrono::hours(24 * 7));
      if (dumps.empty()) {
        bad_request(response, request, "No recent crash dumps found (within last 7 days)");
        return;
      }
      const auto &info = dumps.front();
      std::string current_iso = to_iso8601(file_time_to_system_clock(info.write_time));
      if (filename != info.path.filename().string()) {
        bad_request(response, request, "Crash dump metadata mismatch");
        return;
      }
      CrashDismissalState state;
      state.filename = std::move(filename);
      state.captured_at = captured_at.empty() ? current_iso : std::move(captured_at);
      state.dismissed_at = to_iso8601(std::chrono::system_clock::now());
      if (!save_crash_dismissal_state(state)) {
        bad_request(response, request, "Failed to persist crash dismissal");
        return;
      }
      nlohmann::json out;
      out["status"] = true;
      out["dismissed_at"] = state.dismissed_at;
      send_response(response, out);
    } catch (const std::exception &e) {
      bad_request(response, request, e.what());
    }
  }

  void getCrashBundleManifest(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    try {
      auto snapshot = create_crash_bundle_snapshot();
      if (snapshot->dumps.empty()) {
        bad_request(response, request, "No recent crash dumps found (within last 7 days)");
        return;
      }
      if (!crash_bundle_plan_has_dump(*snapshot)) {
        bad_request(response, request, "Recent crash dumps exceed the 30 MiB crash-bundle limit");
        return;
      }
      nlohmann::json out;
      out["snapshot"] = snapshot->id;
      out["parts"] = nlohmann::json::array();
      for (std::size_t i = 0; i < snapshot->plan.size(); ++i) {
        nlohmann::json part;
        part["index"] = static_cast<int>(i + 1);
        part["filename"] = snapshot->plan[i].filename;
        part["estimated_size_bytes"] = snapshot->plan[i].estimated_size;
        out["parts"].push_back(part);
      }
      send_response(response, out);
    } catch (const std::exception &e) {
      bad_request(response, request, e.what());
    }
  }

  void downloadCrashBundle(resp_https_t response, req_https_t request) {
    if (!authenticate(response, request)) {
      return;
    }
    print_req(request);
    try {
      std::size_t part_index = 1;
      auto query = request->parse_query_string();
      if (const auto it = query.find("part"); it != query.end()) {
        try {
          part_index = static_cast<std::size_t>(std::stoul(it->second));
        } catch (...) {
          bad_request(response, request, "Invalid crash bundle part index");
          return;
        }
        if (part_index == 0) {
          bad_request(response, request, "Invalid crash bundle part index");
          return;
        }
      }

      std::shared_ptr<const CrashBundleSnapshot> snapshot;
      if (const auto snapshot_param = query.find("snapshot"); snapshot_param != query.end() && !snapshot_param->second.empty()) {
        snapshot = find_crash_bundle_snapshot(snapshot_param->second);
        if (!snapshot) {
          conflict(response, "Crash bundle snapshot expired. Please request a new crash-bundle manifest.");
          return;
        }
      } else {
        // Preserve the legacy direct-download endpoint used by older Web UI
        // bundles and bookmarked links. New clients always send a snapshot id.
        snapshot = create_crash_bundle_snapshot();
      }
      if (snapshot->dumps.empty()) {
        bad_request(response, request, "No recent crash dumps found (within last 7 days)");
        return;
      }
      if (!crash_bundle_plan_has_dump(*snapshot)) {
        bad_request(response, request, "Recent crash dumps exceed the 30 MiB crash-bundle limit");
        return;
      }
      if (part_index > snapshot->plan.size()) {
        bad_request(response, request, "Invalid crash bundle part index");
        return;
      }
      const auto &selected = snapshot->plan[part_index - 1];
      const std::vector<ZipDataEntry> empty_entries;
      const auto &data_entries = selected.include_logs ? snapshot->logs : empty_entries;

      wchar_t tmpDir[MAX_PATH] = {};
      wchar_t tmpFile[MAX_PATH] = {};
      if (GetTempPathW(_countof(tmpDir), tmpDir) == 0) {
        bad_request(response, request, "Failed to resolve temporary directory");
        return;
      }
      if (GetTempFileNameW(tmpDir, L"SNC", 0, tmpFile) == 0) {
        bad_request(response, request, "Failed to create temporary file");
        return;
      }
      std::filesystem::path bundle_path(tmpFile);
      std::string error;
      if (!write_zip_bundle_to_path(bundle_path, data_entries, selected.files, error)) {
        std::error_code ec {};
        std::filesystem::remove(bundle_path, ec);
        if (error.empty()) {
          error = "Failed to build crash bundle";
        }
        bad_request(response, request, error);
        return;
      }

      std::ifstream in(bundle_path, std::ios::binary);
      if (!in) {
        std::error_code ec {};
        std::filesystem::remove(bundle_path, ec);
        bad_request(response, request, "Failed to open crash bundle");
        return;
      }

      const std::string &fname = selected.filename.empty() ? crash_bundle_base_name() + ".zip" : selected.filename;

      SimpleWeb::CaseInsensitiveMultimap headers;
      headers.emplace("Content-Type", "application/zip");
      headers.emplace("Content-Disposition", std::string("attachment; filename=\"") + fname + "\"");
      headers.emplace("X-Frame-Options", "DENY");
      headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
      response->write(SimpleWeb::StatusCode::success_ok, in, headers);
      in.close();
      std::error_code ec {};
      std::filesystem::remove(bundle_path, ec);
    } catch (const std::exception &e) {
      bad_request(response, request, e.what());
    }
  }

}  // namespace confighttp

#endif  // _WIN32