diff --git a/deploy/portainer-stack.yml b/deploy/portainer-stack.yml index 6492e8d..a610223 100644 --- a/deploy/portainer-stack.yml +++ b/deploy/portainer-stack.yml @@ -1,6 +1,6 @@ services: beacon-server: - image: beacon-server:0.1.2 + image: beacon-server:0.1.7 restart: always environment: ASPNETCORE_ENVIRONMENT: Production @@ -59,7 +59,7 @@ services: /bin/sh -c '(while sleep 86400; do nginx -s reload; done) & exec nginx -g "daemon off;"' beacon-backup: - image: beacon-backup:0.1.2 + image: beacon-backup:0.1.7 restart: always environment: BACKUP_INTERVAL_SECONDS: "86400" diff --git a/docs/ROADMAP-profiles.md b/docs/ROADMAP-profiles.md index e93ff7b..3e87c5d 100644 --- a/docs/ROADMAP-profiles.md +++ b/docs/ROADMAP-profiles.md @@ -45,7 +45,7 @@ that makes the addition cheap. ``` ProfileEntity Id, AccountId, CharacterName, WorldId - DisplayName, Pronouns, Age, Race, Occupation + DisplayName, Age, Race, Occupation Appearance, Personality, Background (long text) Hooks (short "what you could approach me about" lines) PortraitImageId -> reuses BeaconImageEntity's storage diff --git a/src/Beacon.Plugin/Beacon.Plugin.csproj b/src/Beacon.Plugin/Beacon.Plugin.csproj index ae4bf92..6806467 100644 --- a/src/Beacon.Plugin/Beacon.Plugin.csproj +++ b/src/Beacon.Plugin/Beacon.Plugin.csproj @@ -4,7 +4,7 @@ Beacon Beacon - 0.1.6.0 + 0.1.7.0 enable diff --git a/src/Beacon.Plugin/UI/ProfileLabels.cs b/src/Beacon.Plugin/UI/ProfileLabels.cs index 33193bb..d83edd2 100644 --- a/src/Beacon.Plugin/UI/ProfileLabels.cs +++ b/src/Beacon.Plugin/UI/ProfileLabels.cs @@ -102,16 +102,6 @@ public static class ProfileLabels + "Plenty of people want this and want nothing to do with the explicit tag.", }; - public static string Describe(AgeRange age) => age switch - { - AgeRange.YoungAdult => "Young adult", - AgeRange.Adult => "Adult", - AgeRange.MiddleAged => "Middle-aged", - AgeRange.Elder => "Elder", - AgeRange.Ageless => "Ageless", - _ => "Unsaid", - }; - public static string Describe(GalleryCategory category) => category switch { GalleryCategory.FullBody => "Full body", diff --git a/src/Beacon.Plugin/Windows/ChronicleWindow.cs b/src/Beacon.Plugin/Windows/ChronicleWindow.cs index be4bb6c..20beedf 100644 --- a/src/Beacon.Plugin/Windows/ChronicleWindow.cs +++ b/src/Beacon.Plugin/Windows/ChronicleWindow.cs @@ -433,11 +433,8 @@ private void DrawCardContents(ProfileDto profile, float scale) if (!string.IsNullOrWhiteSpace(profile.Identity.Lineage)) details.Add(profile.Identity.Lineage); - if (profile.Identity.Age != AgeRange.Unspecified) - details.Add(ProfileLabels.Describe(profile.Identity.Age)); - - if (!string.IsNullOrWhiteSpace(profile.Identity.Pronouns)) - details.Add(profile.Identity.Pronouns!); + if (profile.Identity.Age is { } age) + details.Add($"Age {age}"); if (details.Count > 0) Ornament.Text(Theme.InkSoft, string.Join(" · ", details)); diff --git a/src/Beacon.Plugin/Windows/ProfileEditorWindow.cs b/src/Beacon.Plugin/Windows/ProfileEditorWindow.cs index e06db1a..e9a33a7 100644 --- a/src/Beacon.Plugin/Windows/ProfileEditorWindow.cs +++ b/src/Beacon.Plugin/Windows/ProfileEditorWindow.cs @@ -46,10 +46,9 @@ public sealed class ProfileEditorWindow : Window, IDisposable private string race = string.Empty; private string clan = string.Empty; private string gender = string.Empty; - private string pronouns = string.Empty; private string quote = string.Empty; private readonly string[] archetype = ["", "", ""]; - private AgeRange age = AgeRange.Unspecified; + private string age = string.Empty; private readonly HashSet traits = []; private readonly HashSet tones = []; @@ -139,10 +138,9 @@ public void Open(ProfileDto? profile) race = appearance.Race ?? string.Empty; clan = appearance.Clan ?? string.Empty; gender = appearance.Gender ?? string.Empty; - pronouns = string.Empty; quote = string.Empty; Array.Fill(archetype, string.Empty); - age = AgeRange.Unspecified; + age = string.Empty; length = RpLength.Casual; boundaries = string.Empty; @@ -164,9 +162,8 @@ public void Open(ProfileDto? profile) race = identity.Race ?? string.Empty; clan = identity.Clan ?? string.Empty; gender = identity.Gender ?? string.Empty; - pronouns = identity.Pronouns ?? string.Empty; quote = identity.Quote ?? string.Empty; - age = identity.Age; + age = identity.Age?.ToString() ?? string.Empty; for (var i = 0; i < archetype.Length; i++) archetype[i] = i < identity.Archetype.Count ? identity.Archetype[i] : string.Empty; @@ -432,24 +429,10 @@ private void DrawCharacter(float scale) ImGui.SetNextItemWidth(half); ImGui.InputTextWithHint("##gender", "Gender", ref gender, 32); - ImGui.SameLine(0, 8f * scale); - ImGui.SetNextItemWidth(half); - ImGui.InputTextWithHint("##pronouns", "Pronouns", ref pronouns, 32); + Ornament.PageLabel("Age"); ImGui.SetNextItemWidth(half); - if (ImGui.BeginCombo("##age", ProfileLabels.Describe(age))) - { - foreach (var option in Enum.GetValues()) - { - if (ImGui.Selectable(ProfileLabels.Describe(option), age == option)) - age = option; - } - - ImGui.EndCombo(); - } - - ImGui.SameLine(); - Ornament.Text(Theme.MutedDeep, "A bracket, not a number."); + ImGui.InputTextWithHint("##age", "Age", ref age, 5, ImGuiInputTextFlags.CharsDecimal); ImGui.Spacing(); Ornament.PageLabel("A line they might say"); @@ -781,6 +764,20 @@ private async Task SubmitAsync() var characterName = editing?.CharacterName ?? location.CurrentCharacterName; var worldId = editing?.WorldId ?? location.CurrentWorldId; + int? ageYears = null; + if (!string.IsNullOrWhiteSpace(age)) + { + if (!int.TryParse(age, out var parsedAge) + || parsedAge < ProfileLimits.AgeMin + || parsedAge > ProfileLimits.AgeMax) + { + validationError = $"Age must be between {ProfileLimits.AgeMin} and {ProfileLimits.AgeMax}."; + return; + } + + ageYears = parsedAge; + } + if (string.IsNullOrWhiteSpace(characterName) || worldId == 0) { validationError = "Beacon cannot tell which character this is. Wait until you have loaded in."; @@ -802,9 +799,8 @@ private async Task SubmitAsync() Title = Blank(title), Race = Blank(race), Clan = Blank(clan), - Age = age, + Age = ageYears, Gender = Blank(gender), - Pronouns = Blank(pronouns), Archetype = archetype.Where(a => !string.IsNullOrWhiteSpace(a)).Select(a => a.Trim()).ToList(), Quote = Blank(quote), }, diff --git a/src/Beacon.Server/Data/BeaconDbContext.cs b/src/Beacon.Server/Data/BeaconDbContext.cs index 1876fd2..6fadf12 100644 --- a/src/Beacon.Server/Data/BeaconDbContext.cs +++ b/src/Beacon.Server/Data/BeaconDbContext.cs @@ -167,7 +167,6 @@ protected override void OnModelCreating(ModelBuilder b) e.Property(x => x.Race).HasMaxLength(32); e.Property(x => x.Clan).HasMaxLength(32); e.Property(x => x.Gender).HasMaxLength(32); - e.Property(x => x.Pronouns).HasMaxLength(32); e.Property(x => x.ArchetypeCsv).HasMaxLength(96); e.Property(x => x.Quote).HasMaxLength(200); e.Property(x => x.Boundaries).HasMaxLength(256); diff --git a/src/Beacon.Server/Data/Entities/ProfileEntity.cs b/src/Beacon.Server/Data/Entities/ProfileEntity.cs index 7ea2f36..e98c709 100644 --- a/src/Beacon.Server/Data/Entities/ProfileEntity.cs +++ b/src/Beacon.Server/Data/Entities/ProfileEntity.cs @@ -37,12 +37,10 @@ public class ProfileEntity public string? Clan { get; set; } - public AgeRange Age { get; set; } + public int? Age { get; set; } public string? Gender { get; set; } - public string? Pronouns { get; set; } - /// The three archetype words, pipe-delimited. Not searched, so no join table needed. public string ArchetypeCsv { get; set; } = string.Empty; diff --git a/src/Beacon.Server/Data/Migrations/20260921205722_ExactProfileAgeAndRemovePronouns.Designer.cs b/src/Beacon.Server/Data/Migrations/20260921205722_ExactProfileAgeAndRemovePronouns.Designer.cs new file mode 100644 index 0000000..9eed3f5 --- /dev/null +++ b/src/Beacon.Server/Data/Migrations/20260921205722_ExactProfileAgeAndRemovePronouns.Designer.cs @@ -0,0 +1,774 @@ +// +using System; +using Beacon.Server.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Beacon.Server.Data.Migrations +{ + [DbContext(typeof(BeaconDbContext))] + [Migration("20260921205722_ExactProfileAgeAndRemovePronouns")] + partial class ExactProfileAgeAndRemovePronouns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.10"); + + modelBuilder.Entity("Beacon.Server.Data.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AdultConfirmedAt") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsBanned") + .HasColumnType("INTEGER"); + + b.Property("IsModerator") + .HasColumnType("INTEGER"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AethernetShard") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("AllowPublicLighting") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DataCenter") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLit") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LastLitAt") + .HasColumnType("INTEGER"); + + b.Property("LitAt") + .HasColumnType("INTEGER"); + + b.Property("LitByAccountId") + .HasColumnType("TEXT"); + + b.Property("LitByName") + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("LitNote") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LitUntil") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MapX") + .HasColumnType("REAL"); + + b.Property("MapY") + .HasColumnType("REAL"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NearestAetheryteId") + .HasColumnType("INTEGER"); + + b.Property("NearestAetheryteName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("OwnerAccountId") + .HasColumnType("TEXT"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("ShareCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("SubAreaName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TagsCsv") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TerritoryId") + .HasColumnType("INTEGER"); + + b.Property("TimesLit") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.Property("WorldName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.Property("ZoneName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DataCenter"); + + b.HasIndex("OwnerAccountId"); + + b.HasIndex("ShareCode") + .IsUnique(); + + b.HasIndex("TerritoryId"); + + b.HasIndex("WorldId"); + + b.HasIndex("IsLit", "LitUntil"); + + b.HasIndex("IsDeleted", "Visibility", "IsLit"); + + b.ToTable("Beacons"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconImageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BeaconId") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("SizeBytes") + .HasColumnType("INTEGER"); + + b.Property("UploadedByAccountId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BeaconId"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconLightEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("BeaconId") + .HasColumnType("TEXT"); + + b.Property("CharacterName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("ExtinguishedAt") + .HasColumnType("INTEGER"); + + b.Property("LitAt") + .HasColumnType("INTEGER"); + + b.Property("LitUntil") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("BeaconId"); + + b.ToTable("Lights"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.CharacterEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("DataCenter") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("LinkedAt") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.Property("WorldName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Name", "WorldId") + .IsUnique(); + + b.ToTable("Characters"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.FavoriteEntity", b => + { + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("BeaconId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.HasKey("AccountId", "BeaconId"); + + b.HasIndex("BeaconId"); + + b.ToTable("Favorites"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Age") + .HasColumnType("INTEGER"); + + b.Property("ArchetypeCsv") + .IsRequired() + .HasMaxLength(96) + .HasColumnType("TEXT"); + + b.Property("Availability") + .HasColumnType("INTEGER"); + + b.Property("Boundaries") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("CharacterName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("Clan") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DataCenter") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Goals") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("History") + .HasMaxLength(4400) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsMature") + .HasColumnType("INTEGER"); + + b.Property("LastActiveAt") + .HasColumnType("INTEGER"); + + b.Property("Length") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(600) + .HasColumnType("TEXT"); + + b.Property("OwnerAccountId") + .HasColumnType("TEXT"); + + b.Property("PlayerAvailability") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("PlayerContact") + .HasMaxLength(96) + .HasColumnType("TEXT"); + + b.Property("PlayerTimezone") + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("PortraitImageId") + .HasColumnType("TEXT"); + + b.Property("Quote") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Race") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ShareCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(80) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.Property("WalkupsWelcome") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.Property("WorldName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DataCenter"); + + b.HasIndex("OwnerAccountId"); + + b.HasIndex("Race"); + + b.HasIndex("ShareCode") + .IsUnique(); + + b.HasIndex("CharacterName", "WorldId") + .IsUnique(); + + b.HasIndex("IsDeleted", "Visibility", "LastActiveAt"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileHookEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("ProfileHooks"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileImageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Caption") + .HasMaxLength(120) + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("ProfileId"); + + b.ToTable("ProfileImages"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileLinkEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConfirmedAt") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasMaxLength(160) + .HasColumnType("TEXT"); + + b.Property("OtherProfileId") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OtherProfileId"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "OtherProfileId") + .IsUnique(); + + b.ToTable("ProfileLinks"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileTagEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.HasIndex("Kind", "Value"); + + b.HasIndex("ProfileId", "Kind", "Value") + .IsUnique(); + + b.ToTable("ProfileTags"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ReportEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BeaconId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("ReporterAccountId") + .HasColumnType("TEXT"); + + b.Property("Resolved") + .HasColumnType("INTEGER"); + + b.Property("ResolvedAt") + .HasColumnType("INTEGER"); + + b.Property("ResolvedByAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BeaconId"); + + b.HasIndex("ProfileId"); + + b.HasIndex("Resolved", "CreatedAt"); + + b.ToTable("Reports"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.AccountEntity", "Owner") + .WithMany("Beacons") + .HasForeignKey("OwnerAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.CharacterEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.AccountEntity", "Account") + .WithMany("Characters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.FavoriteEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.BeaconEntity", "Beacon") + .WithMany("Favorites") + .HasForeignKey("BeaconId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Beacon"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.AccountEntity", "Owner") + .WithMany() + .HasForeignKey("OwnerAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileHookEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.ProfileEntity", "Profile") + .WithMany("Hooks") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileImageEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.ProfileEntity", "Profile") + .WithMany("Images") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileLinkEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.ProfileEntity", "Profile") + .WithMany() + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileTagEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.ProfileEntity", "Profile") + .WithMany("Tags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.AccountEntity", b => + { + b.Navigation("Beacons"); + + b.Navigation("Characters"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconEntity", b => + { + b.Navigation("Favorites"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileEntity", b => + { + b.Navigation("Hooks"); + + b.Navigation("Images"); + + b.Navigation("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Beacon.Server/Data/Migrations/20260921205722_ExactProfileAgeAndRemovePronouns.cs b/src/Beacon.Server/Data/Migrations/20260921205722_ExactProfileAgeAndRemovePronouns.cs new file mode 100644 index 0000000..a096367 --- /dev/null +++ b/src/Beacon.Server/Data/Migrations/20260921205722_ExactProfileAgeAndRemovePronouns.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Beacon.Server.Data.Migrations +{ + /// + public partial class ExactProfileAgeAndRemovePronouns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Pronouns", + table: "Profiles"); + + migrationBuilder.AlterColumn( + name: "Age", + table: "Profiles", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Age", + table: "Profiles", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Pronouns", + table: "Profiles", + type: "TEXT", + maxLength: 32, + nullable: true); + } + } +} diff --git a/src/Beacon.Server/Data/Migrations/20260921210236_ClearLegacyAgeCategories.Designer.cs b/src/Beacon.Server/Data/Migrations/20260921210236_ClearLegacyAgeCategories.Designer.cs new file mode 100644 index 0000000..33895e3 --- /dev/null +++ b/src/Beacon.Server/Data/Migrations/20260921210236_ClearLegacyAgeCategories.Designer.cs @@ -0,0 +1,774 @@ +// +using System; +using Beacon.Server.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Beacon.Server.Data.Migrations +{ + [DbContext(typeof(BeaconDbContext))] + [Migration("20260921210236_ClearLegacyAgeCategories")] + partial class ClearLegacyAgeCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.10"); + + modelBuilder.Entity("Beacon.Server.Data.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AdultConfirmedAt") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsBanned") + .HasColumnType("INTEGER"); + + b.Property("IsModerator") + .HasColumnType("INTEGER"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AethernetShard") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("AllowPublicLighting") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DataCenter") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLit") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LastLitAt") + .HasColumnType("INTEGER"); + + b.Property("LitAt") + .HasColumnType("INTEGER"); + + b.Property("LitByAccountId") + .HasColumnType("TEXT"); + + b.Property("LitByName") + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("LitNote") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LitUntil") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MapX") + .HasColumnType("REAL"); + + b.Property("MapY") + .HasColumnType("REAL"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NearestAetheryteId") + .HasColumnType("INTEGER"); + + b.Property("NearestAetheryteName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("OwnerAccountId") + .HasColumnType("TEXT"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("ShareCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("SubAreaName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TagsCsv") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TerritoryId") + .HasColumnType("INTEGER"); + + b.Property("TimesLit") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.Property("WorldName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.Property("ZoneName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DataCenter"); + + b.HasIndex("OwnerAccountId"); + + b.HasIndex("ShareCode") + .IsUnique(); + + b.HasIndex("TerritoryId"); + + b.HasIndex("WorldId"); + + b.HasIndex("IsLit", "LitUntil"); + + b.HasIndex("IsDeleted", "Visibility", "IsLit"); + + b.ToTable("Beacons"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconImageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BeaconId") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("SizeBytes") + .HasColumnType("INTEGER"); + + b.Property("UploadedByAccountId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BeaconId"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconLightEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("BeaconId") + .HasColumnType("TEXT"); + + b.Property("CharacterName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("ExtinguishedAt") + .HasColumnType("INTEGER"); + + b.Property("LitAt") + .HasColumnType("INTEGER"); + + b.Property("LitUntil") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("BeaconId"); + + b.ToTable("Lights"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.CharacterEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("DataCenter") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("LinkedAt") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.Property("WorldName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Name", "WorldId") + .IsUnique(); + + b.ToTable("Characters"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.FavoriteEntity", b => + { + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("BeaconId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.HasKey("AccountId", "BeaconId"); + + b.HasIndex("BeaconId"); + + b.ToTable("Favorites"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Age") + .HasColumnType("INTEGER"); + + b.Property("ArchetypeCsv") + .IsRequired() + .HasMaxLength(96) + .HasColumnType("TEXT"); + + b.Property("Availability") + .HasColumnType("INTEGER"); + + b.Property("Boundaries") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("CharacterName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("Clan") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DataCenter") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Goals") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("History") + .HasMaxLength(4400) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsMature") + .HasColumnType("INTEGER"); + + b.Property("LastActiveAt") + .HasColumnType("INTEGER"); + + b.Property("Length") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(600) + .HasColumnType("TEXT"); + + b.Property("OwnerAccountId") + .HasColumnType("TEXT"); + + b.Property("PlayerAvailability") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("PlayerContact") + .HasMaxLength(96) + .HasColumnType("TEXT"); + + b.Property("PlayerTimezone") + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.Property("PortraitImageId") + .HasColumnType("TEXT"); + + b.Property("Quote") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Race") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ShareCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(80) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.Property("WalkupsWelcome") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.Property("WorldName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DataCenter"); + + b.HasIndex("OwnerAccountId"); + + b.HasIndex("Race"); + + b.HasIndex("ShareCode") + .IsUnique(); + + b.HasIndex("CharacterName", "WorldId") + .IsUnique(); + + b.HasIndex("IsDeleted", "Visibility", "LastActiveAt"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileHookEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("ProfileHooks"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileImageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Caption") + .HasMaxLength(120) + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("ProfileId"); + + b.ToTable("ProfileImages"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileLinkEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConfirmedAt") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasMaxLength(160) + .HasColumnType("TEXT"); + + b.Property("OtherProfileId") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OtherProfileId"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "OtherProfileId") + .IsUnique(); + + b.ToTable("ProfileLinks"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileTagEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.HasIndex("Kind", "Value"); + + b.HasIndex("ProfileId", "Kind", "Value") + .IsUnique(); + + b.ToTable("ProfileTags"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ReportEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BeaconId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("ReporterAccountId") + .HasColumnType("TEXT"); + + b.Property("Resolved") + .HasColumnType("INTEGER"); + + b.Property("ResolvedAt") + .HasColumnType("INTEGER"); + + b.Property("ResolvedByAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BeaconId"); + + b.HasIndex("ProfileId"); + + b.HasIndex("Resolved", "CreatedAt"); + + b.ToTable("Reports"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.AccountEntity", "Owner") + .WithMany("Beacons") + .HasForeignKey("OwnerAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.CharacterEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.AccountEntity", "Account") + .WithMany("Characters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.FavoriteEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.BeaconEntity", "Beacon") + .WithMany("Favorites") + .HasForeignKey("BeaconId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Beacon"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.AccountEntity", "Owner") + .WithMany() + .HasForeignKey("OwnerAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileHookEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.ProfileEntity", "Profile") + .WithMany("Hooks") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileImageEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.ProfileEntity", "Profile") + .WithMany("Images") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileLinkEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.ProfileEntity", "Profile") + .WithMany() + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileTagEntity", b => + { + b.HasOne("Beacon.Server.Data.Entities.ProfileEntity", "Profile") + .WithMany("Tags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.AccountEntity", b => + { + b.Navigation("Beacons"); + + b.Navigation("Characters"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.BeaconEntity", b => + { + b.Navigation("Favorites"); + }); + + modelBuilder.Entity("Beacon.Server.Data.Entities.ProfileEntity", b => + { + b.Navigation("Hooks"); + + b.Navigation("Images"); + + b.Navigation("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Beacon.Server/Data/Migrations/20260921210236_ClearLegacyAgeCategories.cs b/src/Beacon.Server/Data/Migrations/20260921210236_ClearLegacyAgeCategories.cs new file mode 100644 index 0000000..9365822 --- /dev/null +++ b/src/Beacon.Server/Data/Migrations/20260921210236_ClearLegacyAgeCategories.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Beacon.Server.Data.Migrations +{ + /// + public partial class ClearLegacyAgeCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // The old values were enum ordinals (YoungAdult = 1, Adult = 2, and so on), not years. + // Clear them after the preceding migration makes Age nullable. + migrationBuilder.Sql("UPDATE \"Profiles\" SET \"Age\" = NULL;"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Exact ages cannot be mapped back to the removed categories without inventing data. + } + } +} diff --git a/src/Beacon.Server/Data/Migrations/BeaconDbContextModelSnapshot.cs b/src/Beacon.Server/Data/Migrations/BeaconDbContextModelSnapshot.cs index 5f2bf91..ad4923f 100644 --- a/src/Beacon.Server/Data/Migrations/BeaconDbContextModelSnapshot.cs +++ b/src/Beacon.Server/Data/Migrations/BeaconDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Beacon.Server.Data; using Microsoft.EntityFrameworkCore; @@ -356,7 +356,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("Age") + b.Property("Age") .HasColumnType("INTEGER"); b.Property("ArchetypeCsv") @@ -439,10 +439,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PortraitImageId") .HasColumnType("TEXT"); - b.Property("Pronouns") - .HasMaxLength(32) - .HasColumnType("TEXT"); - b.Property("Quote") .HasMaxLength(200) .HasColumnType("TEXT"); diff --git a/src/Beacon.Server/Data/ProfileMapper.cs b/src/Beacon.Server/Data/ProfileMapper.cs index 9a4b0fc..56fd8fb 100644 --- a/src/Beacon.Server/Data/ProfileMapper.cs +++ b/src/Beacon.Server/Data/ProfileMapper.cs @@ -91,7 +91,6 @@ public static ProfileDto ToDto( Clan = e.Clan, Age = e.Age, Gender = e.Gender, - Pronouns = e.Pronouns, Archetype = SplitArchetype(e.ArchetypeCsv), Quote = e.Quote, }, diff --git a/src/Beacon.Server/Services/ProfileService.cs b/src/Beacon.Server/Services/ProfileService.cs index 9fccb07..73c7316 100644 --- a/src/Beacon.Server/Services/ProfileService.cs +++ b/src/Beacon.Server/Services/ProfileService.cs @@ -718,7 +718,6 @@ private static void Apply(ProfileEntity profile, SaveProfileRequest request) profile.Clan = NullIfEmpty(Validation.CleanLine(identity.Clan)); profile.Age = identity.Age; profile.Gender = NullIfEmpty(Validation.CleanLine(identity.Gender)); - profile.Pronouns = NullIfEmpty(Validation.CleanLine(identity.Pronouns)); profile.ArchetypeCsv = ProfileMapper.JoinArchetype(identity.Archetype); profile.Quote = NullIfEmpty(Validation.CleanLine(identity.Quote)); @@ -773,6 +772,9 @@ private static List CleanHooks(Guid profileId, IReadOnlyList< if (name.Length > ProfileLimits.NameMaxLength) return $"A character name cannot exceed {ProfileLimits.NameMaxLength} characters."; + if (request.Identity.Age is { } age && (age < ProfileLimits.AgeMin || age > ProfileLimits.AgeMax)) + return $"Age must be between {ProfileLimits.AgeMin} and {ProfileLimits.AgeMax}."; + if ((request.Overview?.Length ?? 0) > ProfileLimits.OverviewMaxLength) return $"The overview cannot exceed {ProfileLimits.OverviewMaxLength} characters."; diff --git a/src/Beacon.Shared/ApiRoutes.cs b/src/Beacon.Shared/ApiRoutes.cs index 43183af..4d5ecc8 100644 --- a/src/Beacon.Shared/ApiRoutes.cs +++ b/src/Beacon.Shared/ApiRoutes.cs @@ -16,7 +16,7 @@ public static class ApiRoutes public const string ProtocolHeader = "X-Beacon-Protocol"; /// Bump when a change to the shared contracts is not backwards compatible. - public const int ProtocolVersion = 2; + public const int ProtocolVersion = 3; /// Liveness probe, unauthenticated. Also reports the server's protocol range. public const string Health = "/health"; diff --git a/src/Beacon.Shared/Profiles/ProfileDto.cs b/src/Beacon.Shared/Profiles/ProfileDto.cs index df33ea6..6ade0cd 100644 --- a/src/Beacon.Shared/Profiles/ProfileDto.cs +++ b/src/Beacon.Shared/Profiles/ProfileDto.cs @@ -15,12 +15,11 @@ public sealed record ProfileIdentity public string? Clan { get; init; } - public AgeRange Age { get; init; } + /// Optional exact character age in years. + public int? Age { get; init; } public string? Gender { get; init; } - public string? Pronouns { get; init; } - /// The three words that set the tone before anybody reads a word of prose. public IReadOnlyList Archetype { get; init; } = []; diff --git a/src/Beacon.Shared/Profiles/ProfileEnums.cs b/src/Beacon.Shared/Profiles/ProfileEnums.cs index a09a00c..52c2171 100644 --- a/src/Beacon.Shared/Profiles/ProfileEnums.cs +++ b/src/Beacon.Shared/Profiles/ProfileEnums.cs @@ -149,25 +149,6 @@ public enum AvailabilityState Closed = 4, } -/// -/// Age brackets. -/// -/// There is deliberately no bracket below adult. This is a matchmaking system whose tone tags include -/// romance, and the pairing of a child-coded character with those tags is the single most common thing -/// a moderator ends up having to act on. Not offering the bracket is cheaper than policing it. -/// -public enum AgeRange -{ - Unspecified = 0, - YoungAdult = 1, - Adult = 2, - MiddleAged = 3, - Elder = 4, - - /// Immortal, unaging, or simply nobody's business. - Ageless = 5, -} - /// /// Gallery slots. Four, not nine. /// diff --git a/src/Beacon.Shared/Profiles/ProfileLimits.cs b/src/Beacon.Shared/Profiles/ProfileLimits.cs index 1446bb4..5842549 100644 --- a/src/Beacon.Shared/Profiles/ProfileLimits.cs +++ b/src/Beacon.Shared/Profiles/ProfileLimits.cs @@ -13,6 +13,9 @@ public static class ProfileLimits /// The epithet under the name. "The Wolf Without a Den". public const int TitleMaxLength = 60; + public const int AgeMin = 18; + public const int AgeMax = 9999; + /// The one line in quotes on the card. public const int QuoteMaxLength = 160; diff --git a/tools/smoke-server.ps1 b/tools/smoke-server.ps1 index 16b5358..259e259 100644 --- a/tools/smoke-server.ps1 +++ b/tools/smoke-server.ps1 @@ -9,6 +9,7 @@ $scratch = Join-Path ([System.IO.Path]::GetTempPath()) ("beacon-smoke-" + [guid] $published = Join-Path $scratch 'server' $data = Join-Path $scratch 'data' $process = $null +$protocol = 3 function Get-FreeLoopbackPort { $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) @@ -56,7 +57,7 @@ function Wait-ForHealth([System.Net.Http.HttpClient]$Client, [string]$BaseUrl) { $response = $Client.GetAsync("$BaseUrl/health").GetAwaiter().GetResult() if ($response.IsSuccessStatusCode) { $health = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() | ConvertFrom-Json - if ($health.status -eq 'ok' -and $health.protocol -eq 2) { + if ($health.status -eq 'ok' -and $health.protocol -eq $protocol) { return } } @@ -95,7 +96,7 @@ try { throw "Protocol guard returned $([int]$withoutProtocol.StatusCode), expected 426." } - $client.DefaultRequestHeaders.Add('X-Beacon-Protocol', '2') + $client.DefaultRequestHeaders.Add('X-Beacon-Protocol', [string]$protocol) $registrationBody = [System.Net.Http.StringContent]::new( '{"displayName":"Release Smoke"}', [System.Text.Encoding]::UTF8, diff --git a/tools/verify-release.ps1 b/tools/verify-release.ps1 index 4189f51..de561d9 100644 --- a/tools/verify-release.ps1 +++ b/tools/verify-release.ps1 @@ -36,6 +36,28 @@ try { if ($profileEditor -match 'CapturePortraitAsync' -or $profileEditor -match 'screenshots\.CaptureAsync\(') { throw 'The profile editor must upload chosen files instead of capturing the viewport.' } + if ($profileEditor -match 'pronouns' -or $profileEditor -match 'Age range' -or $profileEditor -match 'A bracket, not a number') { + throw 'Removed profile fields or cryptic age guidance have returned to the editor.' + } + + $profileContract = Get-Content -LiteralPath 'src/Beacon.Shared/Profiles/ProfileDto.cs' -Raw + if ($profileContract -notmatch 'int\?\s+Age' -or $profileContract -match 'Pronouns') { + throw 'The profile contract must use an optional exact age and must not expose pronouns.' + } + $profileLimits = Get-Content -LiteralPath 'src/Beacon.Shared/Profiles/ProfileLimits.cs' -Raw + if ($profileLimits -notmatch 'AgeMin\s*=\s*18' -or $profileLimits -notmatch 'AgeMax\s*=\s*9999') { + throw 'The exact-age safety bounds are missing.' + } + $ageSchemaMigration = Get-Content -LiteralPath 'src/Beacon.Server/Data/Migrations/20260921205722_ExactProfileAgeAndRemovePronouns.cs' -Raw + foreach ($required in @('DropColumn(', 'name: "Pronouns"', 'nullable: true')) { + if ($ageSchemaMigration.IndexOf($required, [StringComparison]::Ordinal) -lt 0) { + throw "The exact-age migration is incomplete: $required" + } + } + $ageDataMigration = Get-Content -LiteralPath 'src/Beacon.Server/Data/Migrations/20260921210236_ClearLegacyAgeCategories.cs' -Raw + if ($ageDataMigration -notmatch 'SET.+Age.+NULL') { + throw 'The legacy age-category cleanup migration is missing.' + } foreach ($required in @( 'CreateFromImageAsync', 'CropToPngAsync',