diff --git a/Rayson.CV/.env.production b/Rayson.CV/.env.production index 41c0374..47988cd 100644 --- a/Rayson.CV/.env.production +++ b/Rayson.CV/.env.production @@ -1,3 +1,2 @@ -POSTGRES_PASSWORD=change_me_to_a_secure_password JWT_SIGNING_KEY=change_me_to_a_secure_jwt_signing_key GRAPH_CLIENT_SECRET=take_your_best_guess_lol diff --git a/Rayson.CV/Api/Database/Database.csproj b/Rayson.CV/Api/Database/Database.csproj deleted file mode 100644 index 465613d..0000000 --- a/Rayson.CV/Api/Database/Database.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - net8.0 - enable - enable - - - diff --git a/Rayson.CV/Api/Database/Exceptions/DatabaseConfigurationException.cs b/Rayson.CV/Api/Database/Exceptions/DatabaseConfigurationException.cs deleted file mode 100644 index 3e12f9e..0000000 --- a/Rayson.CV/Api/Database/Exceptions/DatabaseConfigurationException.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Database.Exceptions; - -public class DatabaseConfigurationException(string message) - : InvalidOperationException(message) -{ -} diff --git a/Rayson.CV/Api/Database/Extensions/ServiceCollectionExtensions.cs b/Rayson.CV/Api/Database/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index bacf4d5..0000000 --- a/Rayson.CV/Api/Database/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Database.Exceptions; -using Database.HealthChecks; -using Database.SeedData; -using Domain.Repositories; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Database.Extensions; - -public static class ServiceCollectionExtensions -{ - public static void AddDatabaseServices(this IServiceCollection services, IConfiguration configuration) - { - var connectionString = BuildConnectionString(configuration); - - services.AddDbContext((options) => - { - options.UseNpgsql(connectionString); - }); - - services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); - services.AddScoped(); - - services.AddHealthChecks() - .AddCheck("postgresql", tags: new[] { "ready" }); - } - - private static string BuildConnectionString(IConfiguration configuration) - { - var host = configuration["POSTGRES_HOST"]; - var port = configuration["POSTGRES_PORT"]; - var username = configuration["POSTGRES_USERNAME"]; - var password = configuration["POSTGRES_PASSWORD"]; - var database = configuration["POSTGRES_DATABASE"]; - - if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port) || - string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || - string.IsNullOrEmpty(database)) - { - throw new DatabaseConfigurationException("Database connection string is missing. Please provide all PostgreSQL environment variables (POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USERNAME, POSTGRES_PASSWORD, POSTGRES_DATABASE)."); - } - - return $"Server={host};Port={port};Database={database};User Id={username};Password={password};TrustServerCertificate=True"; - } -} diff --git a/Rayson.CV/Api/Database/HealthChecks/NpgsqlHealthCheck.cs b/Rayson.CV/Api/Database/HealthChecks/NpgsqlHealthCheck.cs deleted file mode 100644 index 223586e..0000000 --- a/Rayson.CV/Api/Database/HealthChecks/NpgsqlHealthCheck.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Database.HealthChecks; - -public class NpgsqlHealthCheck : IHealthCheck -{ - private readonly RaysonCVDbContext _context; - - public NpgsqlHealthCheck(RaysonCVDbContext context) - { - _context = context; - } - - public async Task CheckHealthAsync( - HealthCheckContext context, - CancellationToken cancellationToken = default) - { - try - { - await _context.Database.ExecuteSqlRawAsync("SELECT 1", cancellationToken); - return HealthCheckResult.Healthy("PostgreSQL connection is healthy"); - } - catch (Exception ex) - { - return HealthCheckResult.Unhealthy("PostgreSQL connection failed", ex); - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260225083042_Init.Designer.cs b/Rayson.CV/Api/Database/Migrations/20260225083042_Init.Designer.cs deleted file mode 100644 index 2be7ccc..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260225083042_Init.Designer.cs +++ /dev/null @@ -1,303 +0,0 @@ -// -using System; -using 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 Database.Migrations -{ - [DbContext(typeof(RaysonCVDbContext))] - [Migration("20260225083042_Init")] - partial class Init - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Database.Auth.ApplicationRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Database.Auth.ApplicationUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("DisplayName") - .IsRequired() - .HasColumnType("text"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("PasswordHash") - .HasColumnType("text"); - - b.Property("PhoneNumber") - .HasColumnType("text"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("boolean"); - - b.Property("SecurityStamp") - .HasColumnType("text"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("FriendlyName") - .HasColumnType("text"); - - b.Property("Xml") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("DataProtectionKeys"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("ProviderKey") - .HasColumnType("text"); - - b.Property("ProviderDisplayName") - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("integer"); - - b.Property("RoleId") - .HasColumnType("integer"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("integer"); - - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Database.Auth.ApplicationRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("Database.Auth.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Database.Auth.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Database.Auth.ApplicationRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Database.Auth.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Database.Auth.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260225083042_Init.cs b/Rayson.CV/Api/Database/Migrations/20260225083042_Init.cs deleted file mode 100644 index 25f8c51..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260225083042_Init.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Database.Migrations -{ - /// - public partial class Init : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AspNetRoles", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - ConcurrencyStamp = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetRoles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AspNetUsers", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - DisplayName = table.Column(type: "text", nullable: false), - UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - EmailConfirmed = table.Column(type: "boolean", nullable: false), - PasswordHash = table.Column(type: "text", nullable: true), - SecurityStamp = table.Column(type: "text", nullable: true), - ConcurrencyStamp = table.Column(type: "text", nullable: true), - PhoneNumber = table.Column(type: "text", nullable: true), - PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), - TwoFactorEnabled = table.Column(type: "boolean", nullable: false), - LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), - LockoutEnabled = table.Column(type: "boolean", nullable: false), - AccessFailedCount = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUsers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "DataProtectionKeys", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - FriendlyName = table.Column(type: "text", nullable: true), - Xml = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_DataProtectionKeys", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AspNetRoleClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - RoleId = table.Column(type: "integer", nullable: false), - ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); - table.ForeignKey( - name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", - column: x => x.RoleId, - principalTable: "AspNetRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "integer", nullable: false), - ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); - table.ForeignKey( - name: "FK_AspNetUserClaims_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserLogins", - columns: table => new - { - LoginProvider = table.Column(type: "text", nullable: false), - ProviderKey = table.Column(type: "text", nullable: false), - ProviderDisplayName = table.Column(type: "text", nullable: true), - UserId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); - table.ForeignKey( - name: "FK_AspNetUserLogins_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserRoles", - columns: table => new - { - UserId = table.Column(type: "integer", nullable: false), - RoleId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); - table.ForeignKey( - name: "FK_AspNetUserRoles_AspNetRoles_RoleId", - column: x => x.RoleId, - principalTable: "AspNetRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AspNetUserRoles_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserTokens", - columns: table => new - { - UserId = table.Column(type: "integer", nullable: false), - LoginProvider = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: false), - Value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); - table.ForeignKey( - name: "FK_AspNetUserTokens_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AspNetRoleClaims_RoleId", - table: "AspNetRoleClaims", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "AspNetRoles", - column: "NormalizedName", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserClaims_UserId", - table: "AspNetUserClaims", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserLogins_UserId", - table: "AspNetUserLogins", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserRoles_RoleId", - table: "AspNetUserRoles", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "AspNetUsers", - column: "NormalizedEmail"); - - migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "AspNetUsers", - column: "NormalizedUserName", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AspNetRoleClaims"); - - migrationBuilder.DropTable( - name: "AspNetUserClaims"); - - migrationBuilder.DropTable( - name: "AspNetUserLogins"); - - migrationBuilder.DropTable( - name: "AspNetUserRoles"); - - migrationBuilder.DropTable( - name: "AspNetUserTokens"); - - migrationBuilder.DropTable( - name: "DataProtectionKeys"); - - migrationBuilder.DropTable( - name: "AspNetRoles"); - - migrationBuilder.DropTable( - name: "AspNetUsers"); - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260225093106_RemoveIdentity.Designer.cs b/Rayson.CV/Api/Database/Migrations/20260225093106_RemoveIdentity.Designer.cs deleted file mode 100644 index 069a460..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260225093106_RemoveIdentity.Designer.cs +++ /dev/null @@ -1,29 +0,0 @@ -// -using 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 Database.Migrations -{ - [DbContext(typeof(RaysonCVDbContext))] - [Migration("20260225093106_RemoveIdentity")] - partial class RemoveIdentity - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); -#pragma warning restore 612, 618 - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260225093106_RemoveIdentity.cs b/Rayson.CV/Api/Database/Migrations/20260225093106_RemoveIdentity.cs deleted file mode 100644 index f4f2d81..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260225093106_RemoveIdentity.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Database.Migrations -{ - /// - public partial class RemoveIdentity : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AspNetRoleClaims"); - - migrationBuilder.DropTable( - name: "AspNetUserClaims"); - - migrationBuilder.DropTable( - name: "AspNetUserLogins"); - - migrationBuilder.DropTable( - name: "AspNetUserRoles"); - - migrationBuilder.DropTable( - name: "AspNetUserTokens"); - - migrationBuilder.DropTable( - name: "DataProtectionKeys"); - - migrationBuilder.DropTable( - name: "AspNetRoles"); - - migrationBuilder.DropTable( - name: "AspNetUsers"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AspNetRoles", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ConcurrencyStamp = table.Column(type: "text", nullable: true), - Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetRoles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AspNetUsers", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - AccessFailedCount = table.Column(type: "integer", nullable: false), - ConcurrencyStamp = table.Column(type: "text", nullable: true), - DisplayName = table.Column(type: "text", nullable: false), - Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - EmailConfirmed = table.Column(type: "boolean", nullable: false), - LockoutEnabled = table.Column(type: "boolean", nullable: false), - LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), - NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - PasswordHash = table.Column(type: "text", nullable: true), - PhoneNumber = table.Column(type: "text", nullable: true), - PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), - SecurityStamp = table.Column(type: "text", nullable: true), - TwoFactorEnabled = table.Column(type: "boolean", nullable: false), - UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUsers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "DataProtectionKeys", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - FriendlyName = table.Column(type: "text", nullable: true), - Xml = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_DataProtectionKeys", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AspNetRoleClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true), - RoleId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); - table.ForeignKey( - name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", - column: x => x.RoleId, - principalTable: "AspNetRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true), - UserId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); - table.ForeignKey( - name: "FK_AspNetUserClaims_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserLogins", - columns: table => new - { - LoginProvider = table.Column(type: "text", nullable: false), - ProviderKey = table.Column(type: "text", nullable: false), - ProviderDisplayName = table.Column(type: "text", nullable: true), - UserId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); - table.ForeignKey( - name: "FK_AspNetUserLogins_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserRoles", - columns: table => new - { - UserId = table.Column(type: "integer", nullable: false), - RoleId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); - table.ForeignKey( - name: "FK_AspNetUserRoles_AspNetRoles_RoleId", - column: x => x.RoleId, - principalTable: "AspNetRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AspNetUserRoles_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserTokens", - columns: table => new - { - UserId = table.Column(type: "integer", nullable: false), - LoginProvider = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: false), - Value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); - table.ForeignKey( - name: "FK_AspNetUserTokens_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AspNetRoleClaims_RoleId", - table: "AspNetRoleClaims", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "AspNetRoles", - column: "NormalizedName", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserClaims_UserId", - table: "AspNetUserClaims", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserLogins_UserId", - table: "AspNetUserLogins", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserRoles_RoleId", - table: "AspNetUserRoles", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "AspNetUsers", - column: "NormalizedEmail"); - - migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "AspNetUsers", - column: "NormalizedUserName", - unique: true); - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260227205757_AddCvChunks.Designer.cs b/Rayson.CV/Api/Database/Migrations/20260227205757_AddCvChunks.Designer.cs deleted file mode 100644 index f630e7d..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260227205757_AddCvChunks.Designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// -using System; -using 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 Database.Migrations -{ - [DbContext(typeof(RaysonCVDbContext))] - [Migration("20260227205757_AddCvChunks")] - partial class AddCvChunks - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Domain.CvChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChunkIndex") - .HasColumnType("integer"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Embedding") - .IsRequired() - .HasColumnType("double precision[]"); - - b.Property("Section") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("CvChunks"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260227205757_AddCvChunks.cs b/Rayson.CV/Api/Database/Migrations/20260227205757_AddCvChunks.cs deleted file mode 100644 index 941a8e3..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260227205757_AddCvChunks.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Database.Migrations -{ - /// - public partial class AddCvChunks : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS vector"); - - migrationBuilder.CreateTable( - name: "CvChunks", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Content = table.Column(type: "text", nullable: false), - Section = table.Column(type: "text", nullable: false), - Embedding = table.Column(type: "double precision[]", nullable: false), - ChunkIndex = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CvChunks", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "CvChunks"); - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260228000000_FixCvChunksVectorType.Designer.cs b/Rayson.CV/Api/Database/Migrations/20260228000000_FixCvChunksVectorType.Designer.cs deleted file mode 100644 index be67098..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260228000000_FixCvChunksVectorType.Designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// -using System; -using 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 Database.Migrations -{ - [DbContext(typeof(RaysonCVDbContext))] - [Migration("20260228000000_FixCvChunksVectorType")] - partial class FixCvChunksVectorType - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Domain.CvChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChunkIndex") - .HasColumnType("integer"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Embedding") - .IsRequired() - .HasColumnType("vector(768)"); - - b.Property("Section") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("CvChunks"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260228000000_FixCvChunksVectorType.cs b/Rayson.CV/Api/Database/Migrations/20260228000000_FixCvChunksVectorType.cs deleted file mode 100644 index b3a619f..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260228000000_FixCvChunksVectorType.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Database.Migrations -{ - /// - public partial class FixCvChunksVectorType : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS vector"); - - migrationBuilder.DropTable(name: "CvChunks"); - - migrationBuilder.CreateTable( - name: "CvChunks", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Content = table.Column(type: "text", nullable: false), - Section = table.Column(type: "text", nullable: false), - Embedding = table.Column(type: "vector(768)", nullable: false), - ChunkIndex = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CvChunks", x => x.Id); - }); - - migrationBuilder.Sql(@" - CREATE INDEX IF NOT EXISTS cvchunks_embedding_idx - ON ""CvChunks"" - USING ivfflat (""Embedding"" vector_cosine_ops) - WITH (lists = 100)"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "CvChunks"); - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260317000000_ClearCvChunks.Designer.cs b/Rayson.CV/Api/Database/Migrations/20260317000000_ClearCvChunks.Designer.cs deleted file mode 100644 index a6e7e5e..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260317000000_ClearCvChunks.Designer.cs +++ /dev/null @@ -1,20 +0,0 @@ -// -using Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Database.Migrations -{ - [DbContext(typeof(RaysonCVDbContext))] - [Migration("20260317000000_ClearCvChunks")] - partial class ClearCvChunks - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/20260317000000_ClearCvChunks.cs b/Rayson.CV/Api/Database/Migrations/20260317000000_ClearCvChunks.cs deleted file mode 100644 index 26b505f..0000000 --- a/Rayson.CV/Api/Database/Migrations/20260317000000_ClearCvChunks.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Database.Migrations -{ - /// - public partial class ClearCvChunks : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql("TRUNCATE TABLE \"CvChunks\" CASCADE"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - } - } -} diff --git a/Rayson.CV/Api/Database/Migrations/RaysonCVDbContextModelSnapshot.cs b/Rayson.CV/Api/Database/Migrations/RaysonCVDbContextModelSnapshot.cs deleted file mode 100644 index 7c92b30..0000000 --- a/Rayson.CV/Api/Database/Migrations/RaysonCVDbContextModelSnapshot.cs +++ /dev/null @@ -1,61 +0,0 @@ -// -using System; -using Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Database.Migrations -{ - [DbContext(typeof(RaysonCVDbContext))] - partial class RaysonCVDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Domain.CvChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChunkIndex") - .HasColumnType("integer"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Embedding") - .IsRequired() - .HasColumnType("vector(768)"); - - b.Property("Section") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("CvChunks"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Rayson.CV/Api/Database/RaysonDevDbContext.cs b/Rayson.CV/Api/Database/RaysonDevDbContext.cs deleted file mode 100644 index 4cbe25b..0000000 --- a/Rayson.CV/Api/Database/RaysonDevDbContext.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Globalization; -using Microsoft.EntityFrameworkCore; -using Domain; - -namespace Database; - -public class RaysonCVDbContext : DbContext -{ - public RaysonCVDbContext() - : base() { } - - public RaysonCVDbContext(DbContextOptions options) - : base(options) { } - - public DbSet CvChunks { get; set; } = null!; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseNpgsql(); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity(entity => - { - entity.Property(e => e.Embedding) - .HasColumnType("vector(768)") - .HasConversion( - v => "[" + string.Join(",", v.Select(e => e.ToString("G10", CultureInfo.InvariantCulture))) + "]", - v => v.Trim('[', ']').Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(double.Parse) - .ToArray() - ); - }); - } -} \ No newline at end of file diff --git a/Rayson.CV/Api/Database/Repository.cs b/Rayson.CV/Api/Database/Repository.cs deleted file mode 100644 index 1f01465..0000000 --- a/Rayson.CV/Api/Database/Repository.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Domain; -using Domain.Repositories; -using Microsoft.EntityFrameworkCore; - -namespace Database; - -public class Repository(RaysonCVDbContext context) : IRepository where T : Entity -{ - protected readonly DbContext _context = context; - protected readonly DbSet _dbSet = context.Set(); - - public IQueryable Query() - { - return _context.Set().AsQueryable(); - } - - public async Task AddAsync(T entity) - { - await _context.Set().AddAsync(entity); - await _context.SaveChangesAsync(); - } - - public async Task UpdateAsync(T entity) - { - _context.Entry(entity).State = EntityState.Modified; - await _context.SaveChangesAsync(); - } - - public async Task SoftDeleteAsync(T entity) - { - entity.DeletedAt = DateTime.UtcNow; - _context.Entry(entity).State = EntityState.Modified; - await _context.SaveChangesAsync(); - } - - public async Task HardDeleteAsync(T entity) - { - _context.Set().Remove(entity); - await _context.SaveChangesAsync(); - } - - public async Task> GetListAsync(IQueryable? predicate = null, bool includeDeleted = false) - { - IQueryable query = (predicate ?? Query()).Where(e => includeDeleted || e.DeletedAt == null); - return await query.ToListAsync(); - } - - public async Task GetByIdAsync(int id) - { - return await _context.Set().FindAsync(id); - } - - public async Task AddManyAsync(T[] entities) - { - foreach (var entity in entities) - { - await _context.Set().AddAsync(entity); - } - await _context.SaveChangesAsync(); - } - - public async Task UpdateManyAsync(T[] entities) - { - foreach (var entity in entities) - { - _context.Entry(entity).State = EntityState.Modified; - } - await _context.SaveChangesAsync(); - } - - public async Task SoftDeleteManyAsync(T[] entities) - { - foreach (var entity in entities) - { - entity.DeletedAt = DateTime.UtcNow; - _context.Entry(entity).State = EntityState.Modified; - } - await _context.SaveChangesAsync(); - } - - public async Task HardDeleteManyAsync(T[] entities) - { - foreach (var entity in entities) - { - _context.Set().Remove(entity); - } - await _context.SaveChangesAsync(); - } - public async Task> GetByIdManyAsync(int[] ids) - { - return await _context.Set().Where(e => ids.Contains(e.Id)).ToListAsync(); - } -} diff --git a/Rayson.CV/Api/Database/SeedData/DataSeeder.cs b/Rayson.CV/Api/Database/SeedData/DataSeeder.cs deleted file mode 100644 index fe14120..0000000 --- a/Rayson.CV/Api/Database/SeedData/DataSeeder.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Database.SeedData; - -public class DataSeeder : IDataSeeder -{ - public Task SeedDatabaseAsync() - { - return Task.CompletedTask; - } -} diff --git a/Rayson.CV/Api/Database/SeedData/IDataSeeder.cs b/Rayson.CV/Api/Database/SeedData/IDataSeeder.cs deleted file mode 100644 index 2ae1700..0000000 --- a/Rayson.CV/Api/Database/SeedData/IDataSeeder.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Database.SeedData; - -public interface IDataSeeder -{ - public Task SeedDatabaseAsync(); -} diff --git a/Rayson.CV/Api/Dockerfile b/Rayson.CV/Api/Dockerfile index aec5763..9663e31 100644 --- a/Rayson.CV/Api/Dockerfile +++ b/Rayson.CV/Api/Dockerfile @@ -13,7 +13,6 @@ COPY ["Presentation/Presentation.csproj", "Presentation/"] COPY ["Application/Application.csproj", "Application/"] COPY ["Domain/Domain.csproj", "Domain/"] COPY ["Infrastructure/Infrastructure.csproj", "Infrastructure/"] -COPY ["Database/Database.csproj", "Database/"] # Restore NuGet packages based on project references RUN dotnet restore "Presentation/Presentation.csproj" @@ -60,11 +59,6 @@ ENV ASPNETCORE_URLS=http://+:8080 ENV AuthOptions__Issuer=danrayson ENV AuthOptions__Audience=RaysonCVUsers ENV AuthOptions__IssuerSigningKey=development_signing_key_for_testing_only_not_for_production -ENV POSTGRES_HOST=host.docker.internal -ENV POSTGRES_PORT=5433 -ENV POSTGRES_DATABASE=raysoncv -ENV POSTGRES_USERNAME=postgres -ENV POSTGRES_PASSWORD=postgres ENV Cors__AllowedOrigins=http://localhost:3000 ENV OLLAMA__BASEURL=http://ollama:11434 diff --git a/Rayson.CV/Api/Domain/CvChunk.cs b/Rayson.CV/Api/Domain/CvChunk.cs deleted file mode 100644 index 4ad5b9e..0000000 --- a/Rayson.CV/Api/Domain/CvChunk.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Domain; - -public class CvChunk : Entity -{ - public required string Content { get; set; } - public required string Section { get; set; } - public required double[] Embedding { get; set; } - public int ChunkIndex { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; -} diff --git a/Rayson.CV/Api/Domain/IRepository.cs b/Rayson.CV/Api/Domain/IRepository.cs deleted file mode 100644 index b9ce800..0000000 --- a/Rayson.CV/Api/Domain/IRepository.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Domain.Repositories; - -public interface IRepository where T : Entity -{ - IQueryable Query(); - Task AddAsync(T entity); - Task AddManyAsync(T[] entities); - Task UpdateAsync(T entity); - Task UpdateManyAsync(T[] entities); - Task SoftDeleteAsync(T entity); - Task SoftDeleteManyAsync(T[] entities); - Task HardDeleteAsync(T entity); - Task HardDeleteManyAsync(T[] entities); - Task> GetListAsync(IQueryable? predicate = null, bool includeDeleted = false); - Task GetByIdAsync(int id); - Task> GetByIdManyAsync(int[] ids); -} \ No newline at end of file diff --git a/Rayson.CV/Api/Domain/Resources/cv.md b/Rayson.CV/Api/Domain/Resources/cv.md index cec993c..0f8581d 100644 --- a/Rayson.CV/Api/Domain/Resources/cv.md +++ b/Rayson.CV/Api/Domain/Resources/cv.md @@ -4,7 +4,7 @@ Senior DotNet Developer PERSONAL DETAILS -ADDRESS | 85 Bagots Oak, ST17 9SB +ADDRESS | Stafford, ST17 9SB TELEPHONE | 07703 574867 EMAIL | daniel@rayson.dev WEBSITE | www.rayson.dev @@ -35,27 +35,27 @@ Has worked with continuous integration frameworks such as Octopus, Jenkins and T WORK EXPERIENCE -Freelance (Personal and Brochure Websites) 2007 – 2012 +Freelance (Personal and Brochure Websites) 2007 - 2012 Title: Website Development and Support • Design and implementation of custom websites • General site maintenance and updates • Site specification and general IT consultation -DBLogic (Software House) Spring 2012 - Spring 2014 +DBLogic (Software House) 2012 - 2014 Title: Software Development Engineer • Full development life-cycle ownership • Customer support and software bug fixing • Experience with the SCRUM method • Multiple completed business applications -ARVS ltd (Car Accident Claims Management) Summer 2014 - Summer 2016 +ARVS ltd (Car Accident Claims Management) 2014 - 2016 Title: Web Developer • Supporting and maintaining a bespoke motor accident management website • Creating add-ons and new services in an agile environment • Advisor to junior members of staff and introducer of new technologies to the team • Consulted with end users to achieve the best results -Inspired Gaming UK Ltd (Video Streaming and Back-end Services) Winter 2017 – Winter 2021 +Inspired Gaming UK Ltd (Video Streaming and Back-end Services) 2017 – 2021 Title: Senior Software Developer • Development ownership of solo-projects • Supporting and advising the development team @@ -63,18 +63,21 @@ Title: Senior Software Developer • Unit, Behaviour, and Integration testing using various suites • Completed multiple projects through to production -The Site Doctor (Small and Highly Skilled Software House) Winter 2021 – Winter 2023 +The Site Doctor (Small and Highly Skilled Software House) 2021 – 2024 Title: Software Developer • Full project ownership, requirement gathering, work specification, deployment and support • Supporting and advising the development team • Developed B2B APIs deployed to the Cloud using CI • Responsible for communicating with clients and work team -Self Employed (Material Product Arbitrage) Winter 2023 – Current -Title: N/A - • Self-directed research and systems design - • Responsibility for following regulatory requirements - • Automation, data validation, service interfacing +Freelance 2024 - Current +Title: Senior Full-Stack Developer + • Followed ISO 29110 processes to ensure quality delivery + • Cloud and On-site hosting solutions with full CI/CD pipelines + • Full project lifecycle ownership from requirements through deployment + • Completed full-stack applications: arbitrage platform and custom e-commerce shop + • Contract and legal compliance considerations in project delivery + • Self-hosted portfolio website (www.rayson.dev) and additional personal projects HOME EXPERIENCE diff --git a/Rayson.CV/Api/Infrastructure/Chatbot/OllamaChatbotService.cs b/Rayson.CV/Api/Infrastructure/Chatbot/OllamaChatbotService.cs index a51ecd1..4a92e94 100644 --- a/Rayson.CV/Api/Infrastructure/Chatbot/OllamaChatbotService.cs +++ b/Rayson.CV/Api/Infrastructure/Chatbot/OllamaChatbotService.cs @@ -7,8 +7,7 @@ namespace Infrastructure.Chatbot; public class OllamaChatbotService( IOllamaService ollamaService, - ICvProvider cvProvider, - ILogger logger) : IChatbotService + ICvProvider cvProvider) : IChatbotService { private const string Model = "llama3.2:latest"; private const int MaxTokens = 35; // ~100 words diff --git a/Rayson.CV/Api/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/Rayson.CV/Api/Infrastructure/Extensions/ServiceCollectionExtensions.cs index fb03899..16f82f2 100644 --- a/Rayson.CV/Api/Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/Rayson.CV/Api/Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -6,9 +6,9 @@ using Infrastructure.Contact; using Infrastructure.Health; using Infrastructure.Logging; -using Infrastructure.RAG; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Rayson.Ollama.Extensions; namespace Infrastructure.Extensions; @@ -29,8 +29,7 @@ public static void AddInfrastructureServices(this IServiceCollection services, C services.AddOllama(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddHealthChecks() + .AddCheck("ollama", tags: new[] { "ready" }); } } diff --git a/Rayson.CV/Api/Infrastructure/Health/OllamaHealthCheck.cs b/Rayson.CV/Api/Infrastructure/Health/OllamaHealthCheck.cs new file mode 100644 index 0000000..b3b24c1 --- /dev/null +++ b/Rayson.CV/Api/Infrastructure/Health/OllamaHealthCheck.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Rayson.Ollama; + +namespace Infrastructure.Health; + +public class OllamaHealthCheck(IOllamaService ollamaService) : IHealthCheck +{ + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + try + { + var models = await ollamaService.ListModelsAsync(); + return HealthCheckResult.Healthy($"Ollama is running with {models.Count()} models available"); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("Ollama connection failed", ex); + } + } +} diff --git a/Rayson.CV/Api/Infrastructure/Infrastructure.csproj b/Rayson.CV/Api/Infrastructure/Infrastructure.csproj index 6773c8f..7e17a83 100644 --- a/Rayson.CV/Api/Infrastructure/Infrastructure.csproj +++ b/Rayson.CV/Api/Infrastructure/Infrastructure.csproj @@ -3,7 +3,6 @@ - diff --git a/Rayson.CV/Api/Infrastructure/Ollama/ollama.README.md b/Rayson.CV/Api/Infrastructure/Ollama/ollama.README.md index 52d6ca1..9398e83 100755 --- a/Rayson.CV/Api/Infrastructure/Ollama/ollama.README.md +++ b/Rayson.CV/Api/Infrastructure/Ollama/ollama.README.md @@ -57,12 +57,7 @@ await foreach (var chunk in ollama.ChatStreamAsync( Console.Write(chunk.Message?.Content); } -// Generate embeddings -var embedding = await ollama.EmbedAsync("Hello", "nomic-embed-text"); -Console.WriteLine(embedding.Embedding.Count); -``` - -## Prerequisites +// ## Prerequisites ### NVIDIA GPU Support @@ -107,11 +102,11 @@ docker compose ps ### Using Multiple Models -By default, the container starts with `smollm:135m` and `nomic-embed-text`. To configure models, edit the `CUSTOM_MODELS` environment variable in `ollama.docker-compose.yml`: +By default, the container starts with `smollm:135m`. To configure models, edit the `CUSTOM_MODELS` environment variable in `ollama.docker-compose.yml`: ```yaml environment: - - CUSTOM_MODELS=["smollm:135m","nomic-embed-text"] + - CUSTOM_MODELS=["smollm:135m"] ``` The `CUSTOM_MODELS` environment variable accepts a JSON array of model names. The entrypoint script will check if each model exists and pull it only if missing. @@ -124,7 +119,7 @@ The `CUSTOM_MODELS` environment variable accepts a JSON array of model names. Th | Environment Variable | Default | Description | |---------------------|---------|-------------| -| `CUSTOM_MODELS` | `["smollm:135m","nomic-embed-text"]` | Models to check/pull on startup | +| `CUSTOM_MODELS` | `["smollm:135m"]` | Models to check/pull on startup | > **Note:** The `OLLAMA_HOST` in the Ollama container is hardcoded to `0.0.0.0` so connections from other containers are accepted. This could raise security issues for your environment. diff --git a/Rayson.CV/Api/Infrastructure/Ollama/ollama.docker-compose.yml b/Rayson.CV/Api/Infrastructure/Ollama/ollama.docker-compose.yml index cb0690c..94f5256 100755 --- a/Rayson.CV/Api/Infrastructure/Ollama/ollama.docker-compose.yml +++ b/Rayson.CV/Api/Infrastructure/Ollama/ollama.docker-compose.yml @@ -4,10 +4,10 @@ services: context: . dockerfile: ollama.Dockerfile environment: - - CUSTOM_MODELS=["llama3.2:latest","nomic-embed-text"] + - CUSTOM_MODELS=["llama3.2:latest"] volumes: - ollama_data:/root/.ollama - container_name: ollama + container_name: raysoncv-ollama healthcheck: test: ["CMD", "/ollama.healthcheck.sh"] interval: 10s diff --git a/Rayson.CV/Api/Infrastructure/RAG/CvChunkRepository.cs b/Rayson.CV/Api/Infrastructure/RAG/CvChunkRepository.cs deleted file mode 100644 index 95ac2f9..0000000 --- a/Rayson.CV/Api/Infrastructure/RAG/CvChunkRepository.cs +++ /dev/null @@ -1,168 +0,0 @@ -using Database; -using Microsoft.EntityFrameworkCore; -using Npgsql; - -namespace Infrastructure.RAG; - -public class CvChunkRepository(RaysonCVDbContext context) : ICvChunkRepository -{ - private readonly RaysonCVDbContext _context = context; - private const double SectionBoostAmount = 0.15; - - private static readonly Dictionary SectionKeywords = new(StringComparer.OrdinalIgnoreCase) - { - ["PERSONAL DETAILS"] = ["personal", "details", "contact", "email", "phone", "address", "name"], - ["EDUCATION"] = ["education", "school", "university", "degree", "qualification", "college", "gcse", "a-level", "mathematics"], - ["PROFESSIONAL SKILLS SUMMARY"] = ["skill", "skills", "tech", "technology", "stack", "experience", "expertise", "language", "framework", "tool"], - ["WORK EXPERIENCE"] = ["work", "job", "employment", "career", "position", "role", "company", "employer", "professional"], - ["HOME EXPERIENCE"] = ["home", "personal", "project", "projects", "hobby", "interest", "side"], - ["PERSONAL DESCRIPTION"] = ["about", "description", "profile", "background", "summary", "character"], - ["References"] = ["reference", "references", "referee", "contact"] - }; - - public async Task AddRangeAsync(IEnumerable chunks) - { - foreach (var chunk in chunks) - { - var embeddingStr = string.Join(",", chunk.Embedding.Select(e => e.ToString("G10", System.Globalization.CultureInfo.InvariantCulture))); - var deletedAtStr = chunk.DeletedAt.HasValue - ? $"'{chunk.DeletedAt.Value:u}'" - : "NULL"; - - await _context.Database.ExecuteSqlRawAsync($@" - INSERT INTO ""CvChunks"" (""Content"", ""Section"", ""Embedding"", ""ChunkIndex"", ""CreatedAt"", ""DeletedAt"") - VALUES ('{chunk.Content.Replace("'", "''")}', '{chunk.Section.Replace("'", "''")}', '[{embeddingStr}]'::vector, {chunk.ChunkIndex}, '{chunk.CreatedAt:u}', {deletedAtStr})"); - } - } - - public async Task ExistsAsync() - { - return await _context.CvChunks.AnyAsync(); - } - - public async Task> GetMostSimilarAsync(float[] embedding, int topK) - { - return await GetMostSimilarAsync(embedding, topK, null); - } - - public async Task> GetMostSimilarAsync(float[] embedding, int topK, string? query) - { - var embeddingString = string.Join(",", embedding.Select(e => e.ToString("G10", System.Globalization.CultureInfo.InvariantCulture))); - - var (matchedSection, boost) = query != null - ? FindMatchingSection(query) - : (null, 0.0); - - string sql; - if (matchedSection != null && boost > 0) - { - sql = $@" - SELECT ""Content"" - FROM ""CvChunks"" - ORDER BY ""Embedding"" <-> '[{embeddingString}]'::vector + - (CASE WHEN ""Section"" = '{matchedSection.Replace("'", "''")}' THEN {boost.ToString("G10", System.Globalization.CultureInfo.InvariantCulture)} ELSE 0 END) - LIMIT {topK}"; - } - else - { - sql = $@" - SELECT ""Content"" - FROM ""CvChunks"" - ORDER BY ""Embedding"" <-> '[{embeddingString}]'::vector - LIMIT {topK}"; - } - - var result = await _context.Database - .SqlQueryRaw(sql) - .ToListAsync(); - - return result; - } - - private static (string? Section, double Boost) FindMatchingSection(string query) - { - var queryLower = query.ToLowerInvariant(); - var queryWords = queryLower.Split([' ', ',', '.', '!', '?', ';', ':', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries); - - if (queryWords.Length == 0) - return (null, 0); - - var sectionScores = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var (section, keywords) in SectionKeywords) - { - var score = 0.0; - - foreach (var queryWord in queryWords) - { - if (string.IsNullOrWhiteSpace(queryWord) || queryWord.Length < 3) - continue; - - foreach (var keyword in keywords) - { - var similarity = CalculateSimilarity(queryWord, keyword); - if (similarity > 0.6) - { - score += similarity; - } - else if (keyword.Contains(queryWord, StringComparison.OrdinalIgnoreCase) || - queryWord.Contains(keyword, StringComparison.OrdinalIgnoreCase)) - { - score += 0.5; - } - } - } - - if (score > 0) - { - sectionScores[section] = score; - } - } - - if (sectionScores.Count == 0) - return (null, 0); - - var bestSection = sectionScores.OrderByDescending(x => x.Value).First(); - - var maxPossibleScore = queryWords.Length * 1.0; - var normalizedBoost = Math.Min(bestSection.Value / maxPossibleScore, 1.0) * SectionBoostAmount; - - return (bestSection.Key, normalizedBoost); - } - - private static double CalculateSimilarity(string s1, string s2) - { - if (string.IsNullOrEmpty(s1) || string.IsNullOrEmpty(s2)) - return 0; - - var distance = LevenshteinDistance(s1, s2); - var maxLength = Math.Max(s1.Length, s2.Length); - - return 1.0 - (double)distance / maxLength; - } - - private static int LevenshteinDistance(string s1, string s2) - { - var m = s1.Length; - var n = s2.Length; - var d = new int[m + 1, n + 1]; - - for (var i = 0; i <= m; i++) - d[i, 0] = i; - for (var j = 0; j <= n; j++) - d[0, j] = j; - - for (var i = 1; i <= m; i++) - { - for (var j = 1; j <= n; j++) - { - var cost = s1[i - 1] == s2[j - 1] ? 0 : 1; - d[i, j] = Math.Min( - Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), - d[i - 1, j - 1] + cost); - } - } - - return d[m, n]; - } -} diff --git a/Rayson.CV/Api/Infrastructure/RAG/ICvChunkRepository.cs b/Rayson.CV/Api/Infrastructure/RAG/ICvChunkRepository.cs deleted file mode 100644 index 77ef9bd..0000000 --- a/Rayson.CV/Api/Infrastructure/RAG/ICvChunkRepository.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Infrastructure.RAG; - -public interface ICvChunkRepository -{ - Task AddRangeAsync(IEnumerable chunks); - Task ExistsAsync(); - Task> GetMostSimilarAsync(float[] embedding, int topK); - Task> GetMostSimilarAsync(float[] embedding, int topK, string? query); -} diff --git a/Rayson.CV/Api/Infrastructure/RAG/IEmbeddingService.cs b/Rayson.CV/Api/Infrastructure/RAG/IEmbeddingService.cs deleted file mode 100644 index 9aed865..0000000 --- a/Rayson.CV/Api/Infrastructure/RAG/IEmbeddingService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Infrastructure.RAG; - -public interface IEmbeddingService -{ - Task GenerateEmbeddingAsync(string text); - Task EnsureModelAvailableAsync(); -} diff --git a/Rayson.CV/Api/Infrastructure/RAG/IRagService.cs b/Rayson.CV/Api/Infrastructure/RAG/IRagService.cs deleted file mode 100644 index 7a5c97d..0000000 --- a/Rayson.CV/Api/Infrastructure/RAG/IRagService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Infrastructure.RAG; - -public interface IRagService -{ - Task InitializeAsync(); - Task> SearchAsync(string query, int topK = 3); -} diff --git a/Rayson.CV/Api/Infrastructure/RAG/OllamaEmbeddingService.cs b/Rayson.CV/Api/Infrastructure/RAG/OllamaEmbeddingService.cs deleted file mode 100644 index f394681..0000000 --- a/Rayson.CV/Api/Infrastructure/RAG/OllamaEmbeddingService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Extensions.Logging; -using Rayson.Ollama; - -namespace Infrastructure.RAG; - -public class OllamaEmbeddingService( - IOllamaService ollamaService, - ILogger logger) : IEmbeddingService -{ - private const string EmbeddingModel = "nomic-embed-text:latest"; - - public async Task GenerateEmbeddingAsync(string text) - { - var response = await ollamaService.EmbedAsync(text, EmbeddingModel); - - if (response?.Embedding == null) - { - throw new Exception("Invalid response from Ollama embedding endpoint"); - } - - return response.Embedding.Select(x => (float)x).ToArray(); - } - - public async Task EnsureModelAvailableAsync() - { - logger.LogInformation("Checking if embedding model {Model} is available", EmbeddingModel); - - var models = await ollamaService.ListModelsAsync(); - var modelExists = models.Any(m => m.Name == EmbeddingModel); - - if (!modelExists) - { - logger.LogInformation("Pulling embedding model {Model}...", EmbeddingModel); - await ollamaService.PullModelAsync(EmbeddingModel); - } - else - { - logger.LogInformation("Embedding model {Model} is already available", EmbeddingModel); - } - } -} diff --git a/Rayson.CV/Api/Infrastructure/RAG/RagService.cs b/Rayson.CV/Api/Infrastructure/RAG/RagService.cs deleted file mode 100644 index 22497bc..0000000 --- a/Rayson.CV/Api/Infrastructure/RAG/RagService.cs +++ /dev/null @@ -1,260 +0,0 @@ -using Application.Chatbot; -using Domain; -using Microsoft.Extensions.Logging; - -namespace Infrastructure.RAG; - -public class RagService( - ICvChunkRepository cvChunkRepository, - IEmbeddingService embeddingService, - ICvProvider cvProvider, - ILogger logger) : IRagService -{ - private const int MaxChunkSize = 350; - private const int MinChunkSize = 100; - - public async Task InitializeAsync() - { - if (await cvChunkRepository.ExistsAsync()) - { - logger.LogInformation("CV chunks already exist in database, skipping RAG initialization"); - return; - } - - logger.LogInformation("Starting RAG initialization..."); - - await embeddingService.EnsureModelAvailableAsync(); - - var cvContent = cvProvider.GetCvContent(); - var chunks = ChunkCvContent(cvContent); - - logger.LogInformation("Generated {Count} chunks from CV", chunks.Count); - - var cvChunks = new List(); - foreach (var (content, section, index) in chunks) - { - logger.LogDebug("Generating embedding for chunk {Index} ({Section})", index, section); - var embedding = await embeddingService.GenerateEmbeddingAsync(content); - - cvChunks.Add(new CvChunk - { - Content = content, - Section = section, - Embedding = [.. embedding.Select(e => (double)e)], - ChunkIndex = index, - CreatedAt = DateTime.UtcNow - }); - } - - await cvChunkRepository.AddRangeAsync(cvChunks); - - logger.LogInformation("RAG initialization complete. Saved {Count} chunks to database", cvChunks.Count); - } - - public async Task> SearchAsync(string query, int topK = 3) - { - var queryEmbedding = await embeddingService.GenerateEmbeddingAsync(query); - var results = await cvChunkRepository.GetMostSimilarAsync(queryEmbedding, topK, query); - - return results; - } - - private static List<(string Content, string Section, int Index)> ChunkCvContent(string cvContent) - { - var chunks = new List<(string Content, string Section, int Index)>(); - var lines = cvContent.Split('\n'); - var currentSection = "General"; - var sectionContent = new List(); - var chunkIndex = 0; - - var sectionHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "PERSONAL DETAILS", - "EDUCATION", - "PROFESSIONAL SKILLS SUMMARY", - "WORK EXPERIENCE", - "HOME EXPERIENCE", - "PERSONAL DESCRIPTION", - "References" - }; - - foreach (var line in lines) - { - var trimmedLine = line.Trim(); - - if (sectionHeaders.Contains(trimmedLine)) - { - if (sectionContent.Count > 0) - { - var sectionText = string.Join("\n", sectionContent); - var sectionChunks = SplitIntoChunks(sectionText, currentSection); - chunks.AddRange(sectionChunks.Select((content, i) => (content, currentSection, chunkIndex++))); - sectionContent.Clear(); - } - - currentSection = trimmedLine; - } - else - { - if(!string.IsNullOrWhiteSpace(line)) - sectionContent.Add(line); - } - } - - if (sectionContent.Count > 0) - { - var sectionText = string.Join("\n", sectionContent); - var sectionChunks = SplitIntoChunks(sectionText, currentSection); - chunks.AddRange(sectionChunks.Select((content, i) => (content, currentSection, chunkIndex++))); - } - - return chunks; - } - - private static List SplitIntoChunks(string text, string section) - { - if (string.IsNullOrWhiteSpace(text)) - return []; - - var chunks = new List(); - var paragraphs = text.Split("\n\n", StringSplitOptions.RemoveEmptyEntries); - - foreach (var paragraph in paragraphs) - { - var trimmedParagraph = paragraph.Trim(); - if (string.IsNullOrWhiteSpace(trimmedParagraph)) - continue; - - if (trimmedParagraph.Length <= MaxChunkSize) - { - chunks.Add(trimmedParagraph); - continue; - } - - var lines = trimmedParagraph.Split('\n', StringSplitOptions.RemoveEmptyEntries); - var currentChunk = new List(); - - foreach (var line in lines) - { - var trimmedLine = line.Trim(); - if (string.IsNullOrWhiteSpace(trimmedLine)) - continue; - - var potentialChunk = currentChunk.Count > 0 - ? string.Join("\n", currentChunk) + "\n" + trimmedLine - : trimmedLine; - - if (potentialChunk.Length <= MaxChunkSize) - { - currentChunk.Add(trimmedLine); - } - else - { - if (currentChunk.Count > 0) - { - chunks.Add(string.Join("\n", currentChunk)); - } - - if (trimmedLine.Length > MaxChunkSize) - { - chunks.AddRange(SplitBySentences(trimmedLine)); - currentChunk.Clear(); - } - else - { - currentChunk = [trimmedLine]; - } - } - } - - if (currentChunk.Count > 0) - { - chunks.Add(string.Join("\n", currentChunk)); - } - } - - return chunks; - } - - private static List SplitBySentences(string text) - { - var chunks = new List(); - if (string.IsNullOrWhiteSpace(text)) - return chunks; - - var sentences = SplitIntoSentences(text); - var currentChunk = new List(); - - foreach (var sentence in sentences) - { - var trimmed = sentence.Trim(); - if (string.IsNullOrWhiteSpace(trimmed)) - continue; - - var potentialChunk = currentChunk.Count > 0 - ? string.Join(" ", currentChunk) + " " + trimmed - : trimmed; - - if (potentialChunk.Length <= MaxChunkSize) - { - currentChunk.Add(trimmed); - } - else - { - if (currentChunk.Count > 0) - { - chunks.Add(string.Join(" ", currentChunk)); - } - - if (trimmed.Length > MaxChunkSize) - { - for (var i = 0; i < trimmed.Length; i += MaxChunkSize - MinChunkSize) - { - var chunk = trimmed.Substring(i, Math.Min(MaxChunkSize - MinChunkSize, trimmed.Length - i)); - if (!string.IsNullOrWhiteSpace(chunk)) - chunks.Add(chunk); - } - currentChunk.Clear(); - } - else - { - currentChunk = [trimmed]; - } - } - } - - if (currentChunk.Count > 0) - { - chunks.Add(string.Join(" ", currentChunk)); - } - - return chunks; - } - - private static string[] SplitIntoSentences(string text) - { - var sentences = new List(); - var current = new System.Text.StringBuilder(); - - foreach (var c in text) - { - current.Append(c); - - if (c == '.' || c == '!' || c == '?') - { - if (current.Length > 0) - { - sentences.Add(current.ToString()); - current.Clear(); - } - } - } - - if (current.Length > 0) - { - sentences.Add(current.ToString()); - } - - return sentences.ToArray(); - } -} diff --git a/Rayson.CV/Api/Infrastructure/ollama.Dockerfile b/Rayson.CV/Api/Infrastructure/ollama.Dockerfile deleted file mode 100755 index 1414c6a..0000000 --- a/Rayson.CV/Api/Infrastructure/ollama.Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM ollama/ollama:latest - -RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* - -ENV OLLAMA_HOST=0.0.0.0 - -COPY ollama.entrypoint.sh / -COPY ollama.healthcheck.sh / - -RUN chmod +x /ollama.entrypoint.sh /ollama.healthcheck.sh - -EXPOSE 11434 - -ENTRYPOINT ["/ollama.entrypoint.sh"] \ No newline at end of file diff --git a/Rayson.CV/Api/Infrastructure/ollama.README.md b/Rayson.CV/Api/Infrastructure/ollama.README.md deleted file mode 100755 index 52d6ca1..0000000 --- a/Rayson.CV/Api/Infrastructure/ollama.README.md +++ /dev/null @@ -1,265 +0,0 @@ -# Rayson.Ollama - -.NET client library for Ollama with Docker support. - -## Installation - -```bash -dotnet add package Rayson.Ollama -``` - -> **Note:** This package creates 5 files in your project when you build, these are your ollama configuration files. - -## QuickStart - -1. Install the NuGet package and Build your project -2. Move `ollama.Dockerfile` and `*.sh` scripts to suitable location -3. Merge `ollama.docker-compose.yml` into your compose -4. Update `ollama>build>context` to files location -5. Configure `CUSTOM_MODELS` with your desired models -6. Configure `Ollama__Url` for your deployment scenario (see Configuration>Docker section below) -7. Optionally add GPU support, see comment in compose file - -### Usage: - -Using .NET's ServiceCollection for dependency injection: - -```csharp -using Rayson.Ollama.Extensions -services.AddOllama(); -``` - -```csharp -using Rayson.Ollama -var ollama = sp.GetRequiredService(); - -// Generate text (non-streaming) -var result = await ollama.GenerateAsync("Hello", "smollm:135m"); -Console.WriteLine(result.Response); - -// Generate with streaming -await foreach (var chunk in ollama.GenerateStreamAsync("Hello", "smollm:135m")) -{ - Console.Write(chunk.Response); -} - -// Chat (non-streaming) -var chatResult = await ollama.ChatAsync( - [new() { Role = "user", Content = "Hello" }], - "smollm:135m"); -Console.WriteLine(chatResult.Message?.Content); - -// Chat with streaming -await foreach (var chunk in ollama.ChatStreamAsync( - [new() { Role = "user", Content = "Hello" }], - "smollm:135m")) -{ - Console.Write(chunk.Message?.Content); -} - -// Generate embeddings -var embedding = await ollama.EmbedAsync("Hello", "nomic-embed-text"); -Console.WriteLine(embedding.Embedding.Count); -``` - -## Prerequisites - -### NVIDIA GPU Support - -If using an NVIDIA GPU, install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html): - -```bash -# Configure the repository -curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ - | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg -curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \ - | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \ - | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list -sudo apt-get update - -# Install the toolkit -sudo apt-get install -y nvidia-container-toolkit - -# Configure Docker -sudo nvidia-ctk runtime configure --runtime=docker -sudo systemctl restart docker -``` - -After installation, enable NVIDIA GPU support in `ollama.docker-compose.yml` by uncommenting the `deploy` section. - -## Docker Setup - -### Running Ollama - -```bash -docker compose up --build -``` - -> **Note:** By default, Ollama models are persisted in a Docker volume (`ollama_data`). This preserves your downloaded models across container restarts. - -### Healthcheck - -The docker-compose includes a healthcheck that polls the Ollama API every 10 seconds. You can check the health status with: - -```bash -docker compose ps -``` - -### Using Multiple Models - -By default, the container starts with `smollm:135m` and `nomic-embed-text`. To configure models, edit the `CUSTOM_MODELS` environment variable in `ollama.docker-compose.yml`: - -```yaml -environment: - - CUSTOM_MODELS=["smollm:135m","nomic-embed-text"] -``` - -The `CUSTOM_MODELS` environment variable accepts a JSON array of model names. The entrypoint script will check if each model exists and pull it only if missing. - -## Configuration - -### Docker - -#### Ollama container - -| Environment Variable | Default | Description | -|---------------------|---------|-------------| -| `CUSTOM_MODELS` | `["smollm:135m","nomic-embed-text"]` | Models to check/pull on startup | - -> **Note:** The `OLLAMA_HOST` in the Ollama container is hardcoded to `0.0.0.0` so connections from other containers are accepted. This could raise security issues for your environment. - -#### Consumer container - -| Environment Variable | Default | Description | -|---------------------|---------|-------------| -| `Ollama__Url` | `http://ollama:11434` | URL for calling containers configuration to reach Ollama API (Docker internal network) | - -### Connecting to Ollama - -The `Ollama__Url` environment variable specifies the address other containers use to reach Ollama. Set this based on your deployment scenario: - -- **Container-to-container** (recommended): Use `http://ollama:11434` (Docker internal network) -- **Host machine access**: Use `http://localhost:11434` and add `network_mode: host` to your compose: - -```yaml -services: - your-app: - network_mode: host -``` - -### .NET Client - -Configure the Ollama URL in your .NET application to match the `Ollama__Url` value from your docker-compose: - -**Via docker-compose.yml:** -```yaml -services: - webapp: - build: . - image: my-example-app:latest - environment: - - Ollama__Url=http://ollama:11434 -``` - -The URL should match the `Ollama__Url` value in your docker-compose. - -## API Reference - -### IOllamaService - -```csharp -// Generate text (non-streaming) -Task GenerateAsync(string prompt, string model, GenerateOptions? options = null, CancellationToken ct = default) - -// Generate text with streaming -IAsyncEnumerable GenerateStreamAsync(string prompt, string model, GenerateOptions? options = null, CancellationToken ct = default) - -// Chat (non-streaming) -Task ChatAsync(IList messages, string model, ChatOptions? options = null, CancellationToken ct = default) - -// Chat with streaming -IAsyncEnumerable ChatStreamAsync(IList messages, string model, ChatOptions? options = null, CancellationToken ct = default) - -// Generate embeddings -Task EmbedAsync(string input, string model, CancellationToken ct = default) - -// List available models -Task> ListModelsAsync(CancellationToken ct = default) - -// Pull a model -Task PullModelAsync(string model, IProgress? progress = null, CancellationToken ct = default) - -// Delete a model -Task DeleteModelAsync(string model, CancellationToken ct = default) -``` - -### Options - -```csharp -// Options for Generate/GenerateStream -public class GenerateOptions -{ - public string? Model { get; set; } - public string? Prompt { get; set; } - public string? System { get; set; } - public string? Template { get; set; } - public string? Context { get; set; } - public bool Raw { get; set; } - public double? Temperature { get; set; } - public double? TopP { get; set; } - public int? TopK { get; set; } - public double? RepeatPenalty { get; set; } - public int? NumPredict { get; set; } - public IList? Stop { get; set; } -} - -// Options for Chat/ChatStream -public class ChatOptions -{ - public string? Model { get; set; } - public string? System { get; set; } - public IList? Messages { get; set; } - public double? Temperature { get; set; } - public double? TopP { get; set; } - public int? TopK { get; set; } - public double? RepeatPenalty { get; set; } - public int? NumPredict { get; set; } - public IList? Stop { get; set; } -} -``` - -### Response Types - -```csharp -public class OllamaMessage -{ - public string? Role { get; set; } - public string? Content { get; set; } -} - -public class OllamaResponse -{ - public string? Model { get; set; } - public bool Done { get; set; } - public string? DoneReason { get; set; } - public long? TotalDuration { get; set; } - public long? LoadDuration { get; set; } - public int? PromptEvalCount { get; set; } - public int? EvalCount { get; set; } - public long? EvalDuration { get; set; } -} - -public class GenerateResponse : OllamaResponse -{ - public string? Response { get; set; } -} - -public class ChatResponse : OllamaResponse -{ - public OllamaMessage? Message { get; set; } -} - -public record ModelInfo(string Name, string ModifiedAt, long Size); - -public record EmbeddingsResponse(IReadOnlyList Embedding); -``` diff --git a/Rayson.CV/Api/Infrastructure/ollama.docker-compose.yml b/Rayson.CV/Api/Infrastructure/ollama.docker-compose.yml deleted file mode 100755 index d5ae730..0000000 --- a/Rayson.CV/Api/Infrastructure/ollama.docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ -services: - ollama: - build: - context: . - dockerfile: ollama.Dockerfile - environment: - - CUSTOM_MODELS=["smollm2:135m","nomic-embed-text"] - volumes: - - ollama_data:/root/.ollama - container_name: ollama - healthcheck: - test: ["CMD", "/ollama.healthcheck.sh"] - interval: 10s - timeout: 10s - retries: 30 - start_period: 10s - # NVIDIA GPU Support: Uncomment the following to enable NVIDIA GPU - # deploy: - # resources: - # reservations: - # devices: - # - driver: nvidia - # count: all - # capabilities: [gpu] - -volumes: - ollama_data: diff --git a/Rayson.CV/Api/Infrastructure/ollama.entrypoint.sh b/Rayson.CV/Api/Infrastructure/ollama.entrypoint.sh deleted file mode 100755 index 7c1928c..0000000 --- a/Rayson.CV/Api/Infrastructure/ollama.entrypoint.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -set -e - -if [ -z "$OLLAMA_HOST" ]; then - echo "ERROR: OLLAMA_HOST environment variable is required" - exit 1 -fi - -if [ -z "$CUSTOM_MODELS" ]; then - echo "ERROR: CUSTOM_MODELS environment variable is required" - exit 1 -fi - -# This is done in the image build already -# echo "Installing curl..." -# apt-get update && apt-get install -y curl - -echo "Starting Ollama Service..." -ollama serve & - -echo "Waiting for Ollama to be ready..." -until curl -s http://localhost:11434/api/tags > /dev/null 2>&1; do - sleep 1 -done - -MODELS=${CUSTOM_MODELS} -MODELS_CLEANED=$(echo "$MODELS" | tr -d '[]"' | tr ',' '\n') - -for model in $MODELS_CLEANED; do - model=$(echo "$model" | tr -d '[:space:]') - if [ -z "$model" ]; then - continue - fi - - echo "Checking model: $model" - if ollama list | grep -q "$model"; then - echo "Model $model already exists" - else - echo "Pulling model: $model" - ollama pull "$model" - fi -done - -echo "Finished pulling models" -wait \ No newline at end of file diff --git a/Rayson.CV/Api/Infrastructure/ollama.healthcheck.sh b/Rayson.CV/Api/Infrastructure/ollama.healthcheck.sh deleted file mode 100755 index 23e83fa..0000000 --- a/Rayson.CV/Api/Infrastructure/ollama.healthcheck.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -set -x - -echo "CUSTOM_MODELS: $CUSTOM_MODELS" - -if ! curl -sf http://localhost:11434/api/tags > /dev/null 2>&1; then - echo "Healthcheck: Ollama API not responding" - exit 1 -fi - -RESPONSE=$(curl -s http://localhost:11434/api/tags) - -MODELS_CLEANED=$(echo "$CUSTOM_MODELS" | tr -d '[]"' | tr ',' '\n') -echo "Checking models: $MODELS_CLEANED" - -for model in $MODELS_CLEANED; do - model=$(echo "$model" | tr -d '[:space:]') - if [ -z "$model" ]; then - continue - fi - - if ! echo "$RESPONSE" | grep -q "$model"; then - echo "Healthcheck: Model $model not found" - exit 1 - fi -done - -echo "Healthcheck: All models ready" -exit 0 diff --git a/Rayson.CV/Api/Presentation/Endpoints/Files/StaticFilesEndpoints.cs b/Rayson.CV/Api/Presentation/Endpoints/Files/StaticFilesEndpoints.cs index a4efb83..cb37612 100644 --- a/Rayson.CV/Api/Presentation/Endpoints/Files/StaticFilesEndpoints.cs +++ b/Rayson.CV/Api/Presentation/Endpoints/Files/StaticFilesEndpoints.cs @@ -20,7 +20,7 @@ public static void MapStaticFilesEndpoints(this WebApplication webApplication) private static IResult GetCv() { - const string filename = "Daniel-Rayson-DotNet-CV.pdf"; + const string filename = "CV 2026.pdf"; var webRoot = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); var filePath = Path.Combine(webRoot, filename); diff --git a/Rayson.CV/Api/Presentation/Extensions/WebApplicationExtensions.cs b/Rayson.CV/Api/Presentation/Extensions/WebApplicationExtensions.cs deleted file mode 100644 index 7d3b59f..0000000 --- a/Rayson.CV/Api/Presentation/Extensions/WebApplicationExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Database; -using Database.SeedData; -using Infrastructure.RAG; -using Microsoft.EntityFrameworkCore; - -namespace Presentation.Extensions; - -public static class WebApplicationExtensions -{ - public static async Task RunMigrationsAsync(this WebApplication webApplication) - { - var scope = webApplication.Services.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - await context.Database.MigrateAsync(); - var seeder = scope.ServiceProvider.GetRequiredService(); - await seeder.SeedDatabaseAsync(); - } - - public static async Task InitializeRagAsync(this WebApplication webApplication) - { - var scope = webApplication.Services.CreateScope(); - var ragService = scope.ServiceProvider.GetRequiredService(); - await ragService.InitializeAsync(); - } -} diff --git a/Rayson.CV/Api/Presentation/Presentation.csproj b/Rayson.CV/Api/Presentation/Presentation.csproj index d7a9e2a..f7d6372 100644 --- a/Rayson.CV/Api/Presentation/Presentation.csproj +++ b/Rayson.CV/Api/Presentation/Presentation.csproj @@ -8,16 +8,11 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - diff --git a/Rayson.CV/Api/Presentation/Program.cs b/Rayson.CV/Api/Presentation/Program.cs index 06663af..c8743be 100644 --- a/Rayson.CV/Api/Presentation/Program.cs +++ b/Rayson.CV/Api/Presentation/Program.cs @@ -1,6 +1,4 @@ using Presentation.Extensions; -using Database.SeedData; -using Database.Extensions; using Presentation.Endpoints.Health; using Presentation.Endpoints.Logging; using Presentation.Endpoints.Chatbot; @@ -40,7 +38,6 @@ }); builder.Services.AddPresentationServices(); - builder.Services.AddDatabaseServices(builder.Configuration); builder.Services.AddInfrastructureServices(builder.Configuration); var app = builder.Build(); @@ -72,8 +69,6 @@ }); } - await app.RunMigrationsAsync(); - await app.InitializeRagAsync(); app.Run(); } catch (Exception ex) diff --git a/Rayson.CV/Api/RaysonCVBackend.sln b/Rayson.CV/Api/RaysonCVBackend.sln index b9f42f2..1c4ffcb 100644 --- a/Rayson.CV/Api/RaysonCVBackend.sln +++ b/Rayson.CV/Api/RaysonCVBackend.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{233DCA60-C404-4534-A551-0263A2308E08}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database", "Database\Database.csproj", "{B6C6ED32-816C-4BF9-8A64-6148EC9E13B8}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\Application.csproj", "{1534280A-186B-4B41-A481-DC5A51D6D3A2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{146B7D4B-6453-4CB8-A15A-EFF007B603DA}" @@ -23,10 +21,6 @@ Global {233DCA60-C404-4534-A551-0263A2308E08}.Debug|Any CPU.Build.0 = Debug|Any CPU {233DCA60-C404-4534-A551-0263A2308E08}.Release|Any CPU.ActiveCfg = Release|Any CPU {233DCA60-C404-4534-A551-0263A2308E08}.Release|Any CPU.Build.0 = Release|Any CPU - {B6C6ED32-816C-4BF9-8A64-6148EC9E13B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B6C6ED32-816C-4BF9-8A64-6148EC9E13B8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B6C6ED32-816C-4BF9-8A64-6148EC9E13B8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B6C6ED32-816C-4BF9-8A64-6148EC9E13B8}.Release|Any CPU.Build.0 = Release|Any CPU {1534280A-186B-4B41-A481-DC5A51D6D3A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1534280A-186B-4B41-A481-DC5A51D6D3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {1534280A-186B-4B41-A481-DC5A51D6D3A2}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Rayson.CV/Api/prod.Dockerfile b/Rayson.CV/Api/prod.Dockerfile index 4118173..96ee784 100644 --- a/Rayson.CV/Api/prod.Dockerfile +++ b/Rayson.CV/Api/prod.Dockerfile @@ -13,7 +13,6 @@ COPY ["Presentation/Presentation.csproj", "Presentation/"] COPY ["Application/Application.csproj", "Application/"] COPY ["Domain/Domain.csproj", "Domain/"] COPY ["Infrastructure/Infrastructure.csproj", "Infrastructure/"] -COPY ["Database/Database.csproj", "Database/"] # Restore NuGet packages based on project references RUN dotnet restore "Presentation/Presentation.csproj" @@ -64,11 +63,6 @@ ENV ASPNETCORE_URLS=http://+:8080 # via docker-compose or environment file: # # Required: -# - POSTGRES_HOST (e.g., postgres) -# - POSTGRES_PORT (e.g., 5432) -# - POSTGRES_DATABASE (e.g., raysoncv) -# - POSTGRES_USERNAME (e.g., postgres) -# - POSTGRES_PASSWORD (secure password) # - OLLAMA__URL (e.g., http://ollama:11434) # # Optional: diff --git a/Rayson.CV/Api/wwwroot/CV 2026.pdf b/Rayson.CV/Api/wwwroot/CV 2026.pdf new file mode 100644 index 0000000..69cada3 Binary files /dev/null and b/Rayson.CV/Api/wwwroot/CV 2026.pdf differ diff --git a/Rayson.CV/Api/wwwroot/Daniel-Rayson-DotNet-CV.pdf b/Rayson.CV/Api/wwwroot/Daniel-Rayson-DotNet-CV.pdf deleted file mode 100644 index c44f9e3..0000000 Binary files a/Rayson.CV/Api/wwwroot/Daniel-Rayson-DotNet-CV.pdf and /dev/null differ diff --git a/Rayson.CV/UI/src/data/cv.ts b/Rayson.CV/UI/src/data/cv.ts index bce9f4d..2d3fd21 100644 --- a/Rayson.CV/UI/src/data/cv.ts +++ b/Rayson.CV/UI/src/data/cv.ts @@ -38,7 +38,7 @@ export interface PersonalDetails { } export const personalDetails: PersonalDetails = { - address: '85 Bagots Oak, ST17 9SB', + address: 'Stafford, ST17 9SB', phone: '07703 574867', email: 'daniel@rayson.dev', website: 'www.rayson.dev', @@ -46,22 +46,25 @@ export const personalDetails: PersonalDetails = { export const workExperience: WorkExperience[] = [ { - id: 'self-employed', - company: 'Self Employed (Material Product Arbitrage)', - period: 'Winter 2023 – Current', - yearStart: 2023, - title: 'N/A', - description: 'Self-directed research and systems design in material product arbitrage', + id: 'freelance', + company: 'Freelance', + period: '2024 - Current', + yearStart: 2024, + title: 'Senior Full-Stack Developer', + description: 'Full-stack development with focus on quality delivery and project ownership', bulletPoints: [ - 'Self-directed research and systems design', - 'Responsibility for following regulatory requirements', - 'Automation, data validation, service interfacing', + 'Followed ISO 29110 processes to ensure quality delivery', + 'Cloud and On-site hosting solutions with full CI/CD pipelines', + 'Full project lifecycle ownership from requirements through deployment', + 'Completed full-stack applications: arbitrage platform and custom e-commerce shop', + 'Contract and legal compliance considerations in project delivery', + 'Self-hosted portfolio website (www.rayson.dev) and additional personal projects', ], }, { id: 'the-site-doctor', company: 'The Site Doctor (Small and Highly Skilled Software House)', - period: 'Winter 2021 – Winter 2023', + period: '2021 - 2024', yearStart: 2021, title: 'Software Developer', description: 'Full project ownership in a small, highly skilled software house', @@ -75,7 +78,7 @@ export const workExperience: WorkExperience[] = [ { id: 'inspired-gaming', company: 'Inspired Gaming UK Ltd (Video Streaming and Back-end Services)', - period: 'Winter 2017 – Winter 2021', + period: '2017 - 2021', yearStart: 2017, title: 'Senior Software Developer', description: 'Senior developer role in video streaming and back-end services', @@ -90,7 +93,7 @@ export const workExperience: WorkExperience[] = [ { id: 'arvs', company: 'ARVS ltd (Car Accident Claims Management)', - period: 'Summer 2014 – Summer 2016', + period: '2014 - 2016', yearStart: 2014, title: 'Web Developer', description: 'Web developer supporting a bespoke motor accident management website', @@ -102,9 +105,9 @@ export const workExperience: WorkExperience[] = [ ], }, { - id: 'dbl ogic', + id: 'dblogic', company: 'DBLogic (Software House)', - period: 'Spring 2012 – Spring 2014', + period: '2012 - 2014', yearStart: 2012, title: 'Software Development Engineer', description: 'Software development engineer in a software house environment', @@ -118,7 +121,7 @@ export const workExperience: WorkExperience[] = [ { id: 'freelance', company: 'Freelance (Personal and Brochure Websites)', - period: '2007 – 2012', + period: '2007 - 2012', yearStart: 2007, title: 'Website Development and Support', description: 'Freelance website development and support', @@ -188,8 +191,8 @@ export const projects: Project[] = [ id: 'tradepulse', name: 'TradePulse', description: 'Trading API project', - period: '2024', - year: 2024, + period: '2023', + year: 2023, technologies: ['.NET', 'API'], type: 'application', }, @@ -197,8 +200,8 @@ export const projects: Project[] = [ id: 'tradepulse3', name: 'TradePulse3', description: 'Trading system v3', - period: '2023', - year: 2023, + period: '2024', + year: 2024, technologies: ['.NET', 'React'], type: 'application', }, @@ -206,8 +209,8 @@ export const projects: Project[] = [ id: 'rayson-cv', name: 'Rayson.CV', description: 'Personal CV website', - period: '2023', - year: 2023, + period: '2025', + year: 2025, technologies: ['React', 'TypeScript', 'Vite'], type: 'application', }, diff --git a/Rayson.CV/UI/src/pages/TimelinePage.tsx b/Rayson.CV/UI/src/pages/TimelinePage.tsx index af1266f..95f1290 100644 --- a/Rayson.CV/UI/src/pages/TimelinePage.tsx +++ b/Rayson.CV/UI/src/pages/TimelinePage.tsx @@ -13,7 +13,7 @@ import { } from '../data/cv'; function getEndYear(period: string): number { - if (period.toLowerCase().includes('current')) return 2026; + if (period.toLowerCase().includes('current') || period.toLowerCase().includes('onwards')) return 2026; const match = period.match(/(\d{4})/g); if (!match) return 2026; return parseInt(match[match.length - 1]); diff --git a/Rayson.CV/docker-compose.dev.db-api.yml b/Rayson.CV/docker-compose.dev.db-api.yml deleted file mode 100644 index a80bb84..0000000 --- a/Rayson.CV/docker-compose.dev.db-api.yml +++ /dev/null @@ -1,97 +0,0 @@ -# =========================================== -# Docker Compose - Development: Database + API -# =========================================== -# Usage: docker compose -f docker-compose.dev.db-api.yml up -d -# Purpose: Run PostgreSQL and API in Docker, debug UI locally -# UI connects to API at http://localhost:13245 -# =========================================== - -services: - postgres: - image: pgvector/pgvector:pg16 - container_name: raysoncv-postgres - restart: unless-stopped - environment: - POSTGRES_DB: raysoncv - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - volumes: - - postgres-data:/var/lib/postgresql/data - networks: - - internal - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d raysoncv"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - - api: - build: - context: ./Api - dockerfile: Dockerfile - container_name: raysoncv-api - restart: unless-stopped - environment: - POSTGRES_HOST: postgres - POSTGRES_PORT: 5432 - POSTGRES_DATABASE: raysoncv - POSTGRES_USERNAME: postgres - POSTGRES_PASSWORD: postgres - ASPNETCORE_ENVIRONMENT: Development - Graph__ClientSecret: notachancebuddy - Cors__AllowedOrigins: http://localhost:3000,http://localhost:5173 - LOG_LEVEL: Debug - Ollama__Url: http://ollama:11434 - ports: - - "13245:8080" - depends_on: - postgres: - condition: service_healthy - ollama: - condition: service_healthy - networks: - - internal - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - - # =========================================== - # Ollama (AI Chatbot) - # =========================================== - ollama: - build: - context: ./Api/Infrastructure/Ollama - dockerfile: ollama.Dockerfile - environment: - - CUSTOM_MODELS=["llama3.2:latest","nomic-embed-text"] - volumes: - - ollama_data:/root/.ollama - container_name: ollama - healthcheck: - test: ["CMD", "/ollama.healthcheck.sh"] - interval: 10s - timeout: 10s - retries: 30 - start_period: 10s - networks: - - internal - # NVIDIA GPU Support: Uncomment the following to enable NVIDIA GPU - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: all - capabilities: [gpu] - -networks: - internal: - driver: bridge - -volumes: - postgres-data: - ollama_data: diff --git a/Rayson.CV/docker-compose.dev.db-ui.yml b/Rayson.CV/docker-compose.dev.db-ui.yml deleted file mode 100644 index 34725aa..0000000 --- a/Rayson.CV/docker-compose.dev.db-ui.yml +++ /dev/null @@ -1,89 +0,0 @@ -# =========================================== -# Docker Compose - Development: Database + UI -# =========================================== -# Usage: docker compose -f docker-compose.dev.db-ui.yml up -d -# Purpose: Run PostgreSQL and UI in Docker, debug API locally -# API runs on host at http://localhost:13245 -# =========================================== - -services: - postgres: - image: pgvector/pgvector:pg16 - container_name: raysoncv-postgres - restart: unless-stopped - environment: - POSTGRES_DB: raysoncv - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - volumes: - - postgres-data:/var/lib/postgresql/data - ports: - - "5433:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d raysoncv"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - - ui: - build: - context: ./UI - dockerfile: Dockerfile - args: - VITE_API_BASE_URL: http://localhost:13245/ - container_name: raysoncv-ui - restart: unless-stopped - environment: - API_HEALTH_URL: http://host.docker.internal:13245/health - LOG_LEVEL: info - ports: - - "3000:3000" - extra_hosts: - - "host.docker.internal:host-gateway" - depends_on: - postgres: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health/live"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - # =========================================== - # Ollama (AI Chatbot) - # =========================================== - ollama: - build: - context: ./Api/Infrastructure/Ollama - dockerfile: ollama.Dockerfile - environment: - - CUSTOM_MODELS=["llama3.2:latest","nomic-embed-text"] - volumes: - - ollama_data:/root/.ollama - container_name: ollama - healthcheck: - test: ["CMD", "/ollama.healthcheck.sh"] - interval: 10s - timeout: 10s - retries: 30 - start_period: 10s - networks: - - internal - # NVIDIA GPU Support: Uncomment the following to enable NVIDIA GPU - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: all - capabilities: [gpu] - -volumes: - postgres-data: - ollama_data: - -networks: - internal: - driver: bridge diff --git a/Rayson.CV/docker-compose.dev.db.yml b/Rayson.CV/docker-compose.dev.db.yml deleted file mode 100644 index 39e1cf0..0000000 --- a/Rayson.CV/docker-compose.dev.db.yml +++ /dev/null @@ -1,63 +0,0 @@ -# =========================================== -# Docker Compose - Development: Database Only -# =========================================== -# Usage: docker compose -f docker-compose.dev.db.yml up -d -# Purpose: Run only PostgreSQL for local debugging of API and UI -# =========================================== - -services: - postgres: - image: pgvector/pgvector:pg16 - container_name: raysoncv-postgres - restart: unless-stopped - environment: - POSTGRES_DB: raysoncv - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - volumes: - - postgres-data:/var/lib/postgresql/data - ports: - - "5433:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d raysoncv"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - - # =========================================== - # Ollama (AI Chatbot) - # =========================================== - ollama: - build: - context: ./Api/Infrastructure/Ollama - dockerfile: ollama.Dockerfile - environment: - - CUSTOM_MODELS=["llama3.2:latest","nomic-embed-text"] - volumes: - - ollama_data:/root/.ollama - container_name: ollama - healthcheck: - test: ["CMD", "/ollama.healthcheck.sh"] - interval: 10s - timeout: 10s - retries: 30 - start_period: 10s - networks: - - internal - # NVIDIA GPU Support: Uncomment the following to enable NVIDIA GPU - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: all - capabilities: [gpu] - -volumes: - postgres-data: - ollama_data: - -networks: - internal: - driver: bridge diff --git a/Rayson.CV/docker-compose.dev.full.yml b/Rayson.CV/docker-compose.dev.full.yml index f55f02c..705cc0a 100644 --- a/Rayson.CV/docker-compose.dev.full.yml +++ b/Rayson.CV/docker-compose.dev.full.yml @@ -2,32 +2,10 @@ # Docker Compose - Development: Full Stack # =========================================== # Usage: docker compose -f docker-compose.dev.full.yml up -d -# Purpose: Run all services (PostgreSQL, API, UI) in Docker +# Purpose: Run all services (API, UI, Ollama) in Docker # =========================================== services: - # =========================================== - # PostgreSQL Database - # =========================================== - postgres: - image: pgvector/pgvector:pg16 - container_name: raysoncv-postgres - restart: unless-stopped - environment: - POSTGRES_DB: raysoncv - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - volumes: - - postgres-data:/var/lib/postgresql/data - networks: - - internal - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d raysoncv"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - # =========================================== # API (.NET 8.0) # =========================================== @@ -38,11 +16,6 @@ services: container_name: raysoncv-api restart: unless-stopped environment: - POSTGRES_HOST: postgres - POSTGRES_PORT: 5432 - POSTGRES_DATABASE: raysoncv - POSTGRES_USERNAME: postgres - POSTGRES_PASSWORD: postgres ASPNETCORE_ENVIRONMENT: Development Graph__ClientSecret: fake-passwrd Cors__AllowedOrigins: http://localhost:3000 @@ -51,8 +24,6 @@ services: ports: - "13245:8080" depends_on: - postgres: - condition: service_healthy ollama: condition: service_healthy networks: @@ -100,10 +71,10 @@ services: context: ./Api/Infrastructure/Ollama dockerfile: ollama.Dockerfile environment: - - CUSTOM_MODELS=["llama3.2:latest","nomic-embed-text"] + - CUSTOM_MODELS=["llama3.2:latest"] volumes: - ollama_data:/root/.ollama - container_name: ollama + container_name: raysoncv-ollama healthcheck: test: ["CMD", "/ollama.healthcheck.sh"] interval: 10s @@ -132,5 +103,4 @@ networks: # Volumes # =========================================== volumes: - postgres-data: ollama_data: diff --git a/Rayson.CV/docker-compose.prod.full.yml b/Rayson.CV/docker-compose.prod.full.yml index e173170..05ff09c 100644 --- a/Rayson.CV/docker-compose.prod.full.yml +++ b/Rayson.CV/docker-compose.prod.full.yml @@ -2,32 +2,10 @@ # Docker Compose - Production: Full Stack # =========================================== # Usage: docker compose -f docker-compose.prod.full.yml --env-file .env.production up -d -# Purpose: Run all services (PostgreSQL, API, UI) in Docker for production +# Purpose: Run all services (API, UI, Ollama) in Docker for production # =========================================== services: - # =========================================== - # PostgreSQL Database - # =========================================== - postgres: - image: pgvector/pgvector:pg16 - container_name: raysoncv-postgres - restart: unless-stopped - environment: - POSTGRES_DB: raysoncv - POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - volumes: - - postgres-data:/var/lib/postgresql/data - networks: - - internal - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d raysoncv"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - # =========================================== # API (.NET 8.0) # =========================================== @@ -38,11 +16,6 @@ services: container_name: raysoncv-api restart: unless-stopped environment: - POSTGRES_HOST: postgres - POSTGRES_PORT: 5432 - POSTGRES_DATABASE: raysoncv - POSTGRES_USERNAME: postgres - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ASPNETCORE_ENVIRONMENT: Production Cors__AllowedOrigins: https://www.rayson.dev,https://rayson.dev Ollama__Url: http://ollama:11434 @@ -50,8 +23,6 @@ services: ports: - "13245:8080" depends_on: - postgres: - condition: service_healthy ollama: condition: service_healthy networks: @@ -89,7 +60,7 @@ services: interval: 30s timeout: 10s retries: 3 - start_period: 30s + start_period: 10s # =========================================== # Ollama (AI Chatbot) @@ -99,10 +70,10 @@ services: context: ./Api/Infrastructure/Ollama dockerfile: ollama.Dockerfile environment: - - CUSTOM_MODELS=["llama3.2:latest","nomic-embed-text"] + - CUSTOM_MODELS=["llama3.2:latest"] volumes: - ollama_data:/root/.ollama - container_name: ollama + container_name: raysoncv-ollama healthcheck: test: ["CMD", "/ollama.healthcheck.sh"] interval: 30s @@ -131,5 +102,4 @@ networks: # Volumes # =========================================== volumes: - postgres-data: ollama_data: