From a33ed400f13579f179f29d41fe5a6b3496ecfc8a Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Mon, 2 Jun 2025 23:40:47 +0200 Subject: [PATCH 001/103] Dockerfile fix --- Aberwyn/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Aberwyn/Dockerfile b/Aberwyn/Dockerfile index 142b95b..153fc56 100644 --- a/Aberwyn/Dockerfile +++ b/Aberwyn/Dockerfile @@ -15,7 +15,7 @@ EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src -COPY ["Aberwyn.csproj", "."] +COPY ["Aberwyn/Aberwyn.csproj", "."] RUN dotnet restore "Aberwyn.csproj" COPY . . RUN dotnet build "Aberwyn.csproj" -c Release -o /app/build From 2e1e0f46706eaf1fcad66a283d59582ee32058e9 Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Mon, 2 Jun 2025 23:48:43 +0200 Subject: [PATCH 002/103] Clearing up proj file --- Aberwyn/Aberwyn.csproj | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Aberwyn/Aberwyn.csproj b/Aberwyn/Aberwyn.csproj index 005d93a..099c10b 100644 --- a/Aberwyn/Aberwyn.csproj +++ b/Aberwyn/Aberwyn.csproj @@ -9,16 +9,6 @@ Linux - - - - - - - - - - From 628f25a8be05b9eed9df081cc0f204d4fda54c97 Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Mon, 2 Jun 2025 23:52:17 +0200 Subject: [PATCH 003/103] More --- Aberwyn/Aberwyn.csproj | 5 -- Aberwyn/Data/BudgetService.cs | 148 ---------------------------------- 2 files changed, 153 deletions(-) delete mode 100644 Aberwyn/Data/BudgetService.cs diff --git a/Aberwyn/Aberwyn.csproj b/Aberwyn/Aberwyn.csproj index 099c10b..92a479f 100644 --- a/Aberwyn/Aberwyn.csproj +++ b/Aberwyn/Aberwyn.csproj @@ -9,11 +9,6 @@ Linux - - - - - diff --git a/Aberwyn/Data/BudgetService.cs b/Aberwyn/Data/BudgetService.cs deleted file mode 100644 index 17421f5..0000000 --- a/Aberwyn/Data/BudgetService.cs +++ /dev/null @@ -1,148 +0,0 @@ -using MySql.Data.MySqlClient; -using System.Collections.Generic; -using Aberwyn.Models; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; // Add this namespace - -namespace Aberwyn.Data -{ - public class BudgetService - { - private readonly IConfiguration _configuration; - private readonly IHostEnvironment _env; // Add this field - - public BudgetService(IConfiguration configuration, IHostEnvironment env) // Update constructor - { - _configuration = configuration; - _env = env; // Initialize the environment field - } - - public MySqlConnection GetConnection() - { - var connectionString = _env.IsDevelopment() // Use the injected environment variable - ? _configuration.GetConnectionString("DefaultConnection") - : _configuration.GetConnectionString("ProductionConnection"); - - return new MySqlConnection(connectionString); - } - - public bool UpdateBudgetItem(BudgetItem item) - { - using (var connection = GetConnection()) - { - connection.Open(); - - string query = @" - UPDATE tblBudgetItems - SET Name = @name, Amount = @amount - WHERE idtblBudgetItems = @id"; - - using (var cmd = new MySqlCommand(query, connection)) - { - cmd.Parameters.AddWithValue("@name", item.Name); - cmd.Parameters.AddWithValue("@amount", item.Amount); - cmd.Parameters.AddWithValue("@id", item.ID); - return cmd.ExecuteNonQuery() > 0; // Returns true if one or more rows are updated - } - } - } - - public List GetBudgetItems(int month, int year) - { - var budgetItems = new List(); - - using (var connection = GetConnection()) - { - connection.Open(); - - string query = @" - SELECT - b.idtblBudgetItems AS id, - b.Name AS item_name, - b.Amount AS amount, - c1.Name AS category, - b.Month, - b.Year, - b.Description AS description - FROM tblBudgetItems b - LEFT JOIN tblCategories c1 ON b.Category = c1.idtblCategories - WHERE b.Month = @month AND b.Year = @year"; - - using (var cmd = new MySqlCommand(query, connection)) - { - cmd.Parameters.AddWithValue("@month", month); - cmd.Parameters.AddWithValue("@year", year); - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - budgetItems.Add(new BudgetItem - { - ID = reader.GetInt32("id"), - Name = reader.GetString("item_name"), // Updated alias - Amount = reader.GetDecimal("amount"), - Category = reader.GetString("category"), - Month = reader.GetInt32("Month"), - Year = reader.GetInt32("Year"), - Description = reader.IsDBNull(reader.GetOrdinal("description")) - ? null - : reader.GetString("description") - }); - } - } - } - } - - return budgetItems; - } - - public bool AddBudgetItem(BudgetItem item) - { - using (var connection = GetConnection()) - { - connection.Open(); - - string query = @" - INSERT INTO tblBudgetItems (Name, Amount, Category, Month, Year) - VALUES (@name, @amount, @category, @month, @year)"; - - using (var cmd = new MySqlCommand(query, connection)) - { - cmd.Parameters.AddWithValue("@name", item.Name); - cmd.Parameters.AddWithValue("@amount", item.Amount); - cmd.Parameters.AddWithValue("@category", item.Category); - cmd.Parameters.AddWithValue("@month", item.Month); - cmd.Parameters.AddWithValue("@year", item.Year); - return cmd.ExecuteNonQuery() > 0; // Returns true if a row was inserted - } - } - }*/ - - // New method to fetch all categories - public List GetCategories() - { - var categories = new List(); - - using (var connection = GetConnection()) - { - connection.Open(); - - string query = "SELECT Name FROM tblCategories"; // Adjust based on your table structure - - using (var cmd = new MySqlCommand(query, connection)) - { - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - categories.Add(reader.GetString("Name")); - } - } - } - } - - return categories; - } - } -} From 4c60508d6d8ff395ff52f11f5addbb94568a1d32 Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Tue, 3 Jun 2025 00:04:28 +0200 Subject: [PATCH 004/103] Fixes! --- Aberwyn/.drone.yml | 37 ++++++++++++++++++++++++++++++++++--- Aberwyn/Dockerfile | 5 +++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Aberwyn/.drone.yml b/Aberwyn/.drone.yml index 8fa6545..4fedd7a 100644 --- a/Aberwyn/.drone.yml +++ b/Aberwyn/.drone.yml @@ -4,9 +4,9 @@ name: default steps: - name: build-dotnet - image: mcr.microsoft.com/dotnet/sdk:6.0 + image: alpine commands: - - dotnet publish Aberwyn/Aberwyn.csproj -c Release -o out + - echo "Docker build will handle dotnet publish" - name: build-docker image: plugins/docker @@ -18,6 +18,37 @@ steps: password: from_secret: gitea_token dockerfile: Aberwyn/Dockerfile - context: Aberwyn + context: . tags: - latest + insecure: true + + - name: restart-unraid-container + image: appleboy/drone-ssh + settings: + host: 192.168.1.108 + port: 22 + username: + from_secret: unraid_ssh_user + key: + from_secret: unraid_ssh_private_key + script: + - docker pull 192.168.1.9:3000/tai/aberwyn/aberwyn:latest + - docker stop Aberwyn || true + - docker rm Aberwyn || true + - docker run -d --name='Aberwyn' --net='br0' -e TZ='Europe/Berlin' -p 80:80 192.168.1.9:3000/tai/aberwyn/aberwyn:latest + + - name: notify-result + image: alpine + when: + status: + - success + - failure + commands: + - apk add --no-cache curl + - | + if [ "$DRONE_BUILD_STATUS" = "success" ]; then + curl -X POST http://192.168.1.196:8123/api/webhook/aberwyn_update_success + else + curl -X POST http://192.168.1.196:8123/api/webhook/aberwyn_update_failed + fi diff --git a/Aberwyn/Dockerfile b/Aberwyn/Dockerfile index 153fc56..5872d1e 100644 --- a/Aberwyn/Dockerfile +++ b/Aberwyn/Dockerfile @@ -15,9 +15,10 @@ EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src -COPY ["Aberwyn/Aberwyn.csproj", "."] +COPY ["Aberwyn/Aberwyn.csproj", "Aberwyn/"] +WORKDIR /src/Aberwyn RUN dotnet restore "Aberwyn.csproj" -COPY . . +COPY Aberwyn/. . RUN dotnet build "Aberwyn.csproj" -c Release -o /app/build FROM build AS publish From fd6759e3d02046067f3431853fb95470cdeb7ba8 Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Tue, 3 Jun 2025 00:19:40 +0200 Subject: [PATCH 005/103] Gogogo --- Aberwyn/Infrastructure/docker-compose.prod.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Aberwyn/Infrastructure/docker-compose.prod.yml b/Aberwyn/Infrastructure/docker-compose.prod.yml index d9acc43..0e24ec5 100644 --- a/Aberwyn/Infrastructure/docker-compose.prod.yml +++ b/Aberwyn/Infrastructure/docker-compose.prod.yml @@ -2,22 +2,22 @@ version: '3.8' services: aberwyn-app: - image: aberwyn:latest - build: - context: ../ - dockerfile: Dockerfile + image: 192.168.1.9:3000/tai/aberwyn/aberwyn:latest + container_name: aberwyn-app-prod ports: - "8080:80" environment: ASPNETCORE_ENVIRONMENT: Production + DB_HOST: aberwyn-mysql-prod DB_NAME: aberwyn_prod - + DB_USER: aberwyn + DB_PASSWORD: 3edc4RFV depends_on: - - mysql + - aberwyn-mysql-prod networks: - aberwyn-net - mysql: + aberwyn-mysql-prod: image: mysql:8 container_name: aberwyn-mysql-prod restart: always From 3d6aa2d42457256320f73a5b500366dc64cdc26e Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Tue, 3 Jun 2025 00:26:13 +0200 Subject: [PATCH 006/103] Use old db --- Aberwyn/appsettings.Development.json | 2 +- Aberwyn/appsettings.Production.json | 2 +- Aberwyn/appsettings.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Aberwyn/appsettings.Development.json b/Aberwyn/appsettings.Development.json index c537901..2c041e1 100644 --- a/Aberwyn/appsettings.Development.json +++ b/Aberwyn/appsettings.Development.json @@ -6,7 +6,7 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Server=mysql;Database=aberwyn_dev;User=aberwyn;Password=3edc4RFV;", + "DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;", "ProdConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;" } diff --git a/Aberwyn/appsettings.Production.json b/Aberwyn/appsettings.Production.json index 200eef8..9796e46 100644 --- a/Aberwyn/appsettings.Production.json +++ b/Aberwyn/appsettings.Production.json @@ -1,5 +1,5 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=mysql;Database=aberwyn_prod;User=aberwyn;Password=3edc4RFV;" + "DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;" } } \ No newline at end of file diff --git a/Aberwyn/appsettings.json b/Aberwyn/appsettings.json index 6e172d0..1eaa0ce 100644 --- a/Aberwyn/appsettings.json +++ b/Aberwyn/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Server=mysql;Database=aberwyn_dev;User=aberwyn;Password=3edc4RFV;" + "DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;" }, "VapidKeys": { "Subject": "mailto:e@zcz.se", From 908bc469c6a011a68f8e6927f4ed582500b0dde1 Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Tue, 3 Jun 2025 21:36:16 +0200 Subject: [PATCH 007/103] Setup! --- Aberwyn/Aberwyn.csproj | 9 +- Aberwyn/Controllers/SetupApiController.cs | 50 +++ Aberwyn/Controllers/SetupController.cs | 151 +++++++++ Aberwyn/Data/IdentityDataInitializer.cs | 39 ++- Aberwyn/Data/SetupService.cs | 55 +++ Aberwyn/Infrastructure/setup.json | 11 + Aberwyn/Models/SetupSettings.cs | 17 + Aberwyn/Program.cs | 170 ++++++---- Aberwyn/Properties/launchSettings.json | 10 +- Aberwyn/Views/Setup/Index.cshtml | 395 ++++++++++++++++++++++ Aberwyn/Views/Setup/SetupComplete.cshtml | 17 + Aberwyn/Views/Shared/_Layout.cshtml | 57 ++-- Aberwyn/Views/Shared/_LoginPartial.cshtml | 2 +- Aberwyn/appsettings.json | 3 +- 14 files changed, 877 insertions(+), 109 deletions(-) create mode 100644 Aberwyn/Controllers/SetupApiController.cs create mode 100644 Aberwyn/Controllers/SetupController.cs create mode 100644 Aberwyn/Data/SetupService.cs create mode 100644 Aberwyn/Infrastructure/setup.json create mode 100644 Aberwyn/Models/SetupSettings.cs create mode 100644 Aberwyn/Views/Setup/Index.cshtml create mode 100644 Aberwyn/Views/Setup/SetupComplete.cshtml diff --git a/Aberwyn/Aberwyn.csproj b/Aberwyn/Aberwyn.csproj index 92a479f..cc74855 100644 --- a/Aberwyn/Aberwyn.csproj +++ b/Aberwyn/Aberwyn.csproj @@ -9,6 +9,13 @@ Linux + + + + + + + @@ -20,6 +27,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -42,7 +50,6 @@ - diff --git a/Aberwyn/Controllers/SetupApiController.cs b/Aberwyn/Controllers/SetupApiController.cs new file mode 100644 index 0000000..5240a56 --- /dev/null +++ b/Aberwyn/Controllers/SetupApiController.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using MySql.Data.MySqlClient; + +namespace Aberwyn.Controllers +{ + [ApiController] + [Route("api/setup")] + public class SetupApiController : ControllerBase + { + [HttpPost("testdb")] + public IActionResult TestDbConnection([FromBody] DbTestRequest request) + { + try + { + var baseConnStr = $"server={request.Host};port={request.Port};user={request.User};password={request.Pass};"; + + var testDbName = $"testcheck_{Guid.NewGuid():N}".Substring(0, 12); // säker tillfällig databas + + using (var conn = new MySqlConnection(baseConnStr + "database=information_schema;")) + { + conn.Open(); + + // Försök skapa en temporär databas + var createCmd = new MySqlCommand($"CREATE DATABASE `{testDbName}`", conn); + createCmd.ExecuteNonQuery(); + + // Och radera den direkt + var dropCmd = new MySqlCommand($"DROP DATABASE `{testDbName}`", conn); + dropCmd.ExecuteNonQuery(); + } + + return Ok(new { success = true, message = "Anslutning OK och CREATE DATABASE tillåten." }); + } + catch (Exception ex) + { + return Ok(new { success = false, message = ex.Message }); + } + } + + + public class DbTestRequest + { + public string Host { get; set; } + public string Port { get; set; } + public string Db { get; set; } + public string User { get; set; } + public string Pass { get; set; } + } + } +} diff --git a/Aberwyn/Controllers/SetupController.cs b/Aberwyn/Controllers/SetupController.cs new file mode 100644 index 0000000..e7d7f59 --- /dev/null +++ b/Aberwyn/Controllers/SetupController.cs @@ -0,0 +1,151 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; + +namespace Aberwyn.Controllers +{ + using System.Text.Json; + using System.IO; + using Aberwyn.Models; + using Microsoft.AspNetCore.Mvc.Filters; + using MySql.Data.MySqlClient; + using Aberwyn.Data; + using Microsoft.EntityFrameworkCore; + + [Route("setup")] + public class SetupController : Controller + { + private readonly IWebHostEnvironment _env; + + public override void OnActionExecuting(ActionExecutingContext context) + { + ViewBag.IsSetupMode = true; + base.OnActionExecuting(context); + } + public SetupController(IWebHostEnvironment env) + { + _env = env; + } + + [HttpGet] + public IActionResult Index() + { + return View(new SetupSettings()); + } + + [Authorize(Roles = "Admin")] + [HttpPost("reset")] + public IActionResult Reset() + { + var path = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json"); + + var resetSettings = new SetupSettings + { + IsConfigured = false, + DbHost = "", + DbPort = 3306, + DbName = "", + DbUser = "", + DbPassword = "", + AdminUsername = "admin", + AdminEmail = "admin@localhost", + AdminPassword = "Admin123!" + }; + + var json = JsonSerializer.Serialize(resetSettings, new JsonSerializerOptions { WriteIndented = true }); + System.IO.File.WriteAllText(path, json); + + return RedirectToAction("Index"); + } + + + [HttpPost("")] + public async Task Setup([FromBody] SetupSettings model) + { + if (!ModelState.IsValid) + { + var allErrors = ModelState + .Where(e => e.Value.Errors.Count > 0) + .Select(e => new { Field = e.Key, Errors = e.Value.Errors.Select(x => x.ErrorMessage) }); + + return BadRequest(new { error = "Modellen är ogiltig", details = allErrors }); + } + + try + { + // Skapa databasen om den inte finns + var baseConnStr = $"server={model.DbHost};port={model.DbPort};user={model.DbUser};password={model.DbPassword};"; + using (var conn = new MySqlConnection(baseConnStr + "database=information_schema;")) + { + conn.Open(); + var cmd = new MySqlCommand("SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME = @dbName", conn); + cmd.Parameters.AddWithValue("@dbName", model.DbName); + var exists = cmd.ExecuteScalar(); + + if (exists == null) + { + var createCmd = new MySqlCommand($"CREATE DATABASE `{model.DbName}`", conn); + createCmd.ExecuteNonQuery(); + } + } + + // Bygg services temporärt för att skapa admin + var connectionString = $"server={model.DbHost};port={model.DbPort};database={model.DbName};user={model.DbUser};password={model.DbPassword}"; + var tempProvider = SetupService.BuildTemporaryServices(connectionString); + + using var scope = tempProvider.CreateScope(); + + // Skapa databastabeller via EF + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); + + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roleManager = scope.ServiceProvider.GetRequiredService>(); + + // Skapa roller + string[] roles = { "Admin", "Chef", "Budget" }; + foreach (var role in roles) + { + if (!await roleManager.RoleExistsAsync(role)) + await roleManager.CreateAsync(new IdentityRole(role)); + } + + // Skapa adminanvändare + var adminUser = new ApplicationUser + { + UserName = model.AdminUsername, + Email = model.AdminEmail + }; + + var existingUser = await userManager.FindByNameAsync(model.AdminUsername); + if (existingUser == null) + { + var result = await userManager.CreateAsync(adminUser, model.AdminPassword); + if (!result.Succeeded) + { + return BadRequest(new { error = "Kunde inte skapa administratör", details = result.Errors }); + } + + await userManager.AddToRoleAsync(adminUser, "Admin"); + } + + model.IsConfigured = true; + + // Spara inställningarna + var json = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true }); + var filePath = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json"); + System.IO.File.WriteAllText(filePath, json); + + return Ok(new { message = "Installation slutförd!" }); + } + catch (Exception ex) + { + return BadRequest(new { error = "Fel vid installation", details = ex.Message }); + } + } + + public IActionResult SetupComplete() => View(); + } + + +} diff --git a/Aberwyn/Data/IdentityDataInitializer.cs b/Aberwyn/Data/IdentityDataInitializer.cs index 35be21e..189f483 100644 --- a/Aberwyn/Data/IdentityDataInitializer.cs +++ b/Aberwyn/Data/IdentityDataInitializer.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Aberwyn.Models; @@ -6,10 +7,16 @@ namespace Aberwyn.Data { public static class IdentityDataInitializer { - public static async Task SeedData(IServiceProvider serviceProvider) + public static async Task SeedData(IServiceProvider serviceProvider, SetupSettings? setup = null) { var userManager = serviceProvider.GetRequiredService>(); var roleManager = serviceProvider.GetRequiredService>(); + var config = serviceProvider.GetService(); + + if (setup == null && config != null) + { + setup = config.GetSection("SetupSettings").Get() ?? new SetupSettings(); + } string[] roles = { "Admin", "Chef", "Budget" }; @@ -19,26 +26,38 @@ namespace Aberwyn.Data await roleManager.CreateAsync(new IdentityRole(role)); } - string adminUsername = "admin"; - string adminEmail = "admin@localhost"; - string password = "Admin123!"; - - if (await userManager.FindByEmailAsync(adminEmail) == null) + var existingUser = await userManager.FindByEmailAsync(setup.AdminEmail); + if (existingUser == null) { var user = new ApplicationUser { - UserName = adminUsername, - Email = adminEmail, + UserName = setup.AdminUsername, + Email = setup.AdminEmail, EmailConfirmed = true }; - var result = await userManager.CreateAsync(user, password); + var result = await userManager.CreateAsync(user, setup.AdminPassword); + + if (result.Succeeded) + await userManager.AddToRoleAsync(user, "Admin"); + + return result; + } + else + { + var token = await userManager.GeneratePasswordResetTokenAsync(existingUser); + var result = await userManager.ResetPasswordAsync(existingUser, token, setup.AdminPassword); if (result.Succeeded) { - await userManager.AddToRoleAsync(user, "Admin"); + var rolesForUser = await userManager.GetRolesAsync(existingUser); + if (!rolesForUser.Contains("Admin")) + await userManager.AddToRoleAsync(existingUser, "Admin"); } + + return result; } } + } } diff --git a/Aberwyn/Data/SetupService.cs b/Aberwyn/Data/SetupService.cs new file mode 100644 index 0000000..0166182 --- /dev/null +++ b/Aberwyn/Data/SetupService.cs @@ -0,0 +1,55 @@ +using Aberwyn.Models; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; + +namespace Aberwyn.Data +{ + // SetupService.cs + public class SetupService + { + private readonly IWebHostEnvironment _env; + private readonly string _filePath; + + public SetupService(IWebHostEnvironment env) + { + _env = env; + _filePath = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json"); + } + + + public SetupSettings GetSetup() + { + if (!File.Exists(_filePath)) + return new SetupSettings { IsConfigured = false }; + + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize(json) ?? new SetupSettings { IsConfigured = false }; + } + + + internal static IServiceProvider BuildTemporaryServices(string connectionString) + { + var services = new ServiceCollection(); + + // Konfigurera EF + Identity + services.AddDbContext(options => + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); + + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Lägg till en tom konfiguration för att undvika null + services.AddSingleton(new ConfigurationBuilder().Build()); + + // Valfritt: Lägg till loggning om något kräver det + services.AddLogging(); + + return services.BuildServiceProvider(); + } + + } + +} diff --git a/Aberwyn/Infrastructure/setup.json b/Aberwyn/Infrastructure/setup.json new file mode 100644 index 0000000..f9402c6 --- /dev/null +++ b/Aberwyn/Infrastructure/setup.json @@ -0,0 +1,11 @@ +{ + "AdminUsername": "tai", + "AdminEmail": "tai@zcz.se", + "AdminPassword": "Admin123!", + "IsConfigured": true, + "DbHost": "localhost", + "DbPort": 3306, + "DbName": "aberwyn_test", + "DbUser": "root", + "DbPassword": "rootpass" +} \ No newline at end of file diff --git a/Aberwyn/Models/SetupSettings.cs b/Aberwyn/Models/SetupSettings.cs new file mode 100644 index 0000000..4b1f814 --- /dev/null +++ b/Aberwyn/Models/SetupSettings.cs @@ -0,0 +1,17 @@ +namespace Aberwyn.Models +{ + public class SetupSettings + { + public string AdminUsername { get; set; } = "admin"; + public string AdminEmail { get; set; } = "admin@localhost"; + public string AdminPassword { get; set; } = "Admin123!"; + public bool IsConfigured { get; set; } + public string DbHost { get; set; } + public int DbPort { get; set; } + public string DbName { get; set; } + public string DbUser { get; set; } + public string DbPassword { get; set; } + } + + +} diff --git a/Aberwyn/Program.cs b/Aberwyn/Program.cs index 19d3574..edcccf6 100644 --- a/Aberwyn/Program.cs +++ b/Aberwyn/Program.cs @@ -1,14 +1,12 @@ using Microsoft.EntityFrameworkCore; + using Aberwyn.Data; using System.Text; using System.Globalization; using Microsoft.AspNetCore.Localization; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Aberwyn.Models; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using System.Text.Json; var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -16,7 +14,6 @@ var config = new ConfigurationBuilder() .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true) .AddEnvironmentVariables() .Build(); -Console.WriteLine("📦 DEBUG: laddad raw-connectionstring: " + config.GetConnectionString("DefaultConnection")); var builder = WebApplication.CreateBuilder(new WebApplicationOptions { @@ -24,22 +21,47 @@ var builder = WebApplication.CreateBuilder(new WebApplicationOptions EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production" }); builder.Configuration.AddConfiguration(config); -Console.WriteLine("🌍 Miljö: " + builder.Environment.EnvironmentName); -Console.WriteLine("🔗 Connection string: " + builder.Configuration.GetConnectionString("DefaultConnection")); + +// Läser setup.json +var setupFilePath = Path.Combine("infrastructure", "setup.json"); +SetupSettings setup; +try +{ + if (!File.Exists(setupFilePath)) + throw new Exception("setup.json saknas."); + + using var jsonStream = File.OpenRead(setupFilePath); + setup = JsonSerializer.Deserialize(jsonStream)!; + + if (setup.IsConfigured) + { + if (string.IsNullOrWhiteSpace(setup.DbHost) || + string.IsNullOrWhiteSpace(setup.DbName) || + string.IsNullOrWhiteSpace(setup.DbUser) || + string.IsNullOrWhiteSpace(setup.DbPassword)) + { + throw new Exception("Databasinställningarna är ofullständiga."); + } + } +} +catch (Exception ex) +{ + Console.WriteLine($"❌ Fel vid läsning av setup.json: {ex.Message}"); + setup = new SetupSettings { IsConfigured = false }; +} -// Add services to the container. +// Add services to the container builder.Services.AddControllersWithViews() .AddJsonOptions(opts => { - // Beh�ll propertynamn som i C#-klassen (PascalCase) opts.JsonSerializerOptions.PropertyNamingPolicy = null; - // Ignorera null-v�rden vid serialisering opts.JsonSerializerOptions.IgnoreNullValues = true; }); + builder.Services.AddSession(options => { - options.IdleTimeout = TimeSpan.FromHours(1); + options.IdleTimeout = TimeSpan.FromDays(30); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); @@ -47,24 +69,32 @@ builder.Services.AddSession(options => builder.Services.AddRazorPages(); builder.Services.AddHttpClient(); -// Configure your DbContext with MySQLs -Console.WriteLine("🔗 Connection string: " + builder.Configuration.GetConnectionString("DefaultConnection")); - -builder.Services.AddDbContext(options => - options.UseMySql( - builder.Configuration.GetConnectionString("DefaultConnection"), - new MySqlServerVersion(new Version(8, 0, 36)), - mySqlOptions => mySqlOptions.EnableRetryOnFailure() - ) -); - - -builder.Services.AddIdentity(options => +// Registrera rätt databas och identity beroende på om setup är klar +if (setup.IsConfigured) { - options.SignIn.RequireConfirmedAccount = false; -}) -.AddEntityFrameworkStores() -.AddDefaultTokenProviders(); + string connectionString = $"Server={setup.DbHost};Port={setup.DbPort};Database={setup.DbName};User={setup.DbUser};Password={setup.DbPassword};"; + + builder.Services.AddDbContext(options => + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); + + builder.Services.AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = false; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); +} +else +{ + builder.Services.AddDbContext(options => + options.UseInMemoryDatabase("TempSetup")); + + builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); +} + +// Identity inställningar builder.Services.Configure(options => { options.Password.RequireDigit = false; @@ -74,8 +104,7 @@ builder.Services.Configure(options => options.Password.RequireUppercase = false; }); -builder.Services.AddControllersWithViews(); -// Register your services +// Appens övriga tjänster builder.Services.AddScoped(); builder.Services.AddSingleton(sp => @@ -89,11 +118,14 @@ builder.Services.AddSingleton(sp => }); builder.Services.Configure(builder.Configuration.GetSection("Vapid")); + builder.Services.ConfigureApplicationCookie(options => { - options.LoginPath = "/Identity/Account/Login"; // korrekt för ditt nuvarande upplägg + options.LoginPath = "/Identity/Account/Login"; }); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + builder.Services.Configure(options => { var supportedCultures = new[] { new CultureInfo("sv-SE") }; @@ -101,60 +133,70 @@ builder.Services.Configure(options => options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; }); - -//builder.Configuration -// .SetBasePath(Directory.GetCurrentDirectory()) -// .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true) -// .AddEnvironmentVariables(); -builder.Services.AddSingleton(sp => -{ - var config = sp.GetRequiredService(); - return new PushNotificationService( - config["VapidKeys:Subject"], - config["VapidKeys:PublicKey"], - config["VapidKeys:PrivateKey"] - ); -}); +builder.Services.AddSingleton(); var app = builder.Build(); -// Configure the HTTP request pipeline. +app.UseStaticFiles(); +// Middleware: om ej konfigurerad → redirect till /setup +app.Use(async (context, next) => +{ + var setupService = context.RequestServices.GetRequiredService(); + var currentSetup = setupService.GetSetup(); + + var path = context.Request.Path; + var method = context.Request.Method; + + if (!currentSetup.IsConfigured && + !path.StartsWithSegments("/setup") && + !(path == "/setup" && method == "POST") && // 👈 tillåt POST till /setup + !path.StartsWithSegments("/api/setup")) + { + context.Response.Redirect("/setup"); + return; + } + + await next(); +}); + + + +// Configure the HTTP request pipeline if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } -app.UseStaticFiles(); + app.UseRouting(); app.UseSession(); -app.UseAuthentication();; +app.UseAuthentication(); app.UseAuthorization(); -// Map controller endpoints (including AJAX JSON actions) +// Routing app.MapControllers(); - -// Conventional MVC route app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); - -// Map Razor Pages app.MapRazorPages(); -using (var scope = app.Services.CreateScope()) + +// Init: migrera databas och skapa admin +if (setup.IsConfigured) { + using var scope = app.Services.CreateScope(); var services = scope.ServiceProvider; var context = services.GetRequiredService(); - // Vänta tills databasen är redo + int retries = 10; while (retries > 0) { try { - context.Database.OpenConnection(); // bara testa öppna anslutningen + context.Database.OpenConnection(); context.Database.CloseConnection(); - break; // lyckades! + break; } catch { @@ -164,17 +206,17 @@ using (var scope = app.Services.CreateScope()) } } - // Kör alla EF-migrationer automatiskt context.Database.Migrate(); - // Skapa standardroller och admin - await IdentityDataInitializer.SeedData(services); + var userManager = services.GetRequiredService>(); + var anyUsers = await userManager.Users.AnyAsync(); - // (valfritt) Lägg in exempelbudgetdata - // await TestDataSeeder.SeedBudget(context); + if (!anyUsers) + { + Console.WriteLine("🧩 Ingen användare hittades – skapar admin..."); + await IdentityDataInitializer.SeedData(services, setup); + } } - - app.Run(); diff --git a/Aberwyn/Properties/launchSettings.json b/Aberwyn/Properties/launchSettings.json index 66d0fc0..a663ecc 100644 --- a/Aberwyn/Properties/launchSettings.json +++ b/Aberwyn/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "Aberwyn (Dev)": { + "Aberwyn": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { @@ -16,14 +16,6 @@ }, "applicationUrl": "https://localhost:7290;http://localhost:5290" }, - "Aberwyn (Prod)": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production" - }, - "applicationUrl": "https://localhost:7290;http://localhost:5290" - }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, diff --git a/Aberwyn/Views/Setup/Index.cshtml b/Aberwyn/Views/Setup/Index.cshtml new file mode 100644 index 0000000..6a091aa --- /dev/null +++ b/Aberwyn/Views/Setup/Index.cshtml @@ -0,0 +1,395 @@ +@model Aberwyn.Models.SetupSettings +@{ + ViewData["Title"] = "Installera Aberwyn"; +} + +

Installera Aberwyn

+ +@if (ViewBag.Error != null) +{ +
@ViewBag.Error
+} + + + +
+
+

Steg 1: Databasinställningar

+
+ + +
Fältet är obligatoriskt
+
+
+ + +
Fältet är obligatoriskt
+
+
+ + +
Fältet är obligatoriskt
+
+
+ + +
Fältet är obligatoriskt
+
+
+ + +
Fältet är obligatoriskt
+
+ +
+ +
+ + + + + +
+ + +@if (User.IsInRole("Admin")) +{ +
+ +
+} + +@section Scripts { + + + + + +} diff --git a/Aberwyn/Views/Setup/SetupComplete.cshtml b/Aberwyn/Views/Setup/SetupComplete.cshtml new file mode 100644 index 0000000..220085f --- /dev/null +++ b/Aberwyn/Views/Setup/SetupComplete.cshtml @@ -0,0 +1,17 @@ +@{ + ViewData["Title"] = "Klart!"; +} + +

✅ Aberwyn är nu installerad

+

Du kan nu starta om applikationen för att börja använda den.

+ +Till startsidan + +
+ +

🔄 Återställ installation

+

Om du vill nollställa installationen och börja om från början, klicka här:

+ +
+ +
diff --git a/Aberwyn/Views/Shared/_Layout.cshtml b/Aberwyn/Views/Shared/_Layout.cshtml index 94388ae..e01e070 100644 --- a/Aberwyn/Views/Shared/_Layout.cshtml +++ b/Aberwyn/Views/Shared/_Layout.cshtml @@ -21,35 +21,46 @@ Ludwig - + @if (ViewBag.IsSetupMode as bool? != true) + { + + }
diff --git a/Aberwyn/Views/Shared/_LoginPartial.cshtml b/Aberwyn/Views/Shared/_LoginPartial.cshtml index 1f1c8f2..6987907 100644 --- a/Aberwyn/Views/Shared/_LoginPartial.cshtml +++ b/Aberwyn/Views/Shared/_LoginPartial.cshtml @@ -31,7 +31,7 @@ else
- +
+
From 84c6c45a0b181c7c97fad59aaa78863302364aa7 Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Wed, 4 Jun 2025 11:51:08 +0200 Subject: [PATCH 016/103] Another attempt --- Aberwyn/Controllers/AdminController.cs | 4 ++-- Aberwyn/Data/ApplicationDbContextFactory.cs | 19 ++++++------------- Aberwyn/Data/MenuService.cs | 11 ++++++++++- Aberwyn/Data/SetupService.cs | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/Aberwyn/Controllers/AdminController.cs b/Aberwyn/Controllers/AdminController.cs index bbc109c..95f92da 100644 --- a/Aberwyn/Controllers/AdminController.cs +++ b/Aberwyn/Controllers/AdminController.cs @@ -104,7 +104,7 @@ namespace Aberwyn.Controllers [Authorize(Roles = "Admin")] public IActionResult ImportMealsFromProd() { - var prodService = MenuService.CreateWithConfig(_configuration, _env, useProdDb: true); + var prodService = MenuService.CreateWithSetup(_env); var devService = new MenuService(_context); // injicerad context används var prodMeals = prodService.GetMealsDetailed(); @@ -184,7 +184,7 @@ namespace Aberwyn.Controllers public IActionResult ImportBudgetFromProd() { // Hämta connection till produktion - using var prodContext = ApplicationDbContextFactory.CreateWithConfig(_configuration, _env, useProdDb: true); + using var prodContext = ApplicationDbContextFactory.CreateWithConfig(_env, true); // Importera definitioner först var prodCategoryDefs = prodContext.BudgetCategoryDefinitions.ToList(); diff --git a/Aberwyn/Data/ApplicationDbContextFactory.cs b/Aberwyn/Data/ApplicationDbContextFactory.cs index 284459f..7122120 100644 --- a/Aberwyn/Data/ApplicationDbContextFactory.cs +++ b/Aberwyn/Data/ApplicationDbContextFactory.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using System; using System.IO; using System.Text.Json; +using static Aberwyn.Data.SetupService; namespace Aberwyn.Data { @@ -30,26 +31,18 @@ namespace Aberwyn.Data return new ApplicationDbContext(optionsBuilder.Options); } - public static ApplicationDbContext CreateWithConfig(IConfiguration config, IHostEnvironment env, bool useProdDb = false) + public static ApplicationDbContext CreateWithConfig(IHostEnvironment env, bool useProdDb = false) { - var setup = LoadSetup(); - - var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder - { - Server = setup.DbHost, - Port = (uint)setup.DbPort, - Database = setup.DbName, - UserID = setup.DbUser, - Password = setup.DbPassword, - AllowUserVariables = true - }; + var setup = SetupLoader.Load(env); + var connStr = SetupLoader.GetConnectionString(setup); var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseMySql(csBuilder.ConnectionString, new MySqlServerVersion(new Version(8, 0, 36))); + optionsBuilder.UseMySql(connStr, new MySqlServerVersion(new Version(8, 0, 36))); return new ApplicationDbContext(optionsBuilder.Options); } + private static SetupSettings LoadSetup() { var basePath = Directory.GetCurrentDirectory(); diff --git a/Aberwyn/Data/MenuService.cs b/Aberwyn/Data/MenuService.cs index 334bfd0..dbecfd2 100644 --- a/Aberwyn/Data/MenuService.cs +++ b/Aberwyn/Data/MenuService.cs @@ -2,7 +2,7 @@ using Aberwyn.Models; using Microsoft.EntityFrameworkCore; using System.Globalization; - +using static Aberwyn.Data.SetupService; namespace Aberwyn.Data { @@ -47,6 +47,15 @@ public class MenuService var context = new ApplicationDbContext(builder.Options); return new MenuService(context); } + public static MenuService CreateWithSetup(IHostEnvironment env) + { + var setup = SetupLoader.Load(env); + var connStr = SetupLoader.GetConnectionString(setup); + var builder = new DbContextOptionsBuilder(); + builder.UseMySql(connStr, ServerVersion.AutoDetect(connStr)); + var context = new ApplicationDbContext(builder.Options); + return new MenuService(context); + } public void UpdateWeeklyMenu(MenuViewModel model) { diff --git a/Aberwyn/Data/SetupService.cs b/Aberwyn/Data/SetupService.cs index 0166182..282e4f5 100644 --- a/Aberwyn/Data/SetupService.cs +++ b/Aberwyn/Data/SetupService.cs @@ -3,6 +3,9 @@ using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; +using System.IO; +using System.Text.Json; +using Microsoft.Extensions.Hosting; namespace Aberwyn.Data { @@ -50,6 +53,21 @@ namespace Aberwyn.Data return services.BuildServiceProvider(); } + public static class SetupLoader + { + public static SetupSettings Load(IHostEnvironment env) + { + var path = Path.Combine(env.ContentRootPath, "infrastructure", "setup.json"); + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json)!; + } + + public static string GetConnectionString(SetupSettings setup) + { + return $"server={setup.DbHost};port={setup.DbPort};database={setup.DbName};user={setup.DbUser};password={setup.DbPassword}"; + } + } + } } From c44fbfdca9cb56c40fd9549182f6b202d44f9917 Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Wed, 4 Jun 2025 18:01:05 +0200 Subject: [PATCH 017/103] Woohoo working version gooo --- Aberwyn/Controllers/AdminController.cs | 169 ++++++++++++++++--------- Aberwyn/Controllers/MealController.cs | 5 +- Aberwyn/Data/ApplicationDbContext.cs | 3 + Aberwyn/Data/MenuService.cs | 50 +++----- Aberwyn/Infrastructure/setup.json | 10 +- Aberwyn/Models/MenuViewModel.cs | 1 + Aberwyn/Views/Admin/Index.cshtml | 90 +++++++++++-- 7 files changed, 214 insertions(+), 114 deletions(-) diff --git a/Aberwyn/Controllers/AdminController.cs b/Aberwyn/Controllers/AdminController.cs index 95f92da..f07b575 100644 --- a/Aberwyn/Controllers/AdminController.cs +++ b/Aberwyn/Controllers/AdminController.cs @@ -102,54 +102,20 @@ namespace Aberwyn.Controllers } [HttpPost] [Authorize(Roles = "Admin")] - public IActionResult ImportMealsFromProd() + public IActionResult ImportMenusFromCustom(string dbHost, int dbPort, string dbName, string dbUser, string dbPassword) { - var prodService = MenuService.CreateWithSetup(_env); - var devService = new MenuService(_context); // injicerad context används + var connStr = $"Server={dbHost};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};"; + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseMySql(connStr, ServerVersion.AutoDetect(connStr)); - var prodMeals = prodService.GetMealsDetailed(); - - foreach (var meal in prodMeals) - { - // Kopiera utan ID (för att undvika konflikt) och spara - var newMeal = new Meal - { - Name = meal.Name, - Description = meal.Description, - ProteinType = meal.ProteinType, - Category = meal.Category, - CarbType = meal.CarbType, - RecipeUrl = meal.RecipeUrl, - ImageUrl = meal.ImageUrl, - IsAvailable = meal.IsAvailable, - CreatedAt = meal.CreatedAt, - Instructions = meal.Instructions, - ImageData = meal.ImageData, - ImageMimeType = meal.ImageMimeType, - Ingredients = meal.Ingredients.Select(i => new Ingredient - { - Quantity = i.Quantity, - Item = i.Item - }).ToList() - }; - - devService.SaveOrUpdateMealWithIngredients(newMeal); - } - - return Content("Import klar!"); - } - - [HttpPost] - [Authorize(Roles = "Admin")] - public IActionResult ImportMenusFromProd() - { - var prodService = MenuService.CreateWithConfig(_configuration, _env, useProdDb: true); + using var customContext = new ApplicationDbContext(optionsBuilder.Options); + var sourceService = new MenuService(customContext); var devService = new MenuService(_context); - var allProdMenus = prodService.GetAllWeeklyMenus(); - var allMeals = devService.GetMeals(); + var sourceMenus = sourceService.GetAllWeeklyMenus(); + var devMeals = devService.GetMeals(); - foreach (var menu in allProdMenus) + foreach (var menu in sourceMenus) { var newMenu = new WeeklyMenu { @@ -163,34 +129,92 @@ namespace Aberwyn.Controllers }; if (!string.IsNullOrEmpty(menu.BreakfastMealName)) - newMenu.BreakfastMealId = allMeals.FirstOrDefault(m => m.Name == menu.BreakfastMealName)?.Id; + newMenu.BreakfastMealId = devMeals.FirstOrDefault(m => m.Name == menu.BreakfastMealName)?.Id; if (!string.IsNullOrEmpty(menu.LunchMealName)) - newMenu.LunchMealId = allMeals.FirstOrDefault(m => m.Name == menu.LunchMealName)?.Id; + newMenu.LunchMealId = devMeals.FirstOrDefault(m => m.Name == menu.LunchMealName)?.Id; if (!string.IsNullOrEmpty(menu.DinnerMealName)) - newMenu.DinnerMealId = allMeals.FirstOrDefault(m => m.Name == menu.DinnerMealName)?.Id; + newMenu.DinnerMealId = devMeals.FirstOrDefault(m => m.Name == menu.DinnerMealName)?.Id; _context.WeeklyMenus.Add(newMenu); } _context.SaveChanges(); - TempData["Message"] = "Import av veckomenyer klar."; + TempData["Message"] = $"✅ Import av veckomenyer från extern databas klar ({sourceMenus.Count})."; return RedirectToAction("Index"); } + [HttpPost] [Authorize(Roles = "Admin")] - public IActionResult ImportBudgetFromProd() + public IActionResult ImportMealsFromCustom(string dbHost, int dbPort, string dbName, string dbUser, string dbPassword) { - // Hämta connection till produktion - using var prodContext = ApplicationDbContextFactory.CreateWithConfig(_env, true); + var connStr = $"Server={dbHost};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};"; + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseMySql(connStr, ServerVersion.AutoDetect(connStr), mySqlOptions => mySqlOptions.CommandTimeout(180)); + using var customContext = new ApplicationDbContext(optionsBuilder.Options); - // Importera definitioner först - var prodCategoryDefs = prodContext.BudgetCategoryDefinitions.ToList(); - var prodItemDefs = prodContext.BudgetItemDefinitions.ToList(); + var customService = new MenuService(customContext); + var devService = new MenuService(_context); - foreach (var def in prodCategoryDefs) + try + { + var importedMeals = customService.GetMealsDetailed(); // Ska inkludera Ingredients + foreach (var meal in importedMeals) + { + var newMeal = new Meal + { + Id = meal.Id, // 👈 Viktigt! + Name = meal.Name, + Description = meal.Description, + ProteinType = meal.ProteinType, + Category = meal.Category, + CarbType = meal.CarbType, + RecipeUrl = meal.RecipeUrl, + ImageUrl = meal.ImageUrl, + IsAvailable = meal.IsAvailable, + CreatedAt = meal.CreatedAt, + Instructions = meal.Instructions, + ImageData = meal.ImageData, + ImageMimeType = meal.ImageMimeType, + Ingredients = meal.Ingredients.Select(i => new Ingredient + { + MealId = meal.Id, // 👈 Koppla till rätt måltid + Quantity = i.Quantity, + Item = i.Item + }).ToList() + }; + + devService.SaveOrUpdateMealWithIngredients(newMeal); + } + + TempData["Message"] = $"✅ {importedMeals.Count} måltider importerade från extern databas."; + } + catch (Exception ex) + { + TempData["Message"] = $"❌ Fel vid import: {ex.Message}"; + } + + return RedirectToAction("Index"); + } + + + + [HttpPost] + [Authorize(Roles = "Admin")] + public IActionResult ImportBudgetFromCustom(string dbHost, int dbPort, string dbName, string dbUser, string dbPassword) + { + var connStr = $"Server={dbHost};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};"; + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseMySql(connStr, ServerVersion.AutoDetect(connStr)); + + using var sourceContext = new ApplicationDbContext(optionsBuilder.Options); + + var categoryDefs = sourceContext.BudgetCategoryDefinitions.ToList(); + var itemDefs = sourceContext.BudgetItemDefinitions.ToList(); + + foreach (var def in categoryDefs) { if (!_context.BudgetCategoryDefinitions.Any(d => d.Name == def.Name)) { @@ -199,11 +223,10 @@ namespace Aberwyn.Controllers Name = def.Name, Color = def.Color ?? "#cccccc" }); - } } - foreach (var def in prodItemDefs) + foreach (var def in itemDefs) { if (!_context.BudgetItemDefinitions.Any(d => d.Name == def.Name)) { @@ -211,19 +234,17 @@ namespace Aberwyn.Controllers } } - _context.SaveChanges(); // Se till att ID:n finns för FK:n nedan + _context.SaveChanges(); - // Ladda definitioner i minnet för snabb lookup var devCategoryDefs = _context.BudgetCategoryDefinitions.ToList(); var devItemDefs = _context.BudgetItemDefinitions.ToList(); - // Importera budgetperioder med kategorier och items - var prodPeriods = prodContext.BudgetPeriods + var periods = sourceContext.BudgetPeriods .Include(p => p.Categories) .ThenInclude(c => c.Items) .ToList(); - foreach (var period in prodPeriods) + foreach (var period in periods) { var exists = _context.BudgetPeriods .Any(p => p.Year == period.Year && p.Month == period.Month); @@ -259,11 +280,13 @@ namespace Aberwyn.Controllers } _context.SaveChanges(); - TempData["Message"] = "✅ Import av budgetdata från produktion är klar."; + TempData["Message"] = $"✅ Import av budgetdata från extern databas klar ({periods.Count} månader)."; return RedirectToAction("Index"); } + + //Todo [HttpGet] @@ -314,9 +337,31 @@ namespace Aberwyn.Controllers return Ok(); } + [HttpPost] + [Authorize(Roles = "Admin")] + public IActionResult TestDbConnection(string dbHost, int dbPort, string dbName, string dbUser, string dbPassword) + { + var connStr = $"Server={dbHost};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};"; + try + { + var builder = new DbContextOptionsBuilder(); + builder.UseMySql(connStr, ServerVersion.AutoDetect(connStr)); + + using var context = new ApplicationDbContext(builder.Options); + context.Database.OpenConnection(); + context.Database.CloseConnection(); + + return Json(new { success = true, message = "✅ Anslutning lyckades!" }); + } + catch (Exception ex) + { + return Json(new { success = false, message = $"❌ Anslutning misslyckades: {ex.Message}" }); + } + } } + public class AdminUserViewModel { public string UserId { get; set; } diff --git a/Aberwyn/Controllers/MealController.cs b/Aberwyn/Controllers/MealController.cs index 5e7b194..fb0970b 100644 --- a/Aberwyn/Controllers/MealController.cs +++ b/Aberwyn/Controllers/MealController.cs @@ -10,12 +10,13 @@ namespace Aberwyn.Controllers private readonly IConfiguration _configuration; private readonly IHostEnvironment _env; private readonly MenuService _menuService; - - public MealController(IConfiguration configuration, IHostEnvironment env) + public MealController(MenuService menuService, IConfiguration configuration, IHostEnvironment env) { + _menuService = menuService; _configuration = configuration; _env = env; } + [HttpGet] public IActionResult View(int id, bool edit = false) { diff --git a/Aberwyn/Data/ApplicationDbContext.cs b/Aberwyn/Data/ApplicationDbContext.cs index e5ece4e..d6dd3b9 100644 --- a/Aberwyn/Data/ApplicationDbContext.cs +++ b/Aberwyn/Data/ApplicationDbContext.cs @@ -16,6 +16,9 @@ namespace Aberwyn.Data base.OnModelCreating(builder); builder.Entity().ToTable("WeeklyMenu"); + builder.Entity() + .Property(m => m.Id) + .ValueGeneratedNever(); } diff --git a/Aberwyn/Data/MenuService.cs b/Aberwyn/Data/MenuService.cs index dbecfd2..b95ef19 100644 --- a/Aberwyn/Data/MenuService.cs +++ b/Aberwyn/Data/MenuService.cs @@ -16,37 +16,6 @@ public class MenuService _context = context; } - // Detta är en alternativ konstruktör – används manuellt vid t.ex. import - public static MenuService CreateWithConfig(IConfiguration config, IHostEnvironment env, bool useProdDb = false) - { - var basePath = env.ContentRootPath ?? Directory.GetCurrentDirectory(); - var setupPath = Path.Combine(basePath, "infrastructure", "setup.json"); - - if (!File.Exists(setupPath)) - throw new FileNotFoundException("setup.json saknas i infrastructure/"); - - var setupJson = File.ReadAllText(setupPath); - var setup = System.Text.Json.JsonSerializer.Deserialize(setupJson)!; - - if (!setup.IsConfigured || string.IsNullOrWhiteSpace(setup.DbPassword)) - throw new InvalidOperationException("setup.json är ofullständig."); - - var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder - { - Server = setup.DbHost, - Port = (uint)setup.DbPort, - Database = setup.DbName, - UserID = setup.DbUser, - Password = setup.DbPassword, - AllowUserVariables = true - }; - - var builder = new DbContextOptionsBuilder(); - builder.UseMySql(csBuilder.ConnectionString, ServerVersion.AutoDetect(csBuilder.ConnectionString)); - - var context = new ApplicationDbContext(builder.Options); - return new MenuService(context); - } public static MenuService CreateWithSetup(IHostEnvironment env) { var setup = SetupLoader.Load(env); @@ -83,15 +52,26 @@ public class MenuService meal.Name = meal.Name.Trim(); meal.CreatedAt = meal.CreatedAt == default ? DateTime.Now : meal.CreatedAt; - if (meal.Id == 0) - _context.Meals.Add(meal); + var existing = _context.Meals + .AsNoTracking() + .FirstOrDefault(m => m.Id == meal.Id); + + if (existing == null) + { + // Nytt objekt – försök behålla ID:t från prod + _context.Entry(meal).State = EntityState.Added; + } else + { + // Befintlig – uppdatera _context.Meals.Update(meal); + } _context.SaveChanges(); } -public List GetAllWeeklyMenus() + + public List GetAllWeeklyMenus() { var menus = _context.WeeklyMenus.ToList(); @@ -139,10 +119,12 @@ public List GetAllWeeklyMenus() public List GetMealsDetailed() { return _context.Meals + .Include(m => m.Ingredients) // 🧠 detta behövs! .OrderByDescending(m => m.CreatedAt) .ToList(); } + public Meal GetMealById(int id) { var meal = _context.Meals diff --git a/Aberwyn/Infrastructure/setup.json b/Aberwyn/Infrastructure/setup.json index 48eaad4..eba5681 100644 --- a/Aberwyn/Infrastructure/setup.json +++ b/Aberwyn/Infrastructure/setup.json @@ -2,10 +2,10 @@ "AdminUsername": "admin", "AdminEmail": "admin@localhost", "AdminPassword": "Admin123!", - "IsConfigured": false, - "DbHost": null, + "IsConfigured": true, + "DbHost": "192.168.1.108", "DbPort": 3306, - "DbName": null, - "DbUser": null, - "DbPassword": null + "DbName": "lewel_prod", + "DbUser": "lewel", + "DbPassword": "W542.Hl;)%ta" } \ No newline at end of file diff --git a/Aberwyn/Models/MenuViewModel.cs b/Aberwyn/Models/MenuViewModel.cs index 57ee9d0..7958f49 100644 --- a/Aberwyn/Models/MenuViewModel.cs +++ b/Aberwyn/Models/MenuViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Aberwyn.Data; using Aberwyn.Models; diff --git a/Aberwyn/Views/Admin/Index.cshtml b/Aberwyn/Views/Admin/Index.cshtml index c370eed..f30d4af 100644 --- a/Aberwyn/Views/Admin/Index.cshtml +++ b/Aberwyn/Views/Admin/Index.cshtml @@ -84,20 +84,88 @@ -
-

Importera måltider från produktion

-
- -
-
- -
-
- -
+
+
+ Importera från annan databas +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
+ + + + +

🌐 Du är offline

+

LEWEL är inte tillgänglig just nu. Kontrollera din internetanslutning och försök igen.

+ + diff --git a/Aberwyn/Views/Home/Offline.cshtml.cs b/Aberwyn/Views/Home/Offline.cshtml.cs new file mode 100644 index 0000000..9577482 --- /dev/null +++ b/Aberwyn/Views/Home/Offline.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Aberwyn.Views.Home +{ + public class OfflineModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/Aberwyn/Views/Meal/View.cshtml b/Aberwyn/Views/Meal/View.cshtml index 5f1c96e..8ae2584 100644 --- a/Aberwyn/Views/Meal/View.cshtml +++ b/Aberwyn/Views/Meal/View.cshtml @@ -64,6 +64,10 @@ +
+ + +
@@ -79,7 +83,7 @@
- @for (int i = 0; i < Model.Ingredients.Count; i++) + @for (int i = 0; i < (Model.Ingredients?.Count ?? 0); i++) {
@@ -215,7 +219,7 @@ closeImageModal(); } }); - $(document).ready(function () { + $(document).ready(function () { if ($('#Instructions').length > 0) { $('#Instructions').trumbowyg({ autogrow: true, @@ -223,35 +227,6 @@ }); } }); - if ('serviceWorker' in navigator && 'PushManager' in window) { - navigator.serviceWorker.register('/service-worker.js') - .then(function (registration) { - console.log('Service Worker registered', registration); - return registration.pushManager.getSubscription() - .then(async function (subscription) { - if (subscription) { - console.log('Already subscribed to push notifications.'); - return subscription; - } - const response = await fetch('/api/push/vapid-public-key'); - const vapidPublicKey = await response.text(); - const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); - - return registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: convertedVapidKey - }); - }); - }).then(function (subscription) { - return fetch('/api/push/subscribe', { - method: 'POST', - body: JSON.stringify(subscription), - headers: { - 'Content-Type': 'application/json' - } - }); - }); - } diff --git a/Aberwyn/wwwroot/manifest.json b/Aberwyn/wwwroot/manifest.json index fe813b3..9be370e 100644 --- a/Aberwyn/wwwroot/manifest.json +++ b/Aberwyn/wwwroot/manifest.json @@ -4,7 +4,7 @@ "start_url": "/", "display": "standalone", "background_color": "#1F2C3C", - "theme_color": "#6a0dad", + "theme_color": "#1F2C3C", "icons": [ { "src": "/images/lewel-icon.png", From 57bea7b54c318965c3647ce8ff1ab5106a65ef03 Mon Sep 17 00:00:00 2001 From: Tai Date: Wed, 11 Jun 2025 13:06:08 +0200 Subject: [PATCH 024/103] Meal updates --- Aberwyn/Controllers/MealController.cs | 6 +- Aberwyn/Controllers/MealMenuApiController.cs | 5 +- Aberwyn/Data/MenuService.cs | 30 +++++- Aberwyn/Models/MenuViewModel.cs | 35 ++++-- Aberwyn/Views/Meal/Index.cshtml | 48 +++++++++ Aberwyn/wwwroot/css/meal-gallery.css | 107 +++++++++++++++++++ Aberwyn/wwwroot/images/fallback.jpg | Bin 0 -> 31239 bytes 7 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 Aberwyn/Views/Meal/Index.cshtml create mode 100644 Aberwyn/wwwroot/css/meal-gallery.css create mode 100644 Aberwyn/wwwroot/images/fallback.jpg diff --git a/Aberwyn/Controllers/MealController.cs b/Aberwyn/Controllers/MealController.cs index 9272520..c677423 100644 --- a/Aberwyn/Controllers/MealController.cs +++ b/Aberwyn/Controllers/MealController.cs @@ -18,7 +18,11 @@ namespace Aberwyn.Controllers _configuration = configuration; _env = env; } - + [HttpGet("/meal")] + public IActionResult Index() + { + return View("Index"); + } [HttpGet] public IActionResult View(int? id, bool edit = false) { diff --git a/Aberwyn/Controllers/MealMenuApiController.cs b/Aberwyn/Controllers/MealMenuApiController.cs index 57b7956..02d7d17 100644 --- a/Aberwyn/Controllers/MealMenuApiController.cs +++ b/Aberwyn/Controllers/MealMenuApiController.cs @@ -30,11 +30,12 @@ namespace Aberwyn.Controllers [HttpGet("getMeals")] public IActionResult GetMeals() { - var meals = _menuService.GetMealsDetailed(); // Hämtar med ImageData - var mealDtos = meals.Select(MealDto.FromMeal).ToList(); + var meals = _menuService.GetMealsSlim(true); + var mealDtos = meals.Select(m => MealDto.FromMeal(m, includeThumbnail: true)).ToList(); // 👈 fix return Ok(mealDtos); } + [HttpGet("getWeeklyMenu")] public IActionResult GetWeeklyMenu(int weekNumber, int year) { diff --git a/Aberwyn/Data/MenuService.cs b/Aberwyn/Data/MenuService.cs index 4ba0483..e0d071c 100644 --- a/Aberwyn/Data/MenuService.cs +++ b/Aberwyn/Data/MenuService.cs @@ -165,9 +165,35 @@ public class MenuService _context.SaveChanges(); return updatedCount; } - - public List GetAllWeeklyMenus() +public List GetMealsSlim(bool includeThumbnail = false) { + if (includeThumbnail) + { + return _context.Meals + .Select(m => new Meal + { + Id = m.Id, + Name = m.Name, + Description = m.Description, + ThumbnailData = m.ThumbnailData + }) + .ToList(); + } + else + { + return _context.Meals + .Select(m => new Meal + { + Id = m.Id, + Name = m.Name, + Description = m.Description + }) + .ToList(); + } +} + + public List GetAllWeeklyMenus() + { var menus = _context.WeeklyMenus.ToList(); var allMeals = _context.Meals.ToDictionary(m => m.Id, m => m.Name); diff --git a/Aberwyn/Models/MenuViewModel.cs b/Aberwyn/Models/MenuViewModel.cs index 335cd0e..449763e 100644 --- a/Aberwyn/Models/MenuViewModel.cs +++ b/Aberwyn/Models/MenuViewModel.cs @@ -104,20 +104,39 @@ public class WeeklyMenu public string? ImageData { get; set; } // base64 public string? ImageMimeType { get; set; } - public static MealDto FromMeal(Meal meal) + public static MealListDto FromMeal(Meal meal, bool includeThumbnail = false) { - return new MealDto + return new MealListDto { Id = meal.Id, Name = meal.Name, - Category = meal.Category, - IsAvailable = meal.IsAvailable, - ImageUrl = meal.ImageUrl, - ImageMimeType = meal.ImageMimeType, - ImageData = meal.ImageData != null ? Convert.ToBase64String(meal.ImageData) : null + Description = meal.Description, + ThumbnailData = includeThumbnail && meal.ThumbnailData != null + ? Convert.ToBase64String(meal.ThumbnailData) + : null }; } - } + } + public class MealListDto + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public string? Description { get; set; } + public string? ThumbnailData { get; set; } + + public static MealListDto FromMeal(Meal meal, bool includeThumbnail = false) + { + return new MealListDto + { + Id = meal.Id, + Name = meal.Name, + Description = meal.Description, + ThumbnailData = includeThumbnail && meal.ThumbnailData != null + ? Convert.ToBase64String(meal.ThumbnailData) + : null + }; + } + } } diff --git a/Aberwyn/Views/Meal/Index.cshtml b/Aberwyn/Views/Meal/Index.cshtml new file mode 100644 index 0000000..2ba182a --- /dev/null +++ b/Aberwyn/Views/Meal/Index.cshtml @@ -0,0 +1,48 @@ + + + + + Måltider + + + + + + + + + + + + + + diff --git a/Aberwyn/wwwroot/css/meal-gallery.css b/Aberwyn/wwwroot/css/meal-gallery.css new file mode 100644 index 0000000..6601d7a --- /dev/null +++ b/Aberwyn/wwwroot/css/meal-gallery.css @@ -0,0 +1,107 @@ +body { + background-color: #f7f7f7; + font-family: 'Segoe UI', sans-serif; + color: #222; + margin: 0; + } + + .meal-gallery-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; + } + + .meal-gallery-header { + text-align: center; + margin-bottom: 2rem; + } + + .meal-gallery-header h1 { + font-size: 2.4rem; + font-weight: 600; + } + + .search-container { + margin-top: 1rem; + position: relative; + max-width: 400px; + margin-inline: auto; + } + + .search-container input { + width: 100%; + padding: 10px 38px 10px 12px; + border-radius: 25px; + border: 1px solid #ccc; + font-size: 1rem; + } + + .search-container i { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: #888; + } + + .meal-gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 24px; + } + + .meal-card { + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); + transition: transform 0.2s ease, box-shadow 0.2s ease; + display: flex; + flex-direction: column; + } + + .meal-card:hover { + transform: scale(1.015); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + } + + .meal-card img { + width: 100%; + height: 200px; + object-fit: cover; + } + + .meal-card-content { + padding: 16px; + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .meal-card-content h3 { + margin: 0 0 8px; + font-size: 1.2rem; + color: #111; + } + + .meal-card-content p { + flex-grow: 1; + font-size: 0.95rem; + color: #555; + } + + .btn-readmore { + align-self: flex-start; + background: #007d36; + color: white; + padding: 8px 12px; + border-radius: 6px; + text-decoration: none; + font-size: 0.95rem; + margin-top: 12px; + } + + .btn-readmore:hover { + background: #005c27; + } + \ No newline at end of file diff --git a/Aberwyn/wwwroot/images/fallback.jpg b/Aberwyn/wwwroot/images/fallback.jpg new file mode 100644 index 0000000000000000000000000000000000000000..042b1104373d6c7ea935c0ac8de1cb7714c6f0be GIT binary patch literal 31239 zcmeHv2UHZ<@^=qeL=goQMMlIxkSwAo42Wcu90Vk*1Obttg3y2>ibx&|C^O^?f*?sn zF$_t`8AL@S2}*`x!gpr?b@#n}$M@FV|Jn1=o@RP_Zr81LfEDIVxpp0#CM8Vnk9;K5SI4)bt1!J8)_%Z zf0i|{tbt_>ENfs{1Irp%*1)m`mNl@ffn^OWYhYOe%NqD&4Ujvb%g`q3v3t{|-LkuP z@0Qh*1&5x!9_6t2wWl7wi^m=6@qltvemf9I9v%r%2_sQaqlQrT(1!o|Aa{efUXJ_} zVq>BMSG0(25DgoGmJLCEfrtl52zmtdSP}!I3+U(>7@1ZuuUrGsAZTf6=xFKb=|JKE z;Y~>;(6Q043e8 z!J)D7iOH$ynb|o!$Zaf$1O6^Z^n*NXKpq-8I$AnLiaZFKbKs<9qod~$V^}M%&1mVe zZkxCt6Z@g?xZGzecqMeca#)?MXI{U3&-)!?6p^SB{hUDl|C2=D1o|cqxdURMMSuaL zWrI-Az(9Nu#P|R4Nv>N~LH)7{8kSYixU7PvWfe3ptDt3B1+B{}SjGk9Wn4gB#s%bM zTtHsN1?2xLTo3|PzGH^?v&UYPCh{nkaoluCB}4Ru#7P*NnRhJ4D+1S4Z@F1B%_N73 zlXQu8Ds^>gcRX^Yl?)l@4J0#Qn@t`mknm(^Sg$gz{JgsjQF)NWaa|E|8+*tE*@LQvIGMIiZj1H{VAqzT|C>)F`Tquq ztblC3(!0)_JNfam*|sf@eahY&NNx_sJlG{)4a*;kdI8ix7nUxrDaW@)+gNKbMCp`c zY<26zpy7t2`CIfbX?s?`TE+3iZC;^XY;tG1dNNxLE9N8_Vv{qfjz@I8Mn>Yv636i)n#*DF{uGm1(xM>ZeK~PW=jxXvPybp4V#kcJLUl)N1b+W z(#V;jD-4=`4$nVo?NZgMeXJyk zCw^T>S}U=U#Lo08@s$$4Z8C2{K}@Zmjf^KUHG?BOsqFQ3b0uU28B%La)Y4XDhyKPB z@qdtu5!|eYdUV5^9SGc>c|?=w2^SM>*)WRix@>iA*_|rHSFo&ov*pgXJuaEH839`2 zuASq__YMurXipj|NDXA48!3b*8Yj)aCeEMeAxxpd$Pn}H4bswrcfWr(f7nfiYBlTN zfb}cyaflzD*k((H9wqvquS%|r>(*?B%M*_uwVVmpY~1E6u>sB4aZiN-;`Wceb-l#p zWgT~_@_5!z|7TS12;6Nyz`u%OhRTEZurzb4-r7#N_nnLCP?DqOtIK8=Vk1~eD7D5w-5W)BPs=8t0oyvX zYC(zuPsPKOt)zA8MB{__puhN}{X1anc0}Y;4ucP)fa(5L)zaxDmJdSe1 zDn)ffh*UdCA$4b9{1ke7ghpqQW82_>tp9CAF?x0_#4h{b*r!?u#p5C6CqHQ@p(N&* zA8$eDdmCOLLky|;Ws&y8Y1iicXNOUMAiznJPK8S&xGIjr= z6aGsv{q617)@}#`pbe&ttTRx26Nl;UbR&_WS$&h3!r}uypW*H)0LTLLlefkP9gSi# z*4M$=)2MzH=vuF%egzIk=}vNFv1^x?TA9Me45t)QazD8i64AqR;2Aq*E|*ogeA*+L z@nJs;$5rcZZ3Xt5Fh%Sz<)JO?w$WW-sA&1EZkA~h%Cp{$&mg~<&jyuLDpHcQ_I*0Nog)& za#R-cUha*5kiE2uU+;e1M)sk<46>m9vS1|sj!j{x7m2pD!*@k;2wl&vcLy+*ri6*g zGxI&Z(tHE_c~ui-x4~V+fPl0WGhZ6E(CbS18AgN8d~BO|E1vOk%A#|eZNlDve7=tm z*pR8na%N`*uC=Kt|H|(sGA+Pze)Q~7`BZcfmH)@ky2^09sz@z28yQlV$_!M#@o-;< zvi-o&e#;Q|_N{|;Aqj&T$});a4z4wM*!qz>SQ_~?)s}6~hP5VpKYrxSXlqqqNt0f| zdk_|}|0H0vGopJ2HyiRoOCmWw{)$bk*miFj8JTqBzhtaXe@Vm?7M8Hfmk*Iw5%$U^ z(iinc@NVp>qR1K_FedynhCBPl_bJXN;F~?c@MIV~Om>YdU!Bt0ro|(aW&Eyx6gvcC zvkDXynJzI)Bh(-*4khRvLS4mDksWy4e`{-0owAKH;QIZTluC}i#Ha=I_ z@lF>$ZUBzUsydWjrF}*IT@dtXAVX^JtM0u03Uzp|O<5X;>yA!;c`SX7JAp8n9mKy@ zZ>@5LWOZ|ldgN)s_>GysDfU%cIM{vt5oYp(ROY_*zr@F%`DFYHf-1ocTE%aBPKA!< zyq%H71eiT89_Q`Aco%WMovQT<;XAZGh=1%=Ls5uq$|U;>RYydTcR~R2Jb;|t`g&Sd zMHwMos~bGQO7|Ftb#yi^0d5_gdJu6^*T@b?XfgC$q5eBazR(IRpdCW&(D#+3qeW8> zTDIuB^Ysdx-*!J+sH1y2R*%?7L^1cI_FdZ<{EEmrD91IzKF4c&_Z1QN>)RSiAsE-b z`-O0c&zIaufMYwdHM5-CP3Oi&;3xOIan>rfrC=ibO29(DhrUiyMeQf09{?y_G%#vsdh zl7g;PeuJzdqN(V?cRhgHbV>0MAP`OZ0ekcFA%MGHD4{1(eo!<1UQegq;KMQ(dXO%{ zB0c7%B#yx2k~Zegavx0&bV=m`G}uTJpvs;_=dU-?+2pC`rp244WGDwhVH@2H-Xj>g zPN`-%HOo`+XPw>8v_t;lILjttxAtvhd~mC1&6}4!8D|}Ib#IP#hR(#`nX8hwrfl02 zlAB`COCa(Wq@_F$kidL+?-RxJZsH{z^x2Y;koj6kc&$rkAF7eSM=O3e#L6A*8COe&OvaPzHI(%-ITb_> ziOWV6V3I3KXg8dHgQ9ID2p$TT|G^FYIrIPPGWvomwIjV>y{QlO*@gL<^_J1N?-=2g zIP$}O!oZ=mVP7TGbvtmMOt-~e`h=3#P-K=;%} zRKE-KF9d*7B352qRUO!ScFG_vd4_MpvIwP=cU#%s;Uy>_-|J}0aXlSWHxS_+A2nKi zV}ccgF**8!SBou3$1AqnbtJ_X4o6jLNKcuv%^K=uZlS23e-*{+FRjWyGwW%8VWg4a zNjjbp-gF~j>_hPvOJjV3C2wf+gE{7k=8rkorVPqm zchgqn30QQVOA~$3iSB^dsmSxk*-mY#1C$OXJ9|Fm7gd0GzD;(D;(tSI0m33;Qd6qA zo&-AgH_jR8J0H)Uw`0V1q23*A;Sgo+Y3%V%4;?)L3uBTj46>sMmB{+QaZ(hGWa=-< zZ2roS;CWMw6mwa(<|kks4`rI|9WG}2EJyH7pR8OnnT;V1IXQ$njj1r0vNB@n zBC?fXvq}`GMvrO`qIBx0R_~j2&{@1eh3;UD)3fJ;S-XQB%GU;%T~(d>hXU3q2!4kU z#2%&n4a(E#)GI<1+d{pd#AY1VE{eH%H)~Fk(1k|$tHg2SG%6g+%wF7~;JF6oRg`@4 z9t|K;XN-xd|OGU7a&G0P~R8z4L|_nnTVy1{*qs$xweBA#hfE zy1Hzk0ismh2z9y?@D(_Ye6QRwy4XUCVT1#aS#0H2v@X%{rHEC~4KGS{ z6JC&t+U>X0wQRdP=vEVSOKtw67T}4d1ucMXzX=1xUF>&(%SB=Sh0E{S$(b9XsL!`M z{{9oHjFiqoA0A)eE5+jX%Kr_U-=z92n?FcpzQE;@RKHi|qEz3Mu+W?z@mST!{QFof zX?1a^ejITOm>`f-{bFm%I4D!cJ`Ie$F0Dsifk@eg`7FXq!|y{7q-qm&Nze&*6w~0# zdBnK{Ohiq4I(IQRU2{fxJf&4yL^z86*gh_VABjWU-f-niImk>uu`@c^$6@exd~+z; z$AC0QLfil88YJ>hM_ci++H^wvB(|qBBXaCHIV^C|<@co+d6MLH^~(KDsvqk$!s2~b z@aWmZ`s$IP6UAWiAHi$Uv7@a&7osLA%hWXUy1=^HN#vv1B;dSsJug&C7)-I|p4 zq}r7X9gLYwSZohniAY)eIW!N<-G6zqmHH%4eDb^y_3~RQX5*O8h31q#&6gg0EOcbr z1T*)sCrPB;sjNTJCkjE&fogYxDVN^S{iF+0gK}%Mb%_g?%8S1s%%TBxDvLK4NkaZX zfI3wo*+q`AOne#0MTvF35z}b`YAB(qL3`RHhtfA>7@z4KJxuBBhaQ%sj9e1jKPY$c zfn7^{&~pGaQbp2S5{X#=xA-e&u^XW6@2b~X6h)3>AdU2U)&83vR&huzk}LaxT+i<% zP*BwT&f9__S5Xv6y~KPU_JSqlerIkEdgxG7|>Q4|)<6Y0O79noB`t-yC@2O7TWHaVSU$NszI;(wJjhyP?KEGVIOS}7d= z5auy<4^I!BQYOB~Gegz!5y3WAw|5dRuVkmuxcElg=U#wWddt}rLSZ3TN0j!;cH5$~CsPkva?x;T*?+V-_D4AFnreA#il~V4j;-igZ*DtMxA5@QrhR>NhYwO!Z znC41V-=^0sfvBzaUs!og7Q1g1#L(X?)uSv)x z)A*o2mF_|Qf@+`t2ZYOt;ICdtg^uDprhS&62GUScQd5>MGCvzw!&oDuJR3x3-H;(J z>b8$JE;-~bz*UbOyt!tT%T)*4h^X=$!yV5yN=loW9v0)e%z89oD*kzKqqgbSvQp%W zvYFQ+(*doZRM&$ddK%DT`Q)1-W-K5Emp` z>^6y1SNgkdmY4%{zwx#pk^7=Vp(88H7PV!x+Mm+V5)IJNqJk+M`4Qum9*O*cwS^)2 zCQZUpnKNpa}*g?}kn}q4#VvLz<6u8b_GnL^V;|XsEog z+ZBJ1F#f|d&mX0Kez?#0i*hH`rT4ou9`?o#4^L)>%pPRx1S>g5CQ}M|(X~C0!Oy6l zFJs*9K?**P%SV|YkYUob>+Q8N1g1~puwK+irgR$8FR{Olq~yCcI`k7{sPXKSC;KvG8lcR}fz= zk*QiBtpvy`QOSI?{jnh$c=Ux%K!4xJtWXsB9qErCMNA<<;S!3{Au*1FUckdqWqyP- z3J)|5-)`<;2R8vbi_iQ4M-k1R&;#o)%ffumSS@9){wbfoBz5x76`wLpe>#-?XNyl&!SWj_e=@%Kr>yt? z#OF_feg2IjI^Og`do_!C6bPxb;#)MT#&V(<#u?uZV#*5~ZWe$F9OHcTaYY(R()qtC z{iJU&Ozx217`)d|#${yCaF|tx426P?j;{DRhJySaTfY;Xh5LJR6oT%g4U(a;PZ$F- zRCmJ^xPRbr^0M7mtkq=~C^B2&Y~E1dAkeRQE~;`RqBLSN-pW_lxXU|P=!GN53K006 zQs(!VSsd^33+~NY|L{YH(Cx}y+nc@BNiw+kVL8_PQmNflx+m*cO{WNqNyjt2MTj<- z(VPj%+=ZHrZfWV>B@l9m1XvAvZ09n4s~X*Yr8Bty{xrwQ2l-`F ztfV>8q5Q>?bDDYecCC!Vok!p8!>L`@jI|5)H|Wk6-bdS7r^SF)a1kxMT&3jll)K+g zS5LS6!3(Q{r{X4YYEaE}otrf@Cypof_VsdR1yx)>5~jjZQ)+luZW zM;-52c`wPtL8NfwWKw?qyr`9l>t2ahj9chw){pjouJ>m+76OgvkBG$D9Zb`sQKt7U zwaV-^Ss4YMIx3$kwEJSsVWUvNmxHftrG zMt%`;XJ5g2PO9Pgfx|(GkaT}hnlaJZU+P3nNZiBFiKm&vUuFq#JQ)(WQ}y~3w_8_^ zB6m^m_!cOcc6E|#XEk>ls#gb>R)i{(Q%-jYEzf`Q+A;WoDhODEsvhU7K+(eNci9F9 zgP;xr%3Bbuhxi=?)x!v1r#D*8PG!ujom-Rt(Y{?M z4Y!x-Lg+^cqnBlZf(_V%#-ze`5gzP9hNcuiPVH}I*fyL&uqbmk+dr7Bot<%>shv#e zd1HB1fJ@@5OrWcZwUZ>%duuLhuhoxz(;0O@K%Hz&$<8$H9`UJbJE(Neg%IGFm6f+D zV-rb_sU}o63i%exOAJ9N80V^URd7p$>kIGR7Or}k&Zq4ivOI6()ZPh(gZy)*KA0rP z%{|gJ^$1IYQhMSpjsCh#E!k%$NV0wSP$Hd{Z_^#3r9R{ombz~XV@oO-vNN3?$30}7 zZEEqjE>4W^{nTBDbP{(^{@l`ri@HUI+_V_njBmdYW=9bFMrI9)wg!iw1H9}X_L-rI zx~%J(AB!KU-=jRsid2x>q(@mj&zs76``|88L9vnNO~S>Tc}&aYZQY6W&n!ZDo>s#H z%6|GbO?|Z$>6eJvnv^e`&pc??jZDO=?|io9vVAO!+PBeWAMad{g*(T+a~F26W2)D?ft|p> zU$f{>oM3s(RDn0xnLvh+3dEPBlVPt-jv0Hop?&E~SEEgIfq``);hXNjgbTPy)SCp5 zW$x{QpX4*;B$1)aNMbXoAlC?Qj<1Bf&4-N*!3^3{G_a4GNDp0ga<(o!0D&xg_m(G5 z(0APhF)B~-$T1EBzPA?+TM%AXqH0V-f##|FHRV}r>38_{HYD8l=dAJ9UzIP6GPV-wFq5nnk6M{e+ls2wPupyBx_4F{ z6_%{%NJn=oO!)kVXn1NZTmqwcZnv~f9NFWNm%3^YqtWu%)dM*;#o9}Tu&qjbYxiG( zTgr%TC_IO|WVJd8xWVB#;GQzluxzh;U|dsAAjx0Qvxg)D9&mxtmgYHn$J?laP$(Ru6CA6a+y;YgdrR;Q`3XrX;w9(%4d@HeeBve;!%?-7WaIM5h zFl^jd@F=9W?-h@R!h8n zm~kucT0C+jXCgb^_prnft`$!d{OVTsw;8Apxvm=*07>TeTvta>52g8K*)Z)m)=*^{zDOyUD)O=x3mN*H9EOvJOLj%Sqi2@e z>^0Vq{v<^VwJn|1M{_cL`r)Tfnj-u3KawF~K^I5IFiUIm>OD+V$JO3>_bG9=g9Xwu z!`8KPVE&B!kU-Y!%NhY#t?UD<5z{YEwyXDb@oYi%p+eOx-1ONSaZWKG@sKno>4@p; znzt9Tc3GB(T9Wo$G4?)s)<;pFn*er)Z?kv3G(l%Mu#XIFDs;5m#Anwef#<%|$pH*f zm!oa#IBpz!m|<*FbAMp^YQ5=HN9PlFT-K%g`Jb;nt$!fHBi}hPDvYCl4h|5;fR$%W zd_{_S`ZoI*+`@_Tl94{+qT4KkA|31{!1ol=G_=>#_rm5g%CmK15A4-a980d?#<($o zS^A`gV&p{aI3)pktb-VL&57;&zEN+%6OLI;jz+CWc@0H%Z+RO_uIH8qY1@FP3NFA- z=4}&rFMUe(cA8WEZ1+mJIFtE=xM`gItMD4Xd&S#fo*-yQYgEy?i}q;hdorYl84d0G zVT0-Kw`~3J9r2TihHtL(^pfk``)^z)7x4?h2UQQF9oiwGZMYeF8#6M5>m_cOGkA$1 zcq)?Kk@TMC>J_lgPQqaRiW%C+f;GtPJZ1igpCv9Cw5Gqj)C;ZN zn04o6Qx~U>7(qal02B4L?jFR9jrM%O4C+hr#srv;Vu_ldWRUg!F3)weY${4YgRb!L z{J}$F>qk}JxDW;45hYO5L(R^B#`jJ+I!Kn3#z%idK5fR_ZwK|w9f|asKeTvo(pj>* zd}P%d6m$3dDhy%V*!;dRT)QKg?*85ZUe|5}QIKJVBnt#Zl?%q}GsUr6Abe#4Tnl@~ zCZy&_Y^sRI&M(;ffFIBoQe?yf*Vc`!O2VG>NC8?JCe4!cSEFZ=@Vlnc3II#yGq~|o zx17f!^{6Zw$pP>x2}dAEP~)n|P%+Wr?ENW`**WtWR2GwBw z)_%kM<{v+w+g979%9yuy9u?G$*(5MXwW$+D>k zGE`Dl)6@}1Y@iG-kJcbOyn_t6!?)848NcEsMBBrt(`P!$F@PT9djNKy$bZ@h=P*^#pY zL-5L(@14_=HhgOc`78x4F^yxGKUkm(~<~a7` zsz8Fb!YmmYkgN~t-;L@WLgL7fe0H$b_OoE!X2JLY7?=pb-@*8pCqPMQWvoOxgQ+q+ z*p$>|-e)A$Gd3j!mLxXlZ6O)bM$TU+q|&*%-mT=H@<3(_rZThBoB3M!aMYOcA3I_8 zJ_L0u`!<)#^=*?DOxQI;N?JqOC895d#Fg2U8l)6Cv8N=RNe{t0&vUK1rUgYW?q4EK zk=z?qKJ2k$Riq>7typ7v{H3#R`)*IZVKUT`&ZrkTYlXZH`({oa+ZDyy8#WHtVzXY{ zFOUzW12w z(0vg1g<-i()5x*@4GkGXm?7XOVNmpzw0cfD_yMo){oPTlYA8{kqMgh#u4*8CF8jjT zXPm!^Sr830aBRc;Fkv{IquK<(m+OFgH=ZrqYjk#5wyP2aV;{)1u1Pr^T6YR>s3`E| zMi{(?cQV_lMN`-|QRZsx2Qj%VG9VVIx;eC|o(#2b4t#&i6DD|v&Y0j0?#*A$cOia> z@<9>pUpIo0{tTD&H27_9t`@;RBSXg-Ndd=?oUJC^hg;=A+@>Ob|nU7Qx%4a*;Kcl2r+>9FnuG`d&m-Zyiv@)v>^8Yi`#+ zC#9jl%1)#hYaObw{;5Em_9ru<-;A{qr%poAOUQNolC92E*)x3Wso~_^UwP*3XKzK{RKhqSVhlDvpwUu+hGz~%!dP8 zD*Ui3#fL<q_4(9~mwBfrq#jd-5O z2N%4)*A3bIs^@E?aXDPb=L7-2Mklxp6$*^M@1CTx!r+UUTT1Es!PFJxu=c8)E;QWl zY)LxL;j*VPw8veWREv6V9A@&;snmLlXJk_yT^nTKmhCi8l08Ya_=%ZbF)Oc~j5y36 zICvM27(7IKlywTc@l=9Bt83g)Q%d#pW&FrL$?|5=QgHxl-v_LH#?AYxE^AaC`fud+ zwD1-SAK&?zpw8qDQiM7m&*}sp71Vj#A$X$PT}E}t&QCf$ z{<0a3FJs*g!Azl``1qR({cDvBH_P?QJQ(0d$Wj)P3z2mqbqaVeUa}z{3C-bv;)|J=0RrI!npbe^896UV2a%!N>uAQ>Y zadKQQoVTwHq-6=DgT+kh%}mt9x%elYn37G~m0c#haZ|`90|&cS-e6wVY*dn{6$Aw?QnR+rq zdzMl~_DNA|OSL z+T(6&Sitpd*7c=6+x#}!?GeA1Tart=kKq~_YC_GY%o%tmp|ODU>A`RV@+$?n}cclkG74>dK!$1iyg}w3>T{r>YKOb9UqZf zFWY@8c^OW%CpxxPXQP%C?)nA{LwvOz1Q@GupAAChEKG}xO zI}nBw(FZGHWn-dp`c0b)@on&1;~_peS2mTpsywzTZsh?*H?b~xzM9jlg)X!DUNP)< z=Yl*?13vK&VvZJ*hNOVQX-Q_tZw))N-{N(!@hJm>KAi&H!cv^l&DyjTq1 zbLY6W{@&RL-nF+37=tGn2jy;5^*aj@u4y89$IF@w6^@*5P~;mf*0g6h(P__|&RaZE zU-l}iU@AU%&Ub#b=0|~yL2Fb=r0t|7=4*0xbo*RiFA=0uH__=A-5YItC>ow-A;UO( z#|Nw#YKVgN{KS`)X`}K=Tx@DrP(n&@cc_i?pvEeEpW(O6TXu3TL(Ew&u4LL%Wg7hr zVVm74t_?s!;M#sL4O;tabM9kT6JB~OO`rYv(xRLYjfL$#WTNpWYJjjyeYH-LFD(VS6Fn0 z$x33IF14}tX?}Z4=s#`2q5U6AV*>jVCM<1BoPb}_i_&70WXW+Bb-kOzQcS?j8W0TE z)JSZm9jOZ~k1Ng#dH03U8Xr0&C^R`7=8|hZpgfh#d(z>S1@Zt3Fs1H<3{14@uBZy- znl|QyNe*$lnZvzSxG7%n#&AKDNB@O6Ol#u+AC1HL%WEqa>w{fbL1MPi2xq4IiU=)tHfDbdHlAii-sYPgMW(yR<)Zy|B?@}sxs zzTz4wn3ALT$YGVb?bgiuXzu;{cUw$HJ|E$De)gI9Co|0tDCU85@D8ur_(x7;^`IPw zqjLiKP}QNLsdvbfS^ZaT^2N6hUyK?CBSr9eTfA8r2?9lpqpmNu`UuKhb9M@NwgQPS z=pTZg>Su|sO+V$_``+gg!ES5gh3>Ne#hvi<*;zo%-BaJC)ZcPwXT++?yK!$AQ+&{# zLD18@()FJeBCMT=XW;U}wJIMsCs;ghYhE$g=lX>9erylGYAu=5jiV1psZvF}_z$;k zsK*R^(8*oX;6aN8>6`%T&Zno)RzuRc(_qav-^%a2@_zai_%91q3tBPp79I%?O&hhP z0dF0I{Y<@|b-9uW{mD(3|EWCoPx)=Rk_r7}CG(#$qW=q@KUuN*Ur7H`p83hl<3B~5 z|Ao(=jOYIs(*Kla{zn-1PZ95D_*}*X%eVmSb6fsDc$aYjw2TX&Wn2I);{s?I7eM6p F{|7WL@;?9o literal 0 HcmV?d00001 From fc78ec08136f323b1b019a6f57c2a25f6a007710 Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Wed, 11 Jun 2025 13:51:45 +0200 Subject: [PATCH 025/103] Meal fix --- Aberwyn/.drone.yml | 4 +++ Aberwyn/Views/Meal/Index.cshtml | 39 ++++++++++++++-------------- Aberwyn/Views/Shared/_Layout.cshtml | 2 +- Aberwyn/wwwroot/images/fallback.jpg | Bin 31239 -> 13161 bytes 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Aberwyn/.drone.yml b/Aberwyn/.drone.yml index 4fedd7a..d52e248 100644 --- a/Aberwyn/.drone.yml +++ b/Aberwyn/.drone.yml @@ -52,3 +52,7 @@ steps: else curl -X POST http://192.168.1.196:8123/api/webhook/aberwyn_update_failed fi + +trigger: + branch: + - master diff --git a/Aberwyn/Views/Meal/Index.cshtml b/Aberwyn/Views/Meal/Index.cshtml index 2ba182a..07e4abf 100644 --- a/Aberwyn/Views/Meal/Index.cshtml +++ b/Aberwyn/Views/Meal/Index.cshtml @@ -1,3 +1,6 @@ +@{ + +} @@ -8,31 +11,27 @@ -