Skip to content

Commit 33cbce4

Browse files
committed
feat(new): add backend public asset support
2 parents 2af59c7 + 5517536 commit 33cbce4

6 files changed

Lines changed: 326 additions & 5 deletions

File tree

src/commands/DoctorCommand.cpp

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1250,13 +1250,90 @@ namespace vix::commands
12501250
}
12511251
}
12521252

1253+
struct PublicAssetsInfo
1254+
{
1255+
fs::path publicDir{};
1256+
bool exists{false};
1257+
bool isDirectory{false};
1258+
bool indexHtmlExists{false};
1259+
bool appCssExists{false};
1260+
bool appJsExists{false};
1261+
bool statusHtmlExists{false};
1262+
bool statusCssExists{false};
1263+
bool statusJsExists{false};
1264+
std::vector<std::string> missingFiles{};
1265+
};
1266+
1267+
PublicAssetsInfo detect_public_assets()
1268+
{
1269+
PublicAssetsInfo info;
1270+
info.publicDir = fs::current_path() / "public";
1271+
1272+
std::error_code ec;
1273+
1274+
info.exists = fs::exists(info.publicDir, ec);
1275+
info.isDirectory = info.exists && fs::is_directory(info.publicDir, ec);
1276+
1277+
if (!info.isDirectory)
1278+
{
1279+
info.missingFiles.push_back("public/");
1280+
return info;
1281+
}
1282+
1283+
const fs::path indexHtml = info.publicDir / "index.html";
1284+
const fs::path appCss = info.publicDir / "app.css";
1285+
const fs::path appJs = info.publicDir / "app.js";
1286+
const fs::path statusHtml = info.publicDir / "status.html";
1287+
const fs::path statusCss = info.publicDir / "status.css";
1288+
const fs::path statusJs = info.publicDir / "status.js";
1289+
1290+
info.indexHtmlExists =
1291+
fs::exists(indexHtml, ec) && fs::is_regular_file(indexHtml, ec);
1292+
1293+
info.appCssExists =
1294+
fs::exists(appCss, ec) && fs::is_regular_file(appCss, ec);
1295+
1296+
info.appJsExists =
1297+
fs::exists(appJs, ec) && fs::is_regular_file(appJs, ec);
1298+
1299+
info.statusHtmlExists =
1300+
fs::exists(statusHtml, ec) && fs::is_regular_file(statusHtml, ec);
1301+
1302+
info.statusCssExists =
1303+
fs::exists(statusCss, ec) && fs::is_regular_file(statusCss, ec);
1304+
1305+
info.statusJsExists =
1306+
fs::exists(statusJs, ec) && fs::is_regular_file(statusJs, ec);
1307+
1308+
if (!info.indexHtmlExists)
1309+
info.missingFiles.push_back("public/index.html");
1310+
1311+
if (!info.appCssExists)
1312+
info.missingFiles.push_back("public/app.css");
1313+
1314+
if (!info.appJsExists)
1315+
info.missingFiles.push_back("public/app.js");
1316+
1317+
if (!info.statusHtmlExists)
1318+
info.missingFiles.push_back("public/status.html");
1319+
1320+
if (!info.statusCssExists)
1321+
info.missingFiles.push_back("public/status.css");
1322+
1323+
if (!info.statusJsExists)
1324+
info.missingFiles.push_back("public/status.js");
1325+
1326+
return info;
1327+
}
1328+
12531329
int run_production_doctor(bool jsonOut)
12541330
{
12551331
vix::cli::util::section(std::cout, "Production Doctor");
12561332

12571333
const auto projectName = read_project_name().value_or("unknown");
12581334
const auto buildDir = detect_build_dir();
12591335
const auto binary = detect_binary_path(projectName);
1336+
const auto publicAssets = detect_public_assets();
12601337

12611338
const auto service = detect_systemd_service(
12621339
projectName,
@@ -1408,7 +1485,61 @@ namespace vix::commands
14081485
vix::cli::util::kv(std::cout, "TLS", tls ? "enabled" : "unknown");
14091486
vix::cli::util::kv(std::cout, "Local health", localHealth ? "ok" : "unknown");
14101487
vix::cli::util::kv(std::cout, "Public health", publicHealth ? "ok" : "unknown");
1411-
vix::cli::util::section(std::cout, "Production Readiness");
1488+
vix::cli::util::section(std::cout, "Public Assets");
1489+
1490+
vix::cli::util::kv(
1491+
std::cout,
1492+
"Public assets",
1493+
publicAssets.isDirectory ? "detected" : "missing");
1494+
1495+
vix::cli::util::kv(
1496+
std::cout,
1497+
"Public directory",
1498+
publicAssets.publicDir.string());
1499+
1500+
vix::cli::util::kv(
1501+
std::cout,
1502+
"index.html",
1503+
publicAssets.indexHtmlExists ? "ok" : "missing");
1504+
1505+
vix::cli::util::kv(
1506+
std::cout,
1507+
"app.css",
1508+
publicAssets.appCssExists ? "ok" : "missing");
1509+
1510+
vix::cli::util::kv(
1511+
std::cout,
1512+
"app.js",
1513+
publicAssets.appJsExists ? "ok" : "missing");
1514+
1515+
vix::cli::util::kv(
1516+
std::cout,
1517+
"status.html",
1518+
publicAssets.statusHtmlExists ? "ok" : "missing");
1519+
1520+
vix::cli::util::kv(
1521+
std::cout,
1522+
"status.css",
1523+
publicAssets.statusCssExists ? "ok" : "missing");
1524+
1525+
vix::cli::util::kv(
1526+
std::cout,
1527+
"status.js",
1528+
publicAssets.statusJsExists ? "ok" : "missing");
1529+
1530+
if (!publicAssets.missingFiles.empty())
1531+
{
1532+
for (const auto &file : publicAssets.missingFiles)
1533+
{
1534+
vix::cli::util::warn_line(
1535+
std::cerr,
1536+
"Missing public asset: " + file);
1537+
}
1538+
1539+
vix::cli::util::warn_line(
1540+
std::cerr,
1541+
"Fix: create the missing files in public/ or remove static UI expectations from this app.");
1542+
}
14121543

14131544
vix::cli::util::kv(
14141545
std::cout,
@@ -1483,6 +1614,15 @@ namespace vix::commands
14831614
out["environment"] = environment.value_or("");
14841615
out["websocket_port"] = websocketPort.value_or("");
14851616
out["proxy_target"] = proxyTarget.value_or("");
1617+
out["public_assets_detected"] = publicAssets.isDirectory;
1618+
out["public_directory"] = publicAssets.publicDir.string();
1619+
out["public_index_html"] = publicAssets.indexHtmlExists;
1620+
out["public_app_css"] = publicAssets.appCssExists;
1621+
out["public_app_js"] = publicAssets.appJsExists;
1622+
out["public_status_html"] = publicAssets.statusHtmlExists;
1623+
out["public_status_css"] = publicAssets.statusCssExists;
1624+
out["public_status_js"] = publicAssets.statusJsExists;
1625+
out["public_missing_files"] = publicAssets.missingFiles;
14861626

14871627
for (const auto &item : readiness)
14881628
{

src/commands/new/NewGenerator.cpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,134 @@ namespace vix::commands::new_cmd::generator
514514
err))
515515
return false;
516516

517+
if (!write_text_file(publicDir / "status.html",
518+
"<!doctype html>\n"
519+
"<html lang=\"en\">\n"
520+
" <head>\n"
521+
" <meta charset=\"utf-8\" />\n"
522+
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
523+
" <title>" +
524+
projName +
525+
" status</title>\n"
526+
" <link rel=\"stylesheet\" href=\"/status.css\" />\n"
527+
" </head>\n"
528+
" <body>\n"
529+
" <main class=\"status-page\">\n"
530+
" <section class=\"status-card\">\n"
531+
" <p class=\"eyebrow\">Vix backend status</p>\n"
532+
" <h1>" +
533+
projName +
534+
"</h1>\n"
535+
" <p class=\"status-line\" id=\"status-line\">Checking service status...</p>\n"
536+
" <div class=\"status-grid\">\n"
537+
" <a href=\"/\">Home</a>\n"
538+
" <a href=\"/health\">Health</a>\n"
539+
" <a href=\"/api/health\">API Health</a>\n"
540+
" </div>\n"
541+
" </section>\n"
542+
" </main>\n"
543+
" <script src=\"/status.js\"></script>\n"
544+
" </body>\n"
545+
"</html>\n",
546+
err))
547+
return false;
548+
549+
if (!write_text_file(publicDir / "status.css",
550+
":root {\n"
551+
" color-scheme: light dark;\n"
552+
" font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n"
553+
" background: #0b0e14;\n"
554+
" color: #f7f7f8;\n"
555+
"}\n"
556+
"\n"
557+
"* {\n"
558+
" box-sizing: border-box;\n"
559+
"}\n"
560+
"\n"
561+
"body {\n"
562+
" margin: 0;\n"
563+
"}\n"
564+
"\n"
565+
".status-page {\n"
566+
" min-height: 100vh;\n"
567+
" display: grid;\n"
568+
" place-items: center;\n"
569+
" padding: 24px;\n"
570+
"}\n"
571+
"\n"
572+
".status-card {\n"
573+
" width: min(760px, 100%);\n"
574+
" padding: 40px;\n"
575+
" border: 1px solid rgba(255, 255, 255, 0.12);\n"
576+
" border-radius: 24px;\n"
577+
" background: rgba(255, 255, 255, 0.06);\n"
578+
" box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);\n"
579+
"}\n"
580+
"\n"
581+
".eyebrow {\n"
582+
" margin: 0 0 12px;\n"
583+
" color: #ff9900;\n"
584+
" font-weight: 700;\n"
585+
" letter-spacing: 0.08em;\n"
586+
" text-transform: uppercase;\n"
587+
"}\n"
588+
"\n"
589+
"h1 {\n"
590+
" margin: 0;\n"
591+
" font-size: clamp(2.2rem, 8vw, 5rem);\n"
592+
" line-height: 1;\n"
593+
"}\n"
594+
"\n"
595+
".status-line {\n"
596+
" margin-top: 18px;\n"
597+
" font-size: 1.05rem;\n"
598+
" line-height: 1.7;\n"
599+
" color: rgba(255, 255, 255, 0.78);\n"
600+
"}\n"
601+
"\n"
602+
".status-grid {\n"
603+
" display: flex;\n"
604+
" gap: 12px;\n"
605+
" flex-wrap: wrap;\n"
606+
" margin-top: 24px;\n"
607+
"}\n"
608+
"\n"
609+
".status-grid a {\n"
610+
" color: #0b0e14;\n"
611+
" background: #ff9900;\n"
612+
" padding: 10px 16px;\n"
613+
" border-radius: 999px;\n"
614+
" text-decoration: none;\n"
615+
" font-weight: 700;\n"
616+
"}\n",
617+
err))
618+
return false;
619+
620+
if (!write_text_file(publicDir / "status.js",
621+
"async function refreshStatus() {\n"
622+
" const line = document.getElementById(\"status-line\");\n"
623+
" if (!line) return;\n"
624+
"\n"
625+
" try {\n"
626+
" const response = await fetch(\"/api/health\", {\n"
627+
" headers: { \"Accept\": \"application/json\" }\n"
628+
" });\n"
629+
"\n"
630+
" if (!response.ok) {\n"
631+
" line.textContent = \"Service responded with HTTP \" + response.status;\n"
632+
" return;\n"
633+
" }\n"
634+
"\n"
635+
" line.textContent = \"Service is healthy.\";\n"
636+
" } catch (error) {\n"
637+
" line.textContent = \"Service health check failed.\";\n"
638+
" }\n"
639+
"}\n"
640+
"\n"
641+
"refreshStatus();\n",
642+
err))
643+
return false;
644+
517645
if (!write_text_file(applicationDir / ".gitkeep", "", err))
518646
return false;
519647
if (!write_text_file(domainDir / ".gitkeep", "", err))

src/commands/new/templates/backend/BackendBootstrapTemplates.cpp

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,10 @@ namespace vix::commands::new_cmd::templates
119119
s += "#include <" + projectName + "/presentation/routes/RouteRegistry.hpp>\n\n";
120120

121121
s += "#include <vix.hpp>\n";
122-
s += "#include <vix/log.hpp>\n\n";
122+
s += "#include <vix/log.hpp>\n";
123+
s += "#include <vix/middleware/app/adapter.hpp>\n";
124+
s += "#include <vix/middleware/performance/compression.hpp>\n";
125+
s += "#include <vix/middleware/performance/static_compression.hpp>\n\n";
123126

124127
s += "namespace " + projectName + "::app\n";
125128
s += "{\n";
@@ -128,8 +131,41 @@ namespace vix::commands::new_cmd::templates
128131
s += " vix::config::Config cfg{\".env\"};\n";
129132
s += " vix::App app;\n\n";
130133

131-
s += " app.templates(\"views\");\n";
132-
s += " app.static_dir(\"public\", \"/\");\n\n";
134+
s += " const std::string viewsPath = cfg.getString(\"templates.path\", \"views\");\n";
135+
s += " const std::string publicPath = cfg.getString(\"public.path\", \"public\");\n";
136+
s += " const std::string publicMount = cfg.getString(\"public.mount\", \"/\");\n";
137+
s += " const std::string publicIndex = cfg.getString(\"public.index\", \"index.html\");\n";
138+
s += " const std::string publicCacheControl = cfg.getString(\"public.cache_control\", \"public, max-age=3600\");\n";
139+
s += " const bool publicSpaFallback = cfg.getBool(\"public.spa_fallback\", false);\n";
140+
s += " const bool publicCompression = cfg.getBool(\"public.compression\", false);\n";
141+
s += " const int publicCompressionMinSize = cfg.getInt(\"public.compression_min_size\", 1024);\n\n";
142+
143+
s += " if (publicCompression)\n";
144+
s += " {\n";
145+
s += " const auto compressionOptions = vix::middleware::performance::CompressionOptions{\n";
146+
s += " .min_size = static_cast<std::size_t>(publicCompressionMinSize),\n";
147+
s += " .add_vary = true,\n";
148+
s += " .enabled = true,\n";
149+
s += " };\n\n";
150+
151+
s += " auto compressionMiddleware = vix::middleware::app::adapt_ctx(\n";
152+
s += " vix::middleware::performance::compression(compressionOptions));\n\n";
153+
154+
s += " app.use(std::move(compressionMiddleware));\n\n";
155+
156+
s += " vix::App::set_static_response_hook(\n";
157+
s += " vix::middleware::performance::compressed_static_response_hook(compressionOptions));\n";
158+
s += " }\n\n";
159+
160+
s += " app.templates(viewsPath);\n";
161+
s += " app.static_dir(\n";
162+
s += " publicPath,\n";
163+
s += " publicMount,\n";
164+
s += " publicIndex,\n";
165+
s += " true,\n";
166+
s += " publicCacheControl,\n";
167+
s += " true,\n";
168+
s += " publicSpaFallback);\n\n";
133169

134170
s += " presentation::middleware::MiddlewareRegistry::register_all(app);\n";
135171
s += " presentation::routes::RouteRegistry::register_all(app);\n\n";

src/commands/new/templates/backend/BackendConfigTemplates.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ namespace vix::commands::new_cmd::templates
4444
s += " \"path\": \"public\",\n";
4545
s += " \"mount\": \"/\",\n";
4646
s += " \"index\": \"index.html\",\n";
47+
s += " \"cache_control\": \"public, max-age=3600\",\n";
4748
s += " \"spa_fallback\": false\n";
4849
s += " },\n";
4950
s += " \"templates\": {\n";
@@ -110,10 +111,15 @@ namespace vix::commands::new_cmd::templates
110111
s += "LOGGING_QUEUE_MAX=20000\n";
111112
s += "LOGGING_DROP_ON_OVERFLOW=true\n\n";
112113

113-
s += "# ----------------------------------\n";
114114
s += "# Public files and templates\n";
115115
s += "# ----------------------------------\n";
116116
s += "PUBLIC_PATH=public\n";
117+
s += "PUBLIC_MOUNT=/\n";
118+
s += "PUBLIC_INDEX=index.html\n";
119+
s += "PUBLIC_CACHE_CONTROL=public, max-age=3600\n";
120+
s += "PUBLIC_SPA_FALLBACK=false\n";
121+
s += "PUBLIC_COMPRESSION=false\n";
122+
s += "PUBLIC_COMPRESSION_MIN_SIZE=1024\n";
117123
s += "VIEWS_PATH=views\n\n";
118124

119125
s += "# ----------------------------------\n";

src/commands/new/templates/backend/BackendManifestTemplates.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ namespace vix::commands::new_cmd::templates
218218
s += " \"APP_NAME\",\n";
219219
s += " \"APP_ENV\",\n";
220220
s += " \"SERVER_PORT\",\n";
221+
s += " \"PUBLIC_PATH\",\n";
222+
s += " \"PUBLIC_MOUNT\",\n";
223+
s += " \"PUBLIC_INDEX\",\n";
224+
s += " \"PUBLIC_CACHE_CONTROL\",\n";
225+
s += " \"PUBLIC_SPA_FALLBACK\",\n";
226+
s += " \"PUBLIC_COMPRESSION\",\n";
227+
s += " \"PUBLIC_COMPRESSION_MIN_SIZE\",\n";
221228
s += " \"DATABASE_ENGINE\",\n";
222229
s += " \"DATABASE_SQLITE_PATH\"\n";
223230
s += " ]\n";

0 commit comments

Comments
 (0)