diff --git a/docker-compose/.env.template b/docker-compose/.env.template index c960f2c..8ad75e6 100644 --- a/docker-compose/.env.template +++ b/docker-compose/.env.template @@ -1,10 +1,17 @@ # .env.template - Template of environment variables for docker-compose -# Copy this file to `.env.production` or `.env.staging` and fill the secret values. +# Copy this file to `.env` (local), `.env.staging`, or `.env.production` and fill the secret values. # Do NOT commit your `.env.*` files containing real secrets. # Common ASPNETCORE_ENVIRONMENT=Development +# Web (myai-web container) - maps to web appsettings Site:Mode section +# Controls the public site experience: +# Normal - full site (default) +# UnderConstruction - redirect visitors to /under-construction.html +# Unavailable - redirect visitors to /site-unavailable.html (HTTP 503) +Site__Mode=Normal + # API (main) ASPNETCORE_URLS=http://+:8080 APP_ENVIRONMENT_NAME=myai-Development diff --git a/docker-compose/docker-compose.production.yml b/docker-compose/docker-compose.production.yml index 66fa390..4144943 100644 --- a/docker-compose/docker-compose.production.yml +++ b/docker-compose/docker-compose.production.yml @@ -260,6 +260,9 @@ services: - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Production} - ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080} - APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.production} + + # Site: matches web appsettings Site section (Normal, UnderConstruction, Unavailable) + - Site__Mode=${Site__Mode:-Normal} networks: - myai-network restart: unless-stopped diff --git a/docker-compose/docker-compose.staging.yml b/docker-compose/docker-compose.staging.yml index 10901cc..da7ae99 100644 --- a/docker-compose/docker-compose.staging.yml +++ b/docker-compose/docker-compose.staging.yml @@ -260,6 +260,9 @@ services: - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging} - ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080} - APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging} + + # Site: matches web appsettings Site section (Normal, UnderConstruction, Unavailable) + - Site__Mode=${Site__Mode:-Normal} networks: - myai-network restart: unless-stopped diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index ad088ef..4d8a526 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -289,6 +289,9 @@ services: - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Development} - ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080} - APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.local} + + # Site: matches web appsettings Site section (Normal, UnderConstruction, Unavailable) + - Site__Mode=${Site__Mode:-Normal} networks: - myai-network restart: unless-stopped diff --git a/web/Middleware/SiteModeMiddleware.cs b/web/Middleware/SiteModeMiddleware.cs new file mode 100644 index 0000000..fa3f621 --- /dev/null +++ b/web/Middleware/SiteModeMiddleware.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.Options; +using Web.Settings; + +namespace Web.Middleware; + +public sealed class SiteModeMiddleware(RequestDelegate next, IOptions options) +{ + private static readonly string[] StaticAssetPrefixes = + [ + "/css/", + "/js/", + "/img/", + "/logo/", + "/fonts/" + ]; + + public async Task InvokeAsync(HttpContext context) + { + var mode = ResolveMode(options.Value.Mode); + if (mode == SiteModes.Normal) + { + await next(context); + return; + } + + var path = context.Request.Path.Value ?? "/"; + var statusPath = GetStatusPath(mode); + + if (IsAllowedPath(path, statusPath)) + { + if (mode == SiteModes.Unavailable && + path.Equals("/site-unavailable.html", StringComparison.OrdinalIgnoreCase)) + { + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + context.Response.Headers.RetryAfter = "3600"; + } + + await next(context); + return; + } + + context.Response.Redirect(statusPath); + } + + private static string ResolveMode(string? configuredMode) + { + if (string.Equals(configuredMode, SiteModes.UnderConstruction, StringComparison.OrdinalIgnoreCase)) + return SiteModes.UnderConstruction; + + if (string.Equals(configuredMode, SiteModes.Unavailable, StringComparison.OrdinalIgnoreCase)) + return SiteModes.Unavailable; + + return SiteModes.Normal; + } + + private static string GetStatusPath(string mode) => + mode == SiteModes.UnderConstruction + ? "/under-construction.html" + : "/site-unavailable.html"; + + private static bool IsAllowedPath(string path, string statusPath) + { + if (path.Equals(statusPath, StringComparison.OrdinalIgnoreCase)) + return true; + + if (path.Equals("/favicon.ico", StringComparison.OrdinalIgnoreCase)) + return true; + + return StaticAssetPrefixes.Any(prefix => + path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/web/Program.cs b/web/Program.cs index 9eaded4..c14b1dd 100644 --- a/web/Program.cs +++ b/web/Program.cs @@ -1,13 +1,19 @@ // Program.cs +using Web.Middleware; +using Web.Settings; + var builder = WebApplication.CreateBuilder(args); builder.Services.AddRouting(); +builder.Services.Configure(builder.Configuration.GetSection(SiteSettings.SectionName)); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); var app = builder.Build(); +app.UseMiddleware(); + // Static site app.UseDefaultFiles(); app.UseStaticFiles(); diff --git a/web/Settings/SiteSettings.cs b/web/Settings/SiteSettings.cs new file mode 100644 index 0000000..08a182c --- /dev/null +++ b/web/Settings/SiteSettings.cs @@ -0,0 +1,18 @@ +namespace Web.Settings; + +public sealed class SiteSettings +{ + public const string SectionName = "Site"; + + /// + /// Controls which page visitors see: Normal, UnderConstruction, or Unavailable. + /// + public string Mode { get; set; } = SiteModes.Normal; +} + +public static class SiteModes +{ + public const string Normal = "Normal"; + public const string UnderConstruction = "UnderConstruction"; + public const string Unavailable = "Unavailable"; +} diff --git a/web/appsettings.Development.json b/web/appsettings.Development.json index 0cc5ff9..31749e8 100644 --- a/web/appsettings.Development.json +++ b/web/appsettings.Development.json @@ -5,6 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "Site": { + "Mode": "Normal" + }, "ReverseProxy": { "Routes": { "apiRoute": { diff --git a/web/appsettings.json b/web/appsettings.json index d11d0bd..9c9beb7 100644 --- a/web/appsettings.json +++ b/web/appsettings.json @@ -7,6 +7,10 @@ }, "AllowedHosts": "*", + "Site": { + "Mode": "Normal" + }, + "ReverseProxy": { "Routes": { "apiRoute": { diff --git a/web/wwwroot/css/myai.css b/web/wwwroot/css/myai.css index 85f5877..1f9b56b 100644 --- a/web/wwwroot/css/myai.css +++ b/web/wwwroot/css/myai.css @@ -48,6 +48,33 @@ img { overflow: hidden } +.status-hero { + min-height: 100vh; + display: flex; + align-items: center; + padding: 48px 0 +} + +.status-card { + max-width: 720px; + margin: 0 auto; + padding: 40px; + border-radius: var(--card-radius); + background: var(--panel); + border: 1px solid var(--panel-border); + box-shadow: var(--shadow) +} + +.status-brand { + margin-bottom: 28px +} + +.status-note { + margin: 24px 0 0; + color: var(--muted); + line-height: 1.6 +} + .header { position: sticky; top: 0; diff --git a/web/wwwroot/site-unavailable.html b/web/wwwroot/site-unavailable.html new file mode 100644 index 0000000..5d97f57 --- /dev/null +++ b/web/wwwroot/site-unavailable.html @@ -0,0 +1,46 @@ + + + + + + MyAi.ro · Temporarily unavailable + + + + + + + + + + +
+
+
+
+
+ + + MyAi.ro + + + MyAi.ro + AI engineering showcase + + + Temporarily unavailable +

The site is not available right now.

+

+ MyAi.ro is temporarily offline for maintenance. + We expect to be back shortly. +

+

+ RO: Site-ul nu este disponibil temporar. Vă mulțumim pentru răbdare. +

+
+
+
+
+
+ + diff --git a/web/wwwroot/under-construction.html b/web/wwwroot/under-construction.html new file mode 100644 index 0000000..921fa73 --- /dev/null +++ b/web/wwwroot/under-construction.html @@ -0,0 +1,46 @@ + + + + + + MyAi.ro · Under construction + + + + + + + + + + +
+
+
+
+
+ + + MyAi.ro + + + MyAi.ro + AI engineering showcase + + + Under construction +

We are building something new.

+

+ MyAi.ro is being updated with new AI demos and improvements. + Please check back soon. +

+

+ RO: Site-ul este în construcție. Reveniți în curând. +

+
+
+
+
+
+ +