diff --git a/.gitignore b/.gitignore
index 0e5c9aa..bbc75d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,6 +85,8 @@ __screenshots__/
.env.local
.env.*.local
*.local
+API/appsettings.custom.json
+API/appsettings.custom.*.json
# Temporary files
~$*
diff --git a/API/API.csproj b/API/API.csproj
index 58e40dc..6d308c2 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -7,6 +7,11 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/API/Database/ApplicationDbContext.cs b/API/Database/ApplicationDbContext.cs
new file mode 100644
index 0000000..5465878
--- /dev/null
+++ b/API/Database/ApplicationDbContext.cs
@@ -0,0 +1,5 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Database;
+
+public class ApplicationDbContext(DbContextOptions options) : DbContext(options);
diff --git a/API/Dev/docker-compose.yml b/API/Dev/docker-compose.yml
new file mode 100644
index 0000000..b41e3a8
--- /dev/null
+++ b/API/Dev/docker-compose.yml
@@ -0,0 +1,38 @@
+services:
+ postgres:
+ image: postgres:16-alpine
+ container_name: hoard-postgres
+ restart: unless-stopped
+ environment:
+ POSTGRES_DB: hoard
+ POSTGRES_USER: hoard
+ POSTGRES_PASSWORD: hoard_dev_password
+ ports:
+ - "5432:5432"
+ volumes:
+ - hoard_postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U hoard -d hoard"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ pgadmin:
+ image: dpage/pgadmin4:9.3
+ container_name: hoard-pgadmin
+ restart: unless-stopped
+ environment:
+ PGADMIN_DEFAULT_EMAIL: familiehimmelberg@gmail.com
+ PGADMIN_DEFAULT_PASSWORD: admin
+ PGADMIN_CONFIG_SERVER_MODE: "False"
+ ports:
+ - "5050:80"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ volumes:
+ - hoard_pgadmin_data:/var/lib/pgadmin
+
+volumes:
+ hoard_postgres_data:
+ hoard_pgadmin_data:
diff --git a/API/Migrations/20260418134949_InitialPostgres.Designer.cs b/API/Migrations/20260418134949_InitialPostgres.Designer.cs
new file mode 100644
index 0000000..44f7c88
--- /dev/null
+++ b/API/Migrations/20260418134949_InitialPostgres.Designer.cs
@@ -0,0 +1,58 @@
+//
+using System;
+using API.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260418134949_InitialPostgres")]
+ partial class InitialPostgres
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("API.Models.Test.TestItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("test_items", (string)null);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/20260418134949_InitialPostgres.cs b/API/Migrations/20260418134949_InitialPostgres.cs
new file mode 100644
index 0000000..36c25e1
--- /dev/null
+++ b/API/Migrations/20260418134949_InitialPostgres.cs
@@ -0,0 +1,39 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations
+{
+ ///
+ public partial class InitialPostgres : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "test_items",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true),
+ CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_test_items", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "test_items");
+ }
+ }
+}
diff --git a/API/Migrations/20260418153650_RemoveTestItems.Designer.cs b/API/Migrations/20260418153650_RemoveTestItems.Designer.cs
new file mode 100644
index 0000000..4d3ef64
--- /dev/null
+++ b/API/Migrations/20260418153650_RemoveTestItems.Designer.cs
@@ -0,0 +1,29 @@
+//
+using API.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260418153650_RemoveTestItems")]
+ partial class RemoveTestItems
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Migrations/20260418153650_RemoveTestItems.cs b/API/Migrations/20260418153650_RemoveTestItems.cs
new file mode 100644
index 0000000..48bb8f0
--- /dev/null
+++ b/API/Migrations/20260418153650_RemoveTestItems.cs
@@ -0,0 +1,39 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations
+{
+ ///
+ public partial class RemoveTestItems : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "test_items");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "test_items",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false),
+ Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_test_items", x => x.Id);
+ });
+ }
+ }
+}
diff --git a/API/Migrations/ApplicationDbContextModelSnapshot.cs b/API/Migrations/ApplicationDbContextModelSnapshot.cs
new file mode 100644
index 0000000..b22d9fb
--- /dev/null
+++ b/API/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -0,0 +1,26 @@
+//
+using API.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ partial class ApplicationDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Program.cs b/API/Program.cs
index 0028f4f..55cb5c1 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -1,10 +1,28 @@
+using API.Database;
+using Microsoft.EntityFrameworkCore;
+
var builder = WebApplication.CreateBuilder(args);
+builder.Configuration.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true);
+
+var connectionString = builder.Configuration.GetConnectionString("Postgres")
+ ?? throw new InvalidOperationException("Connection string 'Postgres' wurde nicht gefunden.");
+
+builder.Services.AddDbContext(options =>
+{
+ options.UseNpgsql(connectionString);
+});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
+
+using (var scope = app.Services.CreateScope())
+{
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+ dbContext.Database.Migrate();
+}
var webRootPath = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot");
var indexFilePath = Path.Combine(webRootPath, "index.html");
diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json
index 0c208ae..2ea6405 100644
--- a/API/appsettings.Development.json
+++ b/API/appsettings.Development.json
@@ -1,4 +1,7 @@
{
+ "ConnectionStrings": {
+ "Postgres": "Host=localhost;Port=5432;Database=hoard;Username=hoard;Password=hoard_dev_password"
+ },
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/API/appsettings.json b/API/appsettings.json
index 10f68b8..411e4a4 100644
--- a/API/appsettings.json
+++ b/API/appsettings.json
@@ -1,4 +1,7 @@
{
+ "ConnectionStrings": {
+ "Postgres": ""
+ },
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/README.md b/README.md
index c0ef714..1353d49 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Hoard
-
+
Hoard ist eine einfache, selbst gehostete Web-App zur Verwaltung von Dateien und Ordnern – mit integrierter Markdown-Bearbeitung direkt im Browser.
diff --git a/codexInfo.md b/codexInfo.md
index 162f83f..ce45853 100644
--- a/codexInfo.md
+++ b/codexInfo.md
@@ -90,6 +90,11 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für
- Backend-API ist auf ein Minimal-Setup reduziert und stellt aktuell den Test-Endpunkt `GET /api/health` bereit.
- Swagger/OpenAPI ist im Backend nur im Development-Modus aktiv (`/swagger`).
- Frontend-Build (`npm run build` im `GUI`-Projekt) schreibt direkt nach `API/wwwroot`; das Backend liefert die SPA und statische Assets aus.
+- Backend nutzt jetzt PostgreSQL über `ConnectionStrings:Postgres` mit EF Core (`ApplicationDbContext`) und führt Migrationen beim Start automatisch aus.
+- Temporäre Test-Entity und Test-CRUD-Endpunkt (`api/test-items`) wurden wieder entfernt; aktuell bleibt der minimale Health-Endpunkt `GET /api/health`.
+- Für lokale Entwicklung liegt unter `API/Dev/docker-compose.yml` ein Stack mit PostgreSQL (`localhost:5432`) und pgAdmin (`localhost:5050`).
+- API lädt optional `API/appsettings.custom.json`; wenn vorhanden, überschreibt sie Werte aus `appsettings.json`.
+- `API/appsettings.custom.json` ist in `.gitignore` hinterlegt, damit lokale Konfigurationswerte nicht versehentlich committed werden.
## Änderungen durch Codex
- Grundlegender UI-Neuaufbau der App-Shell (`GUI/src/Layout.vue`) inklusive Navigation, Footer und Seitenkontext.
@@ -111,3 +116,12 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für
- `API/Program.cs` erweitert, damit statische Dateien aus `wwwroot` inkl. SPA-Fallback (`index.html`) ausgeliefert werden.
- SPA-Fallback im Backend aufgeteilt: Frontend-Routen liefern `index.html`, unbekannte `/api/*`-Routen bleiben korrekt `404` statt auf die SPA zu fallen.
- Swagger im Backend ergänzt: `Swashbuckle.AspNetCore` eingebunden, Services registriert und UI nur in `Development` aktiviert.
+- PostgreSQL-Integration im Backend umgesetzt: `Npgsql.EntityFrameworkCore.PostgreSQL` + `Microsoft.EntityFrameworkCore.Design` in `API/API.csproj` ergänzt und Connection String `ConnectionStrings:Postgres` in den Settings hinterlegt.
+- `API/Database/ApplicationDbContext.cs` mit Test-Entity `API/Models/Test/TestItem.cs` angelegt; erste Migrationen in `API/Migrations` erstellt.
+- `API/Program.cs` um `AddDbContext` (Postgres) und `Database.Migrate()` beim Start erweitert, damit Migrationen automatisch angewendet werden.
+- `API/Controllers/TestItemsController.cs` als einfacher CRUD-Testcontroller (`GET/POST/PUT/DELETE`) unter `api/test-items` ergänzt.
+- Dev-Stack für lokale Datenbankarbeit ergänzt: `API/Dev/docker-compose.yml` startet PostgreSQL + pgAdmin.
+- `API/Program.cs` so erweitert, dass optional `appsettings.custom.json` geladen wird und bei vorhandener Datei bevorzugte lokale Overrides möglich sind.
+- Neue lokale Konfigurationsdatei `API/appsettings.custom.json` aus der bisherigen `appsettings.json` angelegt und in `.gitignore` ergänzt.
+- Test-Datentyp und Test-API wieder entfernt: `API/Models/Test/TestItem.cs` und `API/Controllers/TestItemsController.cs` gelöscht, `ApplicationDbContext` bereinigt.
+- Neue Migration `RemoveTestItems` erstellt (`API/Migrations/20260418153650_RemoveTestItems.cs`), die Tabelle `test_items` entfernt und den Snapshot aktualisiert.
diff --git a/dotnet-tools.json b/dotnet-tools.json
new file mode 100644
index 0000000..3a299d9
--- /dev/null
+++ b/dotnet-tools.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "10.0.6",
+ "commands": [
+ "dotnet-ef"
+ ],
+ "rollForward": false
+ }
+ }
+}
\ No newline at end of file