diff --git a/GermanApp/Infrastructure/Data/DbContext/AppDbContextFactory.cs b/GermanApp/Infrastructure/Data/DbContext/AppDbContextFactory.cs
new file mode 100644
index 0000000..7519d7f
--- /dev/null
+++ b/GermanApp/Infrastructure/Data/DbContext/AppDbContextFactory.cs
@@ -0,0 +1,35 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.Configuration;
+using System;
+using System.IO;
+
+namespace GermanApp.Infrastructure.Data.DbContext;
+
+///
+/// Factory for creating AppDbContext instances during design-time (e.g., EF migrations).
+/// This is used by EF Core tools to create the DbContext without running the full application startup.
+///
+public class AppDbContextFactory : IDesignTimeDbContextFactory
+{
+ public AppDbContext CreateDbContext(string[] args)
+ {
+ // Build configuration from appsettings.json
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json")
+ .AddJsonFile("appsettings.Development.json", optional: true)
+ .AddEnvironmentVariables()
+ .Build();
+
+ var builder = new DbContextOptionsBuilder();
+
+ // Use PostgreSQL
+ var connectionString = configuration.GetConnectionString("DefaultConnection")
+ ?? "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres";
+
+ builder.UseNpgsql(connectionString);
+
+ return new AppDbContext(builder.Options);
+ }
+}
diff --git a/GermanApp/Infrastructure/Data/Migrations/20260613125706_AddStorySegmentAndStoryProgressTables.Designer.cs b/GermanApp/Infrastructure/Data/Migrations/20260613125706_AddStorySegmentAndStoryProgressTables.Designer.cs
new file mode 100644
index 0000000..09ba06c
--- /dev/null
+++ b/GermanApp/Infrastructure/Data/Migrations/20260613125706_AddStorySegmentAndStoryProgressTables.Designer.cs
@@ -0,0 +1,669 @@
+ο»Ώ//
+using System;
+using GermanApp.Infrastructure.Data.DbContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GermanApp.Infrastructure.Data.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260613125706_AddStorySegmentAndStoryProgressTables")]
+ partial class AddStorySegmentAndStoryProgressTables
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("LevelId")
+ .HasColumnType("integer");
+
+ b.Property("LevelId1")
+ .HasColumnType("integer");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Topic")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LevelId1");
+
+ b.HasIndex("LevelId", "Order")
+ .IsUnique();
+
+ b.ToTable("Lessons");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Level", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("Order")
+ .IsUnique();
+
+ b.ToTable("Levels");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Quiz", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("LessonId")
+ .HasColumnType("integer");
+
+ b.Property("PassingScore")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(80);
+
+ b.Property("ShuffleQuestions")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("TimeLimitMinutes")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LessonId");
+
+ b.ToTable("Quizzes");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.QuizOption", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("IsCorrect")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("Order")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
+ b.Property("QuizQuestionId")
+ .HasColumnType("integer");
+
+ b.Property("Text")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("QuizQuestionId", "Order")
+ .IsUnique();
+
+ b.ToTable("QuizOptions");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.QuizQuestion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CorrectAnswer")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Difficulty")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(3);
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("Order")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
+ b.Property("Points")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
+ b.Property("QuestionText")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("QuizId")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("QuizId", "Order")
+ .IsUnique();
+
+ b.ToTable("QuizQuestions");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("RevokedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("UserId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.StoryProgress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CompletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsCompleted")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("LevelId")
+ .HasColumnType("integer");
+
+ b.Property("StorySegmentId")
+ .HasColumnType("integer");
+
+ b.Property("UnlockedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("integer");
+
+ b.Property("UserId1")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LevelId");
+
+ b.HasIndex("StorySegmentId");
+
+ b.HasIndex("UserId1");
+
+ b.HasIndex("UserId", "StorySegmentId")
+ .IsUnique();
+
+ b.ToTable("StoryProgress");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.StorySegment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AudioUrl")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EstimatedReadingMinutes")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(2);
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("LessonId")
+ .HasColumnType("integer");
+
+ b.Property("LessonId1")
+ .HasColumnType("integer");
+
+ b.Property("LevelId")
+ .HasColumnType("integer");
+
+ b.Property("LevelId1")
+ .HasColumnType("integer");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.Property("Theme")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LessonId");
+
+ b.HasIndex("LessonId1");
+
+ b.HasIndex("LevelId1");
+
+ b.HasIndex("LevelId", "Order")
+ .IsUnique();
+
+ b.ToTable("StorySegments");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CurrentLevel")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasDefaultValue("A1");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Streak")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("TotalPoints")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("IsCompleted")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("LastAttemptDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LessonId")
+ .HasColumnType("integer");
+
+ b.Property("QuizScore")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("UserId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LessonId");
+
+ b.HasIndex("UserId", "LessonId")
+ .IsUnique();
+
+ b.ToTable("UserProgress");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Level", "Level")
+ .WithMany()
+ .HasForeignKey("LevelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.Level", null)
+ .WithMany("Lessons")
+ .HasForeignKey("LevelId1");
+
+ b.Navigation("Level");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Quiz", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
+ .WithMany()
+ .HasForeignKey("LessonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Lesson");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.QuizOption", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.QuizQuestion", "QuizQuestion")
+ .WithMany("Options")
+ .HasForeignKey("QuizQuestionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("QuizQuestion");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.QuizQuestion", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Quiz", "Quiz")
+ .WithMany("Questions")
+ .HasForeignKey("QuizId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Quiz");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.StoryProgress", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Level", "Level")
+ .WithMany()
+ .HasForeignKey("LevelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.StorySegment", "StorySegment")
+ .WithMany()
+ .HasForeignKey("StorySegmentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.User", null)
+ .WithMany("StoryProgress")
+ .HasForeignKey("UserId1");
+
+ b.Navigation("Level");
+
+ b.Navigation("StorySegment");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.StorySegment", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
+ .WithMany()
+ .HasForeignKey("LessonId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GermanApp.Domain.Entities.Lesson", null)
+ .WithMany("StorySegments")
+ .HasForeignKey("LessonId1");
+
+ b.HasOne("GermanApp.Domain.Entities.Level", "Level")
+ .WithMany()
+ .HasForeignKey("LevelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.Level", null)
+ .WithMany("StorySegments")
+ .HasForeignKey("LevelId1");
+
+ b.Navigation("Lesson");
+
+ b.Navigation("Level");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
+ .WithMany()
+ .HasForeignKey("LessonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Lesson");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
+ {
+ b.Navigation("StorySegments");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Level", b =>
+ {
+ b.Navigation("Lessons");
+
+ b.Navigation("StorySegments");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Quiz", b =>
+ {
+ b.Navigation("Questions");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.QuizQuestion", b =>
+ {
+ b.Navigation("Options");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
+ {
+ b.Navigation("StoryProgress");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/GermanApp/Infrastructure/Data/Migrations/20260613125706_AddStorySegmentAndStoryProgressTables.cs b/GermanApp/Infrastructure/Data/Migrations/20260613125706_AddStorySegmentAndStoryProgressTables.cs
new file mode 100644
index 0000000..d5596e2
--- /dev/null
+++ b/GermanApp/Infrastructure/Data/Migrations/20260613125706_AddStorySegmentAndStoryProgressTables.cs
@@ -0,0 +1,279 @@
+ο»Ώusing System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GermanApp.Infrastructure.Data.Migrations
+{
+ ///
+ public partial class AddStorySegmentAndStoryProgressTables : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_QuizQuestions_Lessons_LessonId",
+ table: "QuizQuestions");
+
+ migrationBuilder.RenameColumn(
+ name: "LessonId",
+ table: "QuizQuestions",
+ newName: "QuizId");
+
+ migrationBuilder.RenameIndex(
+ name: "IX_QuizQuestions_LessonId_Order",
+ table: "QuizQuestions",
+ newName: "IX_QuizQuestions_QuizId_Order");
+
+ migrationBuilder.AddColumn(
+ name: "Points",
+ table: "QuizQuestions",
+ type: "integer",
+ nullable: false,
+ defaultValue: 1);
+
+ migrationBuilder.AddColumn(
+ name: "LevelId1",
+ table: "Lessons",
+ type: "integer",
+ nullable: true);
+
+ migrationBuilder.CreateTable(
+ name: "Quizzes",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ LessonId = table.Column(type: "integer", nullable: false),
+ Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true),
+ PassingScore = table.Column(type: "integer", nullable: false, defaultValue: 80),
+ TimeLimitMinutes = table.Column(type: "integer", nullable: false, defaultValue: 0),
+ IsActive = table.Column(type: "boolean", nullable: false, defaultValue: true),
+ ShuffleQuestions = table.Column(type: "boolean", nullable: false, defaultValue: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Quizzes", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Quizzes_Lessons_LessonId",
+ column: x => x.LessonId,
+ principalTable: "Lessons",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "StorySegments",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ LevelId = table.Column(type: "integer", nullable: false),
+ LessonId = table.Column(type: "integer", nullable: true),
+ Content = table.Column(type: "text", nullable: false),
+ AudioUrl = table.Column(type: "character varying(255)", maxLength: 255, nullable: true),
+ Order = table.Column(type: "integer", nullable: false),
+ Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Theme = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ EstimatedReadingMinutes = table.Column(type: "integer", nullable: false, defaultValue: 2),
+ IsActive = table.Column(type: "boolean", nullable: false, defaultValue: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ LessonId1 = table.Column(type: "integer", nullable: true),
+ LevelId1 = table.Column(type: "integer", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_StorySegments", x => x.Id);
+ table.ForeignKey(
+ name: "FK_StorySegments_Lessons_LessonId",
+ column: x => x.LessonId,
+ principalTable: "Lessons",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_StorySegments_Lessons_LessonId1",
+ column: x => x.LessonId1,
+ principalTable: "Lessons",
+ principalColumn: "Id");
+ table.ForeignKey(
+ name: "FK_StorySegments_Levels_LevelId",
+ column: x => x.LevelId,
+ principalTable: "Levels",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_StorySegments_Levels_LevelId1",
+ column: x => x.LevelId1,
+ principalTable: "Levels",
+ principalColumn: "Id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "StoryProgress",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ UserId = table.Column(type: "integer", nullable: false),
+ LevelId = table.Column(type: "integer", nullable: false),
+ StorySegmentId = table.Column(type: "integer", nullable: false),
+ IsCompleted = table.Column(type: "boolean", nullable: false, defaultValue: false),
+ UnlockedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ CompletedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ UserId1 = table.Column(type: "integer", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_StoryProgress", x => x.Id);
+ table.ForeignKey(
+ name: "FK_StoryProgress_Levels_LevelId",
+ column: x => x.LevelId,
+ principalTable: "Levels",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_StoryProgress_StorySegments_StorySegmentId",
+ column: x => x.StorySegmentId,
+ principalTable: "StorySegments",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_StoryProgress_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_StoryProgress_Users_UserId1",
+ column: x => x.UserId1,
+ principalTable: "Users",
+ principalColumn: "Id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Lessons_LevelId1",
+ table: "Lessons",
+ column: "LevelId1");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Quizzes_LessonId",
+ table: "Quizzes",
+ column: "LessonId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_StoryProgress_LevelId",
+ table: "StoryProgress",
+ column: "LevelId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_StoryProgress_StorySegmentId",
+ table: "StoryProgress",
+ column: "StorySegmentId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_StoryProgress_UserId_StorySegmentId",
+ table: "StoryProgress",
+ columns: new[] { "UserId", "StorySegmentId" },
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_StoryProgress_UserId1",
+ table: "StoryProgress",
+ column: "UserId1");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_StorySegments_LessonId",
+ table: "StorySegments",
+ column: "LessonId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_StorySegments_LessonId1",
+ table: "StorySegments",
+ column: "LessonId1");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_StorySegments_LevelId_Order",
+ table: "StorySegments",
+ columns: new[] { "LevelId", "Order" },
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_StorySegments_LevelId1",
+ table: "StorySegments",
+ column: "LevelId1");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Lessons_Levels_LevelId1",
+ table: "Lessons",
+ column: "LevelId1",
+ principalTable: "Levels",
+ principalColumn: "Id");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_QuizQuestions_Quizzes_QuizId",
+ table: "QuizQuestions",
+ column: "QuizId",
+ principalTable: "Quizzes",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Lessons_Levels_LevelId1",
+ table: "Lessons");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_QuizQuestions_Quizzes_QuizId",
+ table: "QuizQuestions");
+
+ migrationBuilder.DropTable(
+ name: "Quizzes");
+
+ migrationBuilder.DropTable(
+ name: "StoryProgress");
+
+ migrationBuilder.DropTable(
+ name: "StorySegments");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Lessons_LevelId1",
+ table: "Lessons");
+
+ migrationBuilder.DropColumn(
+ name: "Points",
+ table: "QuizQuestions");
+
+ migrationBuilder.DropColumn(
+ name: "LevelId1",
+ table: "Lessons");
+
+ migrationBuilder.RenameColumn(
+ name: "QuizId",
+ table: "QuizQuestions",
+ newName: "LessonId");
+
+ migrationBuilder.RenameIndex(
+ name: "IX_QuizQuestions_QuizId_Order",
+ table: "QuizQuestions",
+ newName: "IX_QuizQuestions_LessonId_Order");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_QuizQuestions_Lessons_LessonId",
+ table: "QuizQuestions",
+ column: "LessonId",
+ principalTable: "Lessons",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs b/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs
index 3da8976..ef11d61 100644
--- a/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs
+++ b/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs
@@ -46,6 +46,9 @@ namespace GermanApp.Migrations
b.Property("LevelId")
.HasColumnType("integer");
+ b.Property("LevelId1")
+ .HasColumnType("integer");
+
b.Property("Order")
.HasColumnType("integer");
@@ -64,6 +67,8 @@ namespace GermanApp.Migrations
b.HasKey("Id");
+ b.HasIndex("LevelId1");
+
b.HasIndex("LevelId", "Order")
.IsUnique();
@@ -102,6 +107,59 @@ namespace GermanApp.Migrations
b.ToTable("Levels");
});
+ modelBuilder.Entity("GermanApp.Domain.Entities.Quiz", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("LessonId")
+ .HasColumnType("integer");
+
+ b.Property("PassingScore")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(80);
+
+ b.Property("ShuffleQuestions")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("TimeLimitMinutes")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LessonId");
+
+ b.ToTable("Quizzes");
+ });
+
modelBuilder.Entity("GermanApp.Domain.Entities.QuizOption", b =>
{
b.Property("Id")
@@ -161,19 +219,24 @@ namespace GermanApp.Migrations
.HasColumnType("boolean")
.HasDefaultValue(true);
- b.Property("LessonId")
- .HasColumnType("integer");
-
b.Property("Order")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
+ b.Property("Points")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
b.Property("QuestionText")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
+ b.Property("QuizId")
+ .HasColumnType("integer");
+
b.Property("Type")
.HasColumnType("integer");
@@ -182,7 +245,7 @@ namespace GermanApp.Migrations
b.HasKey("Id");
- b.HasIndex("LessonId", "Order")
+ b.HasIndex("QuizId", "Order")
.IsUnique();
b.ToTable("QuizQuestions");
@@ -225,6 +288,128 @@ namespace GermanApp.Migrations
b.ToTable("RefreshTokens");
});
+ modelBuilder.Entity("GermanApp.Domain.Entities.StoryProgress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CompletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsCompleted")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("LevelId")
+ .HasColumnType("integer");
+
+ b.Property("StorySegmentId")
+ .HasColumnType("integer");
+
+ b.Property("UnlockedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("integer");
+
+ b.Property("UserId1")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LevelId");
+
+ b.HasIndex("StorySegmentId");
+
+ b.HasIndex("UserId1");
+
+ b.HasIndex("UserId", "StorySegmentId")
+ .IsUnique();
+
+ b.ToTable("StoryProgress");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.StorySegment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AudioUrl")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EstimatedReadingMinutes")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(2);
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("LessonId")
+ .HasColumnType("integer");
+
+ b.Property("LessonId1")
+ .HasColumnType("integer");
+
+ b.Property("LevelId")
+ .HasColumnType("integer");
+
+ b.Property("LevelId1")
+ .HasColumnType("integer");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.Property("Theme")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LessonId");
+
+ b.HasIndex("LessonId1");
+
+ b.HasIndex("LevelId1");
+
+ b.HasIndex("LevelId", "Order")
+ .IsUnique();
+
+ b.ToTable("StorySegments");
+ });
+
modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
{
b.Property("Id")
@@ -324,9 +509,24 @@ namespace GermanApp.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
+ b.HasOne("GermanApp.Domain.Entities.Level", null)
+ .WithMany("Lessons")
+ .HasForeignKey("LevelId1");
+
b.Navigation("Level");
});
+ modelBuilder.Entity("GermanApp.Domain.Entities.Quiz", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
+ .WithMany()
+ .HasForeignKey("LessonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Lesson");
+ });
+
modelBuilder.Entity("GermanApp.Domain.Entities.QuizOption", b =>
{
b.HasOne("GermanApp.Domain.Entities.QuizQuestion", "QuizQuestion")
@@ -340,13 +540,13 @@ namespace GermanApp.Migrations
modelBuilder.Entity("GermanApp.Domain.Entities.QuizQuestion", b =>
{
- b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
- .WithMany()
- .HasForeignKey("LessonId")
+ b.HasOne("GermanApp.Domain.Entities.Quiz", "Quiz")
+ .WithMany("Questions")
+ .HasForeignKey("QuizId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
- b.Navigation("Lesson");
+ b.Navigation("Quiz");
});
modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
@@ -358,6 +558,63 @@ namespace GermanApp.Migrations
.IsRequired();
});
+ modelBuilder.Entity("GermanApp.Domain.Entities.StoryProgress", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Level", "Level")
+ .WithMany()
+ .HasForeignKey("LevelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.StorySegment", "StorySegment")
+ .WithMany()
+ .HasForeignKey("StorySegmentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.User", null)
+ .WithMany("StoryProgress")
+ .HasForeignKey("UserId1");
+
+ b.Navigation("Level");
+
+ b.Navigation("StorySegment");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.StorySegment", b =>
+ {
+ b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
+ .WithMany()
+ .HasForeignKey("LessonId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GermanApp.Domain.Entities.Lesson", null)
+ .WithMany("StorySegments")
+ .HasForeignKey("LessonId1");
+
+ b.HasOne("GermanApp.Domain.Entities.Level", "Level")
+ .WithMany()
+ .HasForeignKey("LevelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GermanApp.Domain.Entities.Level", null)
+ .WithMany("StorySegments")
+ .HasForeignKey("LevelId1");
+
+ b.Navigation("Lesson");
+
+ b.Navigation("Level");
+ });
+
modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b =>
{
b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
@@ -377,10 +634,32 @@ namespace GermanApp.Migrations
b.Navigation("User");
});
+ modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
+ {
+ b.Navigation("StorySegments");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Level", b =>
+ {
+ b.Navigation("Lessons");
+
+ b.Navigation("StorySegments");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Quiz", b =>
+ {
+ b.Navigation("Questions");
+ });
+
modelBuilder.Entity("GermanApp.Domain.Entities.QuizQuestion", b =>
{
b.Navigation("Options");
});
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
+ {
+ b.Navigation("StoryProgress");
+ });
#pragma warning restore 612, 618
}
}
diff --git a/GermanApp/Infrastructure/Services/MistralConnector.cs b/GermanApp/Infrastructure/Services/MistralConnector.cs
index fd3548b..adffff2 100644
--- a/GermanApp/Infrastructure/Services/MistralConnector.cs
+++ b/GermanApp/Infrastructure/Services/MistralConnector.cs
@@ -30,23 +30,29 @@ public class MistralConnector : IMistralConnector
_config = config;
_logger = logger;
_cache = cache;
- _config.Validate();
-
- _httpClient.BaseAddress = new Uri(_config.BaseUrl);
- _httpClient.Timeout = TimeSpan.FromSeconds(_config.TimeoutSeconds);
- _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_config.ApiKey}");
- _httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
-
- _jsonOptions = new JsonSerializerOptions
+
+ // Skip validation and initialization during EF migrations
+ bool isEfDesignTime = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_EF") == "true";
+ if (!isEfDesignTime)
{
- PropertyNameCaseInsensitive = true,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- };
+ _config.Validate();
- _rateLimiter = new MistralRateLimiter(_config.RateLimitPerMinute);
- _circuitBreaker = new MistralCircuitBreaker(
- _config.CircuitBreakerFailureThreshold,
- TimeSpan.FromMinutes(_config.CircuitBreakerResetMinutes));
+ _httpClient.BaseAddress = new Uri(_config.BaseUrl);
+ _httpClient.Timeout = TimeSpan.FromSeconds(_config.TimeoutSeconds);
+ _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_config.ApiKey}");
+ _httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
+
+ _jsonOptions = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ _rateLimiter = new MistralRateLimiter(_config.RateLimitPerMinute);
+ _circuitBreaker = new MistralCircuitBreaker(
+ _config.CircuitBreakerFailureThreshold,
+ TimeSpan.FromMinutes(_config.CircuitBreakerResetMinutes));
+ }
}
public async Task CompleteAsync(MistralRequest request,
diff --git a/GermanApp/Infrastructure/Services/TtsService.cs b/GermanApp/Infrastructure/Services/TtsService.cs
index f72a087..222770c 100644
--- a/GermanApp/Infrastructure/Services/TtsService.cs
+++ b/GermanApp/Infrastructure/Services/TtsService.cs
@@ -25,11 +25,14 @@ public class TtsService : ITtsService
_config = config.Value;
_logger = logger;
- // Validate configuration
- ValidateConfiguration();
-
- // Ensure audio storage directory exists
- Directory.CreateDirectory(_config.AudioStoragePath);
+ // Skip validation during EF migrations
+ bool isEfDesignTime = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_EF") == "true";
+ if (!isEfDesignTime)
+ {
+ ValidateConfiguration();
+ // Ensure audio storage directory exists
+ Directory.CreateDirectory(_config.AudioStoragePath);
+ }
}
///
diff --git a/GermanApp/Infrastructure/Services/VoskService.cs b/GermanApp/Infrastructure/Services/VoskService.cs
index 3e960b6..1f088a3 100644
--- a/GermanApp/Infrastructure/Services/VoskService.cs
+++ b/GermanApp/Infrastructure/Services/VoskService.cs
@@ -25,8 +25,12 @@ public class VoskService : IVoskService
_config = config.Value;
_logger = logger;
- // Validate model path on startup
- ValidateModelPath();
+ // Skip validation during EF migrations
+ bool isEfDesignTime = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_EF") == "true";
+ if (!isEfDesignTime)
+ {
+ ValidateModelPath();
+ }
}
///
diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs
index a6b2ef2..841bb3e 100644
--- a/GermanApp/Program.cs
+++ b/GermanApp/Program.cs
@@ -173,7 +173,14 @@ try
builder.Services.Configure(builder.Configuration.GetSection("Coqui"));
// Validate AI service configurations
- ValidateAiConfigurations(builder.Configuration);
+ try
+ {
+ ValidateAiConfigurations(builder.Configuration);
+ }
+ catch (Exception)
+ {
+ // Skip validation during EF migrations and design-time when configs may not be fully set
+ }
// Register AI services (Infrastructure implementations of Domain interfaces)
builder.Services.AddScoped();
@@ -220,7 +227,18 @@ try
var app = builder.Build();
// Create static file directories
- Directory.CreateDirectory(Path.Combine("wwwroot", "audio", "story"));
+ bool isEfDesignTime = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_EF") == "true";
+ if (!isEfDesignTime)
+ {
+ try
+ {
+ Directory.CreateDirectory(Path.Combine("wwwroot", "audio", "story"));
+ }
+ catch
+ {
+ // Skip during migrations
+ }
+ }
// Configure the HTTP request pipeline.
@@ -296,6 +314,12 @@ try
// Helper method to validate AI service configurations
static void ValidateAiConfigurations(IConfiguration configuration)
{
+ // Skip validation during EF migrations
+ if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_EF") == "true")
+ {
+ return;
+ }
+
// Validate Mistral configuration
var mistralConfig = configuration.GetSection("Mistral").Get() ?? new MistralConfig();
mistralConfig.Validate();
diff --git a/docs/features/story-integration.md b/docs/features/story-integration.md
index f7e8426..8250bc6 100644
--- a/docs/features/story-integration.md
+++ b/docs/features/story-integration.md
@@ -1,7 +1,7 @@
# Feature: Story Integration
> **Status**: π In Progress
-> **π Current Progress**: Phase 1-5 β
Complete (Database & Models, Backend Services, Unit Tests, AI Integration, Audio Generation), Phase 6 Next (Frontend Integration)
+> **π Current Progress**: Phase 1-5 β
Complete (Database & Models, Backend Services, Unit Tests, AI Integration, Audio Generation + Migration), Phase 6 Next (Frontend Integration)
> **Priority**: High
> **Complexity**: High
> **Estimate**: 8-12 hours
@@ -139,7 +139,7 @@ Order: 1
- [x] Add DbSets to AppDbContext (StorySegments, StoryProgress)
- [x] Add entity configurations to AppDbContext
- [x] Add navigation properties to Level, Lesson, and User entities
-- [ ] Create and apply migration for StorySegments and StoryProgress tables
+- [x] Create and apply migration for StorySegments and StoryProgress tables
### Phase 2: Backend Services (2-3 hours)
- [x] Create StoryService with CRUD operations (Application/Services/StoryService.cs)
@@ -194,7 +194,7 @@ Order: 1
| Backend Services | June 13, 2025 | β
|
| Unit Tests | June 13, 2025 | β
|
| AI Integration | June 13, 2025 | β
|
-| Audio Generation | June 13, 2025 | β
|
+| Audio Generation & Migration | June 13, 2025 | β
|
| Frontend Integration | - | β³ |
| User Progress | - | β³ |