diff --git a/src/routes/pages 2.cpp b/src/routes/pages 2.cpp deleted file mode 100644 index 3656103..0000000 --- a/src/routes/pages 2.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "crow.h" -#include "routes.h" -#include -#include - -namespace routes { - -namespace { - -std::string read_file(const std::string& path) { - std::ifstream ifs(path); - if (!ifs.is_open()) return ""; - std::ostringstream ss; - ss << ifs.rdbuf(); - return ss.str(); -} - -} // anonymous namespace - -void register_pages(crow::SimpleApp& app) { - - // Home page — serves static HTML - CROW_ROUTE(app, "/") - ([]() { - std::string html = read_file("static/index.html"); - if (html.empty()) { - return crow::response(500, "index.html not found"); - } - auto resp = crow::response(html); - resp.add_header("Content-Type", "text/html; charset=utf-8"); - return resp; - }); - - // Static file serving for CSS, JS, images - CROW_ROUTE(app, "/static/") - ([](const std::string& path) { - std::string full_path = "static/" + path; - std::string content = read_file(full_path); - if (content.empty()) { - return crow::response(404); - } - - auto resp = crow::response(content); - - // Set content type based on extension - if (path.ends_with(".css")) resp.add_header("Content-Type", "text/css"); - else if (path.ends_with(".js")) resp.add_header("Content-Type", "application/javascript"); - else if (path.ends_with(".html")) resp.add_header("Content-Type", "text/html"); - else if (path.ends_with(".json")) resp.add_header("Content-Type", "application/json"); - else if (path.ends_with(".png")) resp.add_header("Content-Type", "image/png"); - else if (path.ends_with(".svg")) resp.add_header("Content-Type", "image/svg+xml"); - else resp.add_header("Content-Type", "application/octet-stream"); - - return resp; - }); -} - -} // namespace routes diff --git a/src/routes/proxy 2.cpp b/src/routes/proxy 2.cpp deleted file mode 100644 index 800d062..0000000 --- a/src/routes/proxy 2.cpp +++ /dev/null @@ -1,254 +0,0 @@ -// Reverse-proxy routes for the 4 sidecar services. Uses cpp-httplib as the -// upstream client (single-header, no extra system packages). -// -// Upstream URLs are read from env at startup; missing envs make that prefix -// return 503 so the SPA can render a graceful "service unavailable" state. - -#define CPPHTTPLIB_OPENSSL_SUPPORT 0 -#include - -#include "crow.h" -#include "routes.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace routes { - -namespace { - -struct Upstream { - std::string scheme; // "http" or "https" - std::string host; - int port = 0; -}; - -std::optional parse_upstream(const std::string& url) { - // Expect: http(s)://host[:port] - if (url.empty()) return std::nullopt; - Upstream up; - std::string rest; - if (url.rfind("https://", 0) == 0) { - up.scheme = "https"; - rest = url.substr(8); - up.port = 443; - } else if (url.rfind("http://", 0) == 0) { - up.scheme = "http"; - rest = url.substr(7); - up.port = 80; - } else { - return std::nullopt; - } - auto slash = rest.find('/'); - std::string hostport = slash == std::string::npos ? rest : rest.substr(0, slash); - auto colon = hostport.find(':'); - if (colon == std::string::npos) { - up.host = hostport; - } else { - up.host = hostport.substr(0, colon); - try { up.port = std::stoi(hostport.substr(colon + 1)); } catch (...) {} - } - if (up.host.empty()) return std::nullopt; - return up; -} - -std::optional env_upstream(const char* var) { - const char* v = std::getenv(var); - if (!v || !*v) return std::nullopt; - return parse_upstream(std::string(v)); -} - -const std::unordered_set& hop_by_hop() { - static const std::unordered_set h = { - "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", - "te", "trailers", "transfer-encoding", "upgrade", "host", - "content-length", - }; - return h; -} - -std::string to_lower(std::string s) { - for (auto& c : s) c = static_cast(::tolower(static_cast(c))); - return s; -} - -crow::response service_unavailable(const std::string& name) { - crow::json::wvalue j; - j["error"] = "upstream_unavailable"; - j["service"] = name; - j["hint"] = "set CXAI_" + name + "_UPSTREAM env var (e.g. http://cxai-" + to_lower(name) + ":8101)"; - auto resp = crow::response(503, j); - resp.add_header("Content-Type", "application/json"); - return resp; -} - -// Strip the "/api/" prefix and return the remainder (always starts with '/'). -std::string strip_prefix(const std::string& url, const std::string& prefix) { - if (url.size() < prefix.size() || url.compare(0, prefix.size(), prefix) != 0) { - return "/"; - } - std::string rest = url.substr(prefix.size()); - if (rest.empty() || rest.front() != '/') rest = "/" + rest; - return rest; -} - -// Build an httplib::Client per request (cheap; keeps things thread-safe). -httplib::Client make_client(const Upstream& up) { - httplib::Client cli(up.host, up.port); - cli.set_connection_timeout(5, 0); - cli.set_read_timeout(120, 0); - cli.set_write_timeout(30, 0); - return cli; -} - -httplib::Headers forward_headers(const crow::request& req) { - httplib::Headers h; - for (const auto& kv : req.headers) { - std::string k = to_lower(kv.first); - if (hop_by_hop().count(k)) continue; - h.emplace(kv.first, kv.second); - } - return h; -} - -crow::response from_httplib(const httplib::Result& r, const std::string& service) { - if (!r) { - crow::json::wvalue j; - j["error"] = "upstream_error"; - j["service"] = service; - j["detail"] = httplib::to_string(r.error()); - auto resp = crow::response(502, j); - resp.add_header("Content-Type", "application/json"); - return resp; - } - crow::response out(r->status, r->body); - for (const auto& kv : r->headers) { - std::string k = to_lower(kv.first); - if (hop_by_hop().count(k)) continue; - out.add_header(kv.first, kv.second); - } - return out; -} - -crow::response forward(const Upstream& up, const std::string& service, - const crow::request& req, const std::string& path) { - auto cli = make_client(up); - auto headers = forward_headers(req); - std::string upstream_path = path; - if (!req.url_params.get(nullptr)) { - // url_params has no direct stringify; reconstruct from raw url if present. - } - // Crow stores the query string inside `req.raw_url`; append it. - auto qpos = req.raw_url.find('?'); - if (qpos != std::string::npos) { - upstream_path += req.raw_url.substr(qpos); - } - - auto content_type_it = req.headers.find("Content-Type"); - std::string content_type = content_type_it != req.headers.end() - ? content_type_it->second - : std::string("application/octet-stream"); - - switch (req.method) { - case crow::HTTPMethod::GET: - return from_httplib(cli.Get(upstream_path.c_str(), headers), service); - case crow::HTTPMethod::DELETE: - return from_httplib(cli.Delete(upstream_path.c_str(), headers), service); - case crow::HTTPMethod::HEAD: - return from_httplib(cli.Head(upstream_path.c_str(), headers), service); - case crow::HTTPMethod::POST: - return from_httplib( - cli.Post(upstream_path.c_str(), headers, req.body, content_type.c_str()), - service); - case crow::HTTPMethod::PUT: - return from_httplib( - cli.Put(upstream_path.c_str(), headers, req.body, content_type.c_str()), - service); - case crow::HTTPMethod::PATCH: - return from_httplib( - cli.Patch(upstream_path.c_str(), headers, req.body, content_type.c_str()), - service); - default: - return crow::response(405, "method not allowed via proxy"); - } -} - -struct ProxyMount { - std::string service; // "diffusion" / "demand" / "lang" / "slack" - std::string env_var; // "CXAI_DIFFUSION_UPSTREAM" / ... - std::string prefix; // "/api/diffusion" - std::optional upstream; -}; - -std::vector& mounts() { - static std::vector m = { - {"diffusion", "CXAI_DIFFUSION_UPSTREAM", "/api/diffusion", std::nullopt}, - {"demand", "CXAI_DEMAND_UPSTREAM", "/api/demand", std::nullopt}, - {"lang", "CXAI_LANG_UPSTREAM", "/api/lang", std::nullopt}, - {"slack", "CXAI_SLACK_UPSTREAM", "/api/slack", std::nullopt}, - }; - static std::once_flag once; - std::call_once(once, [] { - for (auto& mt : m) mt.upstream = env_upstream(mt.env_var.c_str()); - }); - return m; -} - -void install_one(crow::SimpleApp& app, ProxyMount& mount) { - // Crow `` matches multiple segments; we register one catch-all per - // mount, plus an exact-prefix variant for the bare `/api/` URL. - static const std::vector methods = { - crow::HTTPMethod::GET, crow::HTTPMethod::POST, crow::HTTPMethod::PUT, - crow::HTTPMethod::DELETE, crow::HTTPMethod::PATCH, crow::HTTPMethod::HEAD, - }; - - auto handler = [&mount](const crow::request& req) -> crow::response { - if (!mount.upstream) return service_unavailable(mount.service); - std::string path = strip_prefix(req.url, mount.prefix); - return forward(*mount.upstream, mount.service, req, path); - }; - - // Sub-paths: /api//<...> - app.route_dynamic(mount.prefix + "/") - .methods(methods.data(), methods.size())( - [handler](const crow::request& req, const std::string&) { - return handler(req); - }); - // Bare prefix (no trailing path) - app.route_dynamic(mount.prefix) - .methods(methods.data(), methods.size())(handler); -} - -} // anonymous namespace - -void register_proxy(crow::SimpleApp& app) { - // Health-summary endpoint, lists configured proxies. - CROW_ROUTE(app, "/api/services") - ([]() { - crow::json::wvalue r; - std::vector arr; - for (const auto& m : mounts()) { - crow::json::wvalue item; - item["service"] = m.service; - item["prefix"] = m.prefix; - item["configured"] = static_cast(m.upstream); - if (m.upstream) { - item["upstream_host"] = m.upstream->host; - item["upstream_port"] = m.upstream->port; - item["upstream_scheme"] = m.upstream->scheme; - } - arr.push_back(std::move(item)); - } - r["services"] = std::move(arr); - return r; - }); - - for (auto& m : mounts()) install_one(app, m); -} - -} // namespace routes diff --git a/src/routes/proxy.cpp b/src/routes/proxy.cpp index 696d3ab..baf29c3 100644 --- a/src/routes/proxy.cpp +++ b/src/routes/proxy.cpp @@ -33,6 +33,7 @@ struct Upstream { std::string scheme; std::string host; int port = 0; + std::string path_prefix; // e.g. "/rest/v1" — appended before forwarded path. }; std::optional parse_upstream(const std::string& url) { @@ -52,6 +53,11 @@ std::optional parse_upstream(const std::string& url) { } auto slash = rest.find('/'); std::string hostport = slash == std::string::npos ? rest : rest.substr(0, slash); + if (slash != std::string::npos) { + up.path_prefix = rest.substr(slash); + // Trim trailing slash so concatenation stays clean. + while (up.path_prefix.size() > 1 && up.path_prefix.back() == '/') up.path_prefix.pop_back(); + } auto colon = hostport.find(':'); if (colon == std::string::npos) { up.host = hostport; @@ -127,7 +133,7 @@ crow::response forward(const Upstream& up, const std::string& service, cli.set_write_timeout(30, 0); auto headers = forward_headers(req); - std::string upstream_path = "/" + tail; + std::string upstream_path = up.path_prefix + "/" + tail; auto qpos = req.raw_url.find('?'); if (qpos != std::string::npos) { upstream_path += req.raw_url.substr(qpos); @@ -166,6 +172,8 @@ struct UpstreamSet { std::optional demand; std::optional lang; std::optional slack; + std::optional files; + std::string files_anon_key; }; const UpstreamSet& upstreams() { static UpstreamSet s; @@ -175,10 +183,58 @@ const UpstreamSet& upstreams() { s.demand = env_upstream("CXAI_DEMAND_UPSTREAM"); s.lang = env_upstream("CXAI_LANG_UPSTREAM"); s.slack = env_upstream("CXAI_SLACK_UPSTREAM"); + s.files = env_upstream("CXAI_FILES_UPSTREAM"); + if (const char* k = std::getenv("FILES_ANON_KEY")) s.files_anon_key = k; + else if (const char* k = std::getenv("CXAI_FILES_ANON_KEY")) s.files_anon_key = k; }); return s; } +// Files-specific forwarder: injects PostgREST auth headers from FILES_ANON_KEY. +crow::response forward_files(const Upstream& up, const crow::request& req, + const std::string& tail) { + httplib::Client cli(up.host, up.port); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(120, 0); + cli.set_write_timeout(30, 0); + auto headers = forward_headers(req); + const auto& key = upstreams().files_anon_key; + if (!key.empty()) { + // PostgREST expects both "apikey" and a bearer token. + headers.erase("apikey"); + headers.erase("Authorization"); + headers.emplace("apikey", key); + headers.emplace("Authorization", std::string("Bearer ") + key); + } + std::string upstream_path = up.path_prefix + "/" + tail; + auto qpos = req.raw_url.find('?'); + if (qpos != std::string::npos) { + upstream_path += req.raw_url.substr(qpos); + } + std::string content_type = "application/json"; + auto it = req.headers.find("Content-Type"); + if (it != req.headers.end()) content_type = it->second; + + switch (req.method) { + case crow::HTTPMethod::Get: + return from_httplib(cli.Get(upstream_path.c_str(), headers), "files"); + case crow::HTTPMethod::Post: + return from_httplib( + cli.Post(upstream_path.c_str(), headers, req.body, content_type.c_str()), + "files"); + case crow::HTTPMethod::Patch: + return from_httplib( + cli.Patch(upstream_path.c_str(), headers, req.body, content_type.c_str()), + "files"); + case crow::HTTPMethod::Delete: + return from_httplib(cli.Delete(upstream_path.c_str(), headers), "files"); + case crow::HTTPMethod::Head: + return from_httplib(cli.Head(upstream_path.c_str(), headers), "files"); + default: + return crow::response(405, "method not allowed via files proxy"); + } +} + #define PROXY_PREFIX(APP, PREFIX, NAME, UPGETTER) \ CROW_ROUTE((APP), PREFIX).methods( \ crow::HTTPMethod::Get, crow::HTTPMethod::Post, crow::HTTPMethod::Put, \ @@ -227,6 +283,7 @@ void register_proxy(crow::SimpleApp& app) { arr.push_back(add("demand", upstreams().demand, "/api/demand")); arr.push_back(add("lang", upstreams().lang, "/api/lang")); arr.push_back(add("slack", upstreams().slack, "/api/slack")); + arr.push_back(add("files", upstreams().files, "/api/files")); r["services"] = std::move(arr); return r; }); @@ -235,6 +292,24 @@ void register_proxy(crow::SimpleApp& app) { PROXY_PREFIX(app, "/api/demand", "demand", up_demand) PROXY_PREFIX(app, "/api/lang", "lang", up_lang) PROXY_PREFIX(app, "/api/slack", "slack", up_slack) + + // Files: dedicated routes that inject PostgREST auth headers. + CROW_ROUTE(app, "/api/files").methods( + crow::HTTPMethod::Get, crow::HTTPMethod::Post, crow::HTTPMethod::Patch, + crow::HTTPMethod::Delete, crow::HTTPMethod::Head) + ([](const crow::request& req) { + const auto& up = upstreams().files; + if (!up) return service_unavailable("files"); + return forward_files(*up, req, ""); + }); + CROW_ROUTE(app, "/api/files/").methods( + crow::HTTPMethod::Get, crow::HTTPMethod::Post, crow::HTTPMethod::Patch, + crow::HTTPMethod::Delete, crow::HTTPMethod::Head) + ([](const crow::request& req, std::string tail) { + const auto& up = upstreams().files; + if (!up) return service_unavailable("files"); + return forward_files(*up, req, tail); + }); } } // namespace routes diff --git a/src/routes/system.cpp b/src/routes/system.cpp index 4918803..4081b5a 100644 --- a/src/routes/system.cpp +++ b/src/routes/system.cpp @@ -3,10 +3,19 @@ #include #include #include +#include +#include #include +#include #include +#include #include +#if defined(__APPLE__) +# include +# include +#endif + namespace routes { namespace { @@ -21,6 +30,63 @@ long long boot_epoch() { return boot; } +// Per-process resident memory in bytes, best-effort across Linux/macOS. +long long rss_bytes() { +#if defined(__APPLE__) + mach_task_basic_info info{}; + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, + reinterpret_cast(&info), &count) == KERN_SUCCESS) { + return static_cast(info.resident_size); + } + return 0; +#else + std::ifstream f("/proc/self/status"); + std::string line; + while (std::getline(f, line)) { + if (line.rfind("VmRSS:", 0) == 0) { + std::istringstream ss(line.substr(6)); + long long kb = 0; + ss >> kb; + return kb * 1024; + } + } + return 0; +#endif +} + +// 1/5/15-minute load average (returns 0s on platforms without getloadavg). +void load_avg(double out[3]) { + out[0] = out[1] = out[2] = 0.0; +#if defined(_SC_NPROCESSORS_ONLN) || defined(__APPLE__) || defined(__linux__) + double a[3] = {0, 0, 0}; + if (getloadavg(a, 3) == 3) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; } +#endif +} + +long long total_memory_bytes() { +#if defined(__APPLE__) + int mib[2] = {CTL_HW, HW_MEMSIZE}; + int64_t mem = 0; + size_t len = sizeof(mem); + if (sysctl(mib, 2, &mem, &len, nullptr, 0) == 0) { + return static_cast(mem); + } + return 0; +#else + std::ifstream f("/proc/meminfo"); + std::string line; + while (std::getline(f, line)) { + if (line.rfind("MemTotal:", 0) == 0) { + std::istringstream ss(line.substr(9)); + long long kb = 0; ss >> kb; + return kb * 1024; + } + } + return 0; +#endif +} + } // anonymous namespace void register_system(crow::SimpleApp& app) { @@ -54,6 +120,25 @@ void register_system(crow::SimpleApp& app) { r["pid"] = static_cast(::getpid()); r["uptime_seconds"] = static_cast(std::time(nullptr) - boot_epoch()); r["cxx_standard"] = static_cast(__cplusplus); + r["cpu_count"] = static_cast(std::thread::hardware_concurrency()); + + long long rss = rss_bytes(); + long long total = total_memory_bytes(); + r["rss_bytes"] = static_cast(rss); + r["mem_total"] = static_cast(total); + if (total > 0) r["mem_used_pct"] = static_cast(rss) / static_cast(total) * 100.0; + + double la[3]; load_avg(la); + crow::json::wvalue::list lvec; + lvec.emplace_back(la[0]); lvec.emplace_back(la[1]); lvec.emplace_back(la[2]); + r["loadavg"] = std::move(lvec); + + struct rusage ru{}; + if (getrusage(RUSAGE_SELF, &ru) == 0) { + r["user_cpu_s"] = static_cast(ru.ru_utime.tv_sec) + ru.ru_utime.tv_usec / 1e6; + r["system_cpu_s"] = static_cast(ru.ru_stime.tv_sec) + ru.ru_stime.tv_usec / 1e6; + r["max_rss_kb"] = static_cast(ru.ru_maxrss); + } return r; }); diff --git a/static/css/app.css b/static/css/app.css index 4ac2f5e..962d763 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -500,3 +500,258 @@ html:not(.dark) .console { background: #0d1530; color: #d4f0c9; } .tool-card + .tool-card { margin-top: 8px; } .tool-card h4 { margin: 0; font-size: 13.5px; font-weight: 600; font-family: var(--font-mono); } .tool-card p { margin: 4px 0 0; color: var(--fg-muted); font-size: 12.5px; } + +.ml-2 { margin-left: 8px; } + +/* tab bar (Files / Platform pane) */ +.tabs { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; } +.tab { + background: transparent; + border: 1px solid transparent; + border-radius: var(--r-sm); + padding: 6px 12px; + font-size: 13px; + color: var(--fg-muted); + cursor: pointer; + font-weight: 500; +} +.tab:hover { background: var(--bg-hover); color: var(--fg); } +.tab.active { + background: var(--bg-active); + color: var(--fg); + border-color: var(--border); +} + +/* tables (used by Files pane) */ +.table { width: 100%; border-collapse: collapse; font-size: 13px; } +.table th, .table td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border); } +.table th { font-weight: 600; color: var(--fg-muted); background: var(--bg-active); } +.table tbody tr:hover { background: var(--bg-hover); } + +/* ---------- meters / gauges ---------- */ +.meter { + position: relative; + height: 8px; + background: var(--bg-active); + border-radius: 999px; + overflow: hidden; +} +.meter > span { + display: block; + height: 100%; + background: var(--brand-grad); + border-radius: 999px; + transition: width var(--t) ease; +} +.meter.ok > span { background: linear-gradient(90deg, #16a34a, #4ade80); } +.meter.warn > span { background: linear-gradient(90deg, #d97706, #fbbf24); } +.meter.err > span { background: linear-gradient(90deg, #be123c, #f43f5e); } + +.gauge { + display: grid; + grid-template-columns: 110px 1fr 70px; + align-items: center; + gap: 12px; + padding: 8px 0; + font-size: 12.5px; +} +.gauge .gname { color: var(--fg-muted); font-weight: 500; } +.gauge .gval { text-align: right; font-family: var(--font-mono); color: var(--fg); } + +/* ---------- segmented control ---------- */ +.seg { + display: inline-flex; + background: var(--bg-active); + padding: 3px; + border-radius: var(--r-sm); + gap: 2px; +} +.seg > button { + border: 0; + background: transparent; + color: var(--fg-muted); + padding: 4px 10px; + border-radius: 5px; + font: inherit; + font-size: 12.5px; + cursor: pointer; + transition: background var(--t-fast), color var(--t-fast); +} +.seg > button:hover { color: var(--fg); } +.seg > button.active { + background: var(--bg-elev); + color: var(--fg); + box-shadow: var(--shadow-1); +} + +/* ---------- kbd ---------- */ +kbd { + display: inline-block; + padding: 1px 6px; + font-family: var(--font-mono); + font-size: 11px; + background: var(--bg-active); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 4px; + box-shadow: 0 1px 0 var(--border-2); +} + +/* ---------- code / copy ---------- */ +pre.code { + position: relative; + background: var(--bg-elev-2); + border: 1px solid var(--border); + font-size: 12px; + line-height: 1.55; +} +.copy-btn { + position: absolute; + top: 6px; right: 6px; + width: 26px; height: 26px; + display: grid; place-items: center; + background: var(--bg); + border: 1px solid var(--border); + color: var(--fg-muted); + border-radius: var(--r-sm); + cursor: pointer; + opacity: 0; + transition: opacity var(--t-fast); +} +.code-wrap { position: relative; } +.code-wrap:hover .copy-btn { opacity: 1; } +.copy-btn:hover { background: var(--bg-hover); color: var(--fg); } +.copy-btn svg { width: 13px; height: 13px; } + +/* ---------- sparkline / chart ---------- */ +.spark { + width: 100%; + height: 36px; + display: block; +} +.spark path.l { fill: none; stroke: var(--brand); stroke-width: 1.5; } +.spark path.a { fill: rgb(124 92 255 / .14); stroke: none; } + +/* ---------- chip / select-multi ---------- */ +.chips { display: flex; flex-wrap: wrap; gap: 6px; } +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 10px; + border-radius: 999px; + background: var(--bg-active); + color: var(--fg); + font-size: 12px; + cursor: pointer; + border: 1px solid transparent; + user-select: none; + transition: background var(--t-fast), border-color var(--t-fast); +} +.chip:hover { background: var(--bg-hover); } +.chip.active { background: rgb(124 92 255 / .15); color: var(--brand); border-color: var(--brand); } +.chip .x { color: var(--fg-faint); } + +/* ---------- range slider ---------- */ +input[type="range"].slider { + width: 100%; + -webkit-appearance: none; + appearance: none; + background: transparent; + height: 24px; +} +input[type="range"].slider::-webkit-slider-runnable-track { + height: 4px; background: var(--bg-active); border-radius: 999px; +} +input[type="range"].slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; height: 16px; + border-radius: 50%; + background: var(--brand); + margin-top: -6px; + box-shadow: 0 1px 2px rgb(0 0 0 / .25); + cursor: pointer; +} +input[type="range"].slider::-moz-range-track { + height: 4px; background: var(--bg-active); border-radius: 999px; +} +input[type="range"].slider::-moz-range-thumb { + width: 16px; height: 16px; border: 0; border-radius: 50%; + background: var(--brand); cursor: pointer; +} + +/* ---------- definition list ---------- */ +.dl { display: grid; grid-template-columns: 140px 1fr; gap: 6px 12px; font-size: 13px; } +.dl dt { color: var(--fg-muted); font-weight: 500; } +.dl dd { margin: 0; font-family: var(--font-mono); font-size: 12.5px; word-break: break-word; } + +/* ---------- small bits ---------- */ +.small { font-size: 12px; } +.xsmall { font-size: 11px; } +.text-right { text-align: right; } +.flex-col { display: flex; flex-direction: column; } +.gap-1 { gap: 4px; } +.mt-1 { margin-top: 4px; } +.mb-1 { margin-bottom: 4px; } +.p-0 { padding: 0; } +.p-2 { padding: 8px; } +.no-pad { padding: 0; } +.full-w { width: 100%; } +.scroll-x { overflow-x: auto; } +.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.dim { opacity: .65; } +.divider-vert { width: 1px; background: var(--border); margin: 0 8px; align-self: stretch; } + +/* ---------- link cards ---------- */ +.link-card { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--r); + text-decoration: none; + color: var(--fg); + background: var(--bg); + transition: border-color var(--t-fast), background var(--t-fast), transform var(--t-fast); +} +.link-card:hover { + text-decoration: none; + border-color: var(--brand); + background: var(--bg-hover); + transform: translateY(-1px); +} +.link-card .lc-ic { + width: 32px; height: 32px; + display: grid; place-items: center; + border-radius: 8px; + background: rgb(124 92 255 / .14); + color: var(--brand); + flex-shrink: 0; +} +.link-card .lc-ic svg { width: 16px; height: 16px; } +.link-card .lc-title { font-weight: 600; font-size: 13.5px; } +.link-card .lc-sub { font-size: 12px; color: var(--fg-muted); } + +/* ---------- collapsible / details ---------- */ +details.card-d { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--r); + padding: 0; +} +details.card-d > summary { + padding: 10px 14px; + cursor: pointer; + font-weight: 500; + list-style: none; + display: flex; + align-items: center; + gap: 8px; +} +details.card-d > summary::-webkit-details-marker { display: none; } +details.card-d > summary::before { + content: '▸'; color: var(--fg-faint); transition: transform var(--t-fast); +} +details.card-d[open] > summary::before { transform: rotate(90deg); } +details.card-d > .body { padding: 12px 14px; border-top: 1px solid var(--border); } diff --git a/static/index 2.html b/static/index 2.html deleted file mode 100644 index 4d4811a..0000000 --- a/static/index 2.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - CxWebApp - - - -
-

CxWebApp

-

C++ Web Application

-
- -
-
-

Items

-
Loading…
-
- - - -
-
- -
-

Health

-
Checking…
-
-
- - - - diff --git a/static/index.html b/static/index.html index 470f8fb..36f4091 100644 --- a/static/index.html +++ b/static/index.html @@ -75,6 +75,10 @@ macOS +