diff --git a/Areas/Admin/Views/ApiToken/Index.cshtml b/Areas/Admin/Views/ApiToken/Index.cshtml
index 83718719..5453cb97 100644
--- a/Areas/Admin/Views/ApiToken/Index.cshtml
+++ b/Areas/Admin/Views/ApiToken/Index.cshtml
@@ -31,7 +31,7 @@
@@ -623,7 +623,7 @@
@:var newTokenFromTempData = "@Html.Raw(newToken ?? "")";
@:var newTokenNameFromTempData = "@Html.Raw(newTokenName ?? "")";
@:var isHashedToken = @(token.IsHashedToken ? "true" : "false");
- @:var plainToken = "@Html.Raw(token.Token ?? "")";
+ @:var plainToken = "";
@:var serverBaseUrl = "@serverBaseUrl";
@:var userName = "@Model.UserName";
@:var serverName = "@Context.Request.Host.Host";
diff --git a/Areas/User/Views/LocationProviderSettings/Index.cshtml b/Areas/User/Views/LocationProviderSettings/Index.cshtml
new file mode 100644
index 00000000..af4be33f
--- /dev/null
+++ b/Areas/User/Views/LocationProviderSettings/Index.cshtml
@@ -0,0 +1,44 @@
+@model Wayfarer.Areas.User.LocationProviderModels.LocationProviderSettingsViewModel
+
+
Personal location providers
+
Credentials are protected and never displayed again or sent to WayfarerMobile. Read the credential and usage guide.
+@if (Model.LegacyMigrationState is Wayfarer.Models.LocationProviders.LegacyMapboxMigrationState.Conflict or Wayfarer.Models.LocationProviders.LegacyMapboxMigrationState.ProtectedCredentialUnavailable)
+{
Legacy Mapbox migration needs explicit recovery. No provider contact is authorized and no stored value was removed.
}
+
Wayfarer records only its own contacts. Other applications can consume the provider account allowance; use a dedicated Wayfarer key when possible. Multiple keys may still share one provider allowance.
+@foreach (var profile in Model.Profiles)
+{
+
+
@profile.DisplayName
+
Credential: @(profile.CredentialConfigured ? profile.Mask + " (configured)" : "not configured")
+
+ @if (profile.ProviderKey == "mapbox")
+ {
+
Directions guard: @(profile.DirectionsGuardEnabled == true ? "enabled" : "disabled") — @profile.DirectionsUsed / @profile.DirectionsLimit contacts. Separate Wayfarer UTC calendar-month safety cycle.
+
+ }
+
Guard: @(profile.GuardEnabled ? "enabled" : "disabled") — @profile.Used / @profile.Limit @profile.Unit. @profile.WindowExplanation. @(profile.Exhausted ? "New contacts are paused; stored data remains available." : "")
+
+ @if (profile.CredentialConfigured)
+ {
}
+
+}
+
Cache and stored-result reuse costs no new credit. Exhaustion pauses new contacts without deleting source or historical data. Disabling a guard may incur paid usage. Imports and backfills share the same remaining allowance.
diff --git a/Areas/User/Views/Settings/Index.cshtml b/Areas/User/Views/Settings/Index.cshtml
index 8594931b..56a45fed 100644
--- a/Areas/User/Views/Settings/Index.cshtml
+++ b/Areas/User/Views/Settings/Index.cshtml
@@ -131,6 +131,7 @@
diff --git a/Migrations/20260823091629_AddPersonalLocationProviderFoundation.Designer.cs b/Migrations/20260823091629_AddPersonalLocationProviderFoundation.Designer.cs
new file mode 100644
index 00000000..6699ddd3
--- /dev/null
+++ b/Migrations/20260823091629_AddPersonalLocationProviderFoundation.Designer.cs
@@ -0,0 +1,2313 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NetTopologySuite.Geometries;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Wayfarer.Models;
+
+#nullable disable
+
+namespace Wayfarer.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260823091629_AddPersonalLocationProviderFoundation")]
+ partial class AddPersonalLocationProviderFoundation
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ApplicationSettings", b =>
+ {
+ b.Property
("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ActiveRoutingProviderConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("ExternalRouteGenerationEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("ExternalRouteGenerationVersion")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
+ b.Property("ImageCacheExpiryDays")
+ .HasColumnType("integer");
+
+ b.Property("IsRegistrationOpen")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("LocationAccuracyThresholdMeters")
+ .HasColumnType("integer");
+
+ b.Property("LocationDistanceThresholdMeters")
+ .HasColumnType("integer");
+
+ b.Property("LocationTimeThresholdMinutes")
+ .HasColumnType("integer");
+
+ b.Property("MaxCacheImageSizeInMB")
+ .HasColumnType("integer");
+
+ b.Property("MaxCacheTileSizeInMB")
+ .HasColumnType("integer");
+
+ b.Property("MaxProxyImageDownloadMB")
+ .HasColumnType("integer");
+
+ b.Property("ProxyImageRateLimitEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("ProxyImageRateLimitPerMinute")
+ .HasColumnType("integer");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("TileMetadataHotCacheSizeMB")
+ .HasColumnType("integer");
+
+ b.Property("TileOutboundBudgetHistorical30Acknowledged")
+ .HasColumnType("boolean");
+
+ b.Property("TileOutboundBudgetPerIpPerMinute")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderAdvancedLimitsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("TileProviderApiKey")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("TileProviderAttribution")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("TileProviderBurstCapacity")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderFallbackBaseDelayMs")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderFallbackDelayCapSeconds")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderKey")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("character varying(40)");
+
+ b.Property("TileProviderMaxAttempts")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderMaxConcurrency")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderMaxIndividualWaitSeconds")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderSustainedRequestsPerSecond")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderTotalRetryCeilingSeconds")
+ .HasColumnType("integer");
+
+ b.Property("TileProviderUrlTemplate")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("TileRateLimitAuthenticatedPerMinute")
+ .HasColumnType("integer");
+
+ b.Property("TileRateLimitEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("TileRateLimitPerMinute")
+ .HasColumnType("integer");
+
+ b.Property("TileTrafficMode")
+ .HasColumnType("integer");
+
+ b.Property("UploadSizeLimitMB")
+ .HasColumnType("integer");
+
+ b.Property("VisitNotificationCooldownHours")
+ .HasColumnType("integer");
+
+ b.Property("VisitedAccuracyMultiplier")
+ .HasColumnType("double precision");
+
+ b.Property("VisitedAccuracyRejectMeters")
+ .HasColumnType("integer");
+
+ b.Property("VisitedMaxRadiusMeters")
+ .HasColumnType("integer");
+
+ b.Property("VisitedMaxSearchRadiusMeters")
+ .HasColumnType("integer");
+
+ b.Property("VisitedMinRadiusMeters")
+ .HasColumnType("integer");
+
+ b.Property("VisitedPlaceNotesSnapshotMaxHtmlChars")
+ .HasColumnType("integer");
+
+ b.Property("VisitedRequiredHits")
+ .HasColumnType("integer");
+
+ b.Property("VisitedSuggestionMaxRadiusMultiplier")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ActiveRoutingProviderConfigurationId");
+
+ b.ToTable("ApplicationSettings");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ 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("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")
+ .IsRequired()
+ .HasColumnType("text");
+
+ 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")
+ .IsRequired()
+ .HasColumnType("text");
+
+ 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")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ 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("TripTags", b =>
+ {
+ b.Property("TripId")
+ .HasColumnType("uuid");
+
+ b.Property("TagId")
+ .HasColumnType("uuid");
+
+ b.HasKey("TripId", "TagId");
+
+ b.HasIndex("TagId");
+
+ b.HasIndex("TripId");
+
+ b.ToTable("TripTags", (string)null);
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.ActivityType", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("ActivityTypes");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.ApiToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Token")
+ .HasColumnType("text");
+
+ b.Property("TokenHash")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Name", "UserId")
+ .IsUnique()
+ .HasDatabaseName("IX_ApiToken_Name_UserId");
+
+ b.ToTable("ApiTokens");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ 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("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("IsProtected")
+ .HasColumnType("boolean");
+
+ b.Property("IsTimelinePublic")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ 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("PublicTimelineTimeThreshold")
+ .HasColumnType("text");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TimelineTitle")
+ .HasMaxLength(80)
+ .HasColumnType("character varying(80)");
+
+ 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.HasIndex("UserName")
+ .IsUnique();
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.Area", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("DisplayOrder")
+ .HasColumnType("integer");
+
+ b.Property("FillHex")
+ .HasColumnType("text");
+
+ b.Property("Geometry")
+ .IsRequired()
+ .HasColumnType("geometry");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("RegionId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RegionId");
+
+ b.ToTable("Areas");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.AuditLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Details")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("AuditLogs");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.Group", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("GroupType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("IsArchived")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("OrgPeerVisibilityEnabled")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("OwnerUserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerUserId", "Name")
+ .IsUnique();
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.GroupInvitation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GroupId")
+ .HasColumnType("uuid");
+
+ b.Property("InviteeEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("InviteeUserId")
+ .HasColumnType("text");
+
+ b.Property("InviterUserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("RespondedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("character varying(40)");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InviteeUserId");
+
+ b.HasIndex("InviterUserId");
+
+ b.HasIndex("Token")
+ .IsUnique();
+
+ b.HasIndex("GroupId", "InviteeUserId")
+ .IsUnique()
+ .HasDatabaseName("IX_GroupInvitation_GroupId_InviteeUserId_Pending")
+ .HasFilter("\"Status\" = 'Pending' AND \"InviteeUserId\" IS NOT NULL");
+
+ b.ToTable("GroupInvitations");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.GroupMember", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("GroupId")
+ .HasColumnType("uuid");
+
+ b.Property("JoinedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("LeftAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("OrgPeerVisibilityAccessDisabled")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("character varying(40)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("character varying(40)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("GroupId", "Status")
+ .HasDatabaseName("IX_GroupMember_GroupId_Status");
+
+ b.HasIndex("GroupId", "UserId")
+ .IsUnique();
+
+ b.ToTable("GroupMembers");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.HiddenArea", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Area")
+ .IsRequired()
+ .HasColumnType("geometry");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("HiddenAreas");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.ImageCacheMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CacheKey")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("FilePath")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("LastAccessed")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Size")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CacheKey")
+ .IsUnique()
+ .HasDatabaseName("IX_ImageCacheMetadata_CacheKey");
+
+ b.ToTable("ImageCacheMetadata");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.JobHistory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("JobName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("LastRunTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("JobHistories");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.Location", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Accuracy")
+ .HasColumnType("double precision");
+
+ b.Property("ActivityTypeId")
+ .HasColumnType("integer");
+
+ b.Property("Address")
+ .HasColumnType("text");
+
+ b.Property("AddressNumber")
+ .HasColumnType("text");
+
+ b.Property("Altitude")
+ .HasColumnType("double precision");
+
+ b.Property("AppBuild")
+ .HasColumnType("text");
+
+ b.Property("AppVersion")
+ .HasColumnType("text");
+
+ b.Property("BatteryLevel")
+ .HasColumnType("integer");
+
+ b.Property("Bearing")
+ .HasColumnType("double precision");
+
+ b.Property("Coordinates")
+ .IsRequired()
+ .HasColumnType("geography(Point, 4326)");
+
+ b.Property("Country")
+ .HasColumnType("text");
+
+ b.Property("DeviceModel")
+ .HasColumnType("text");
+
+ b.Property("FullAddress")
+ .HasColumnType("text");
+
+ b.Property("IdempotencyKey")
+ .HasColumnType("uuid");
+
+ b.Property("IsCharging")
+ .HasColumnType("boolean");
+
+ b.Property("IsUserInvoked")
+ .HasColumnType("boolean");
+
+ b.Property("LocalTimestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LocationType")
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("OsVersion")
+ .HasColumnType("text");
+
+ b.Property("Place")
+ .HasColumnType("text");
+
+ b.Property("PostCode")
+ .HasColumnType("text");
+
+ b.Property("Provider")
+ .HasColumnType("text");
+
+ b.Property("Region")
+ .HasColumnType("text");
+
+ b.Property("Source")
+ .HasColumnType("text");
+
+ b.Property("Speed")
+ .HasColumnType("double precision");
+
+ b.Property("StreetName")
+ .HasColumnType("text");
+
+ b.Property("TimeZoneId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ActivityTypeId");
+
+ b.HasIndex("Coordinates")
+ .HasDatabaseName("IX_Location_Coordinates");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Coordinates"), "GIST");
+
+ b.HasIndex("UserId", "IdempotencyKey")
+ .IsUnique()
+ .HasDatabaseName("IX_Location_UserId_IdempotencyKey");
+
+ b.ToTable("Locations");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.LocationImport", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("ErrorMessage")
+ .HasColumnType("text");
+
+ b.Property("FilePath")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FileType")
+ .HasColumnType("integer");
+
+ b.Property("LastImportedRecord")
+ .HasColumnType("text");
+
+ b.Property("LastProcessedIndex")
+ .HasColumnType("integer");
+
+ b.Property("SkippedDuplicates")
+ .HasColumnType("integer");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("TotalRecords")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("LocationImports");
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.LocationProviders.GeoapifyUsageAdmission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdmittedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("clock_timestamp()");
+
+ b.Property("Credits")
+ .HasColumnType("integer");
+
+ b.Property("Product")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "AdmittedAt");
+
+ b.ToTable("GeoapifyUsageAdmissions", t =>
+ {
+ t.HasCheckConstraint("CK_GeoapifyUsageAdmission_Credits", "\"Credits\" > 0");
+
+ t.HasCheckConstraint("CK_GeoapifyUsageAdmission_Product", "\"Product\" IN (1, 2)");
+ });
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.LocationProviders.GeoapifyUsageGuard", b =>
+ {
+ b.Property("UserId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("CreditLimit")
+ .HasColumnType("integer");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.HasKey("UserId");
+
+ b.ToTable("GeoapifyUsageGuards", t =>
+ {
+ t.HasCheckConstraint("CK_GeoapifyUsageGuard_Limit", "\"CreditLimit\" >= 0");
+ });
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.LocationProviders.MapboxProductMeter", b =>
+ {
+ b.Property("UserId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Product")
+ .HasColumnType("integer");
+
+ b.Property("AdmittedCount")
+ .HasColumnType("integer");
+
+ b.Property("CycleStart")
+ .HasColumnType("date");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("Limit")
+ .HasColumnType("integer");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.HasKey("UserId", "Product");
+
+ b.ToTable("MapboxProductMeters", t =>
+ {
+ t.HasCheckConstraint("CK_MapboxProductMeter_Counts", "\"Limit\" >= 0 AND \"AdmittedCount\" >= 0");
+
+ t.HasCheckConstraint("CK_MapboxProductMeter_Product", "\"Product\" IN (3, 4)");
+ });
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.LocationProviders.PersonalLocationProviderProfile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CredentialGeneration")
+ .HasColumnType("integer");
+
+ b.Property("GeocodingAuthorized")
+ .HasColumnType("boolean");
+
+ b.Property("GeocodingGeneration")
+ .HasColumnType("integer");
+
+ b.Property("GeocodingVerification")
+ .HasColumnType("integer");
+
+ b.Property("GeocodingVerifiedConfigurationGeneration")
+ .HasColumnType("integer");
+
+ b.Property("GeocodingVerifiedCredentialGeneration")
+ .HasColumnType("integer");
+
+ b.Property("LegacyMigrationState")
+ .HasColumnType("integer");
+
+ b.Property("ProtectedCredential")
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)");
+
+ b.Property("ProviderKey")
+ .IsRequired()
+ .HasMaxLength(24)
+ .HasColumnType("character varying(24)");
+
+ b.Property("RevokedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RoutingAuthorized")
+ .HasColumnType("boolean");
+
+ b.Property("RoutingGeneration")
+ .HasColumnType("integer");
+
+ b.Property("RoutingVerification")
+ .HasColumnType("integer");
+
+ b.Property("RoutingVerifiedConfigurationGeneration")
+ .HasColumnType("integer");
+
+ b.Property("RoutingVerifiedCredentialGeneration")
+ .HasColumnType("integer");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ProviderKey")
+ .IsUnique();
+
+ b.ToTable("PersonalLocationProviderProfiles", t =>
+ {
+ t.HasCheckConstraint("CK_PersonalProvider_Generations", "\"CredentialGeneration\" > 0 AND \"GeocodingGeneration\" > 0 AND \"RoutingGeneration\" > 0");
+
+ t.HasCheckConstraint("CK_PersonalProvider_Provider", "\"ProviderKey\" IN ('geoapify', 'mapbox')");
+
+ t.HasCheckConstraint("CK_PersonalProvider_Verification", "\"GeocodingVerification\" BETWEEN 0 AND 3 AND \"RoutingVerification\" BETWEEN 0 AND 3");
+ });
+ });
+
+ modelBuilder.Entity("Wayfarer.Models.LocationProviders.PersonalLocationProviderSelection", b =>
+ {
+ b.Property("UserId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("GeocodingProviderKey")
+ .HasMaxLength(24)
+ .HasColumnType("character varying(24)");
+
+ b.Property("GeocodingSelectionGeneration")
+ .HasColumnType("integer");
+
+ b.Property