diff --git a/.forgejo/workflows/publish-container.yml b/.forgejo/workflows/publish-container.yml
index 2c37c3d..e7aefd3 100644
--- a/.forgejo/workflows/publish-container.yml
+++ b/.forgejo/workflows/publish-container.yml
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
- DOTNET_VERSION: 10.0.x
+ DOTNET_VERSION: 9.0.x
REGISTRY: ${{ vars.FORGEJO_REGISTRY }}
IMAGE_NAMESPACE: ${{ vars.IMAGE_NAMESPACE }}
IMAGE_NAME: ${{ vars.IMAGE_NAME }}
diff --git a/.gitignore b/.gitignore
index 988f615..065c614 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,8 @@ obj/
# VS Code
.vscode/
!.vscode/extensions.json
+!.vscode/launch.json
+!.vscode/tasks.json
# JetBrains
.idea/
@@ -22,6 +24,8 @@ Data/*.db
Data/*.db-*
*.sqlite
*.sqlite3
+App_Data/
+.docker-data/
# Secrets and environment files
.env
diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor
index c7aca4a..ff782f1 100644
--- a/Components/Pages/Home.razor
+++ b/Components/Pages/Home.razor
@@ -4,7 +4,7 @@
WorkTracker
-Phase 1 baseline is active: authentication, locale defaults, and configurable settings with MongoDB storage.
+Phase 1 baseline is active: authentication, locale defaults, and configurable settings with local Couchbase Lite storage.
diff --git a/Configuration/CouchbaseLiteOptions.cs b/Configuration/CouchbaseLiteOptions.cs
new file mode 100644
index 0000000..17c9ef7
--- /dev/null
+++ b/Configuration/CouchbaseLiteOptions.cs
@@ -0,0 +1,10 @@
+namespace WorkTracker.Configuration;
+
+public sealed class CouchbaseLiteOptions
+{
+ public const string SectionName = "CouchbaseLite";
+
+ public string DatabaseName { get; init; } = "worktracker";
+
+ public string Directory { get; init; } = "App_Data/couchbase";
+}
\ No newline at end of file
diff --git a/Configuration/MongoDbOptions.cs b/Configuration/MongoDbOptions.cs
deleted file mode 100644
index 1e0f862..0000000
--- a/Configuration/MongoDbOptions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace WorkTracker.Configuration;
-
-public sealed class MongoDbOptions
-{
- public const string SectionName = "MongoDb";
-
- public string ConnectionString { get; init; } = "mongodb://localhost:27017";
-
- public string DatabaseName { get; init; } = "worktracker";
-}
diff --git a/Dockerfile b/Dockerfile
index 05eda42..d452343 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS dev
+FROM mcr.microsoft.com/dotnet/sdk:9.0-noble AS dev
WORKDIR /workspace
RUN apt-get update \
@@ -10,7 +10,7 @@ ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
-FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
+FROM mcr.microsoft.com/dotnet/sdk:9.0-noble AS build
WORKDIR /src
COPY ["WorkTracker.csproj", "./"]
@@ -19,18 +19,21 @@ RUN dotnet restore "WorkTracker.csproj"
COPY . .
RUN dotnet publish "WorkTracker.csproj" -c Release -o /app/publish /p:UseAppHost=false
-FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS final
+FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble AS final
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends wget \
+ && mkdir -p /data/couchbase \
&& rm -rf /var/lib/apt/lists/*
ENV ASPNETCORE_URLS=http://+:8080
+ENV CouchbaseLite__Directory=/data/couchbase
COPY --from=build /app/publish .
EXPOSE 8080
+VOLUME ["/data/couchbase"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:8080/ >/dev/null 2>&1 || exit 1
diff --git a/Domain/AppSettingsDocument.cs b/Domain/AppSettingsDocument.cs
index dbd25e5..d7fa2a1 100644
--- a/Domain/AppSettingsDocument.cs
+++ b/Domain/AppSettingsDocument.cs
@@ -1,10 +1,7 @@
-using MongoDB.Bson.Serialization.Attributes;
-
namespace WorkTracker.Domain;
public sealed class AppSettingsDocument
{
- [BsonId]
public string Id { get; set; } = "global";
public decimal StandardWorkHoursPerDay { get; set; } = 8m;
diff --git a/Program.cs b/Program.cs
index 72a2d19..06dbbe3 100644
--- a/Program.cs
+++ b/Program.cs
@@ -5,12 +5,12 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options;
-using MongoDB.Driver;
using WorkTracker.Components;
using WorkTracker.Configuration;
using WorkTracker.Services.Auth;
using WorkTracker.Services.Festivities;
using WorkTracker.Services.Settings;
+using WorkTracker.Services.Storage;
var builder = WebApplication.CreateBuilder(args);
@@ -33,24 +33,12 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
builder.Services.AddLocalization();
-builder.Services.Configure
(builder.Configuration.GetSection(MongoDbOptions.SectionName));
+builder.Services.Configure(builder.Configuration.GetSection(CouchbaseLiteOptions.SectionName));
builder.Services.Configure(builder.Configuration.GetSection(SingleUserOptions.SectionName));
-builder.Services.AddSingleton(sp =>
-{
- var options = sp.GetRequiredService>().Value;
- return new MongoClient(options.ConnectionString);
-});
-
-builder.Services.AddSingleton(sp =>
-{
- var options = sp.GetRequiredService>().Value;
- var mongoClient = sp.GetRequiredService();
- return mongoClient.GetDatabase(options.DatabaseName);
-});
-
-builder.Services.AddScoped();
-builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddScoped();
+builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddHostedService();
@@ -88,7 +76,7 @@ app.UseAuthorization();
app.UseAntiforgery();
-app.MapPost("/login", async (HttpContext context, IMongoAuthService authService) =>
+app.MapPost("/login", async (HttpContext context, IAuthService authService) =>
{
var form = await context.Request.ReadFormAsync();
var email = form["email"].ToString();
diff --git a/README.Docker.md b/README.Docker.md
index 6100df2..df5160d 100644
--- a/README.Docker.md
+++ b/README.Docker.md
@@ -1,4 +1,4 @@
-Running and debugging with Docker (includes MongoDB)
+Running and debugging with Docker or local Couchbase Lite storage
Quick run (Docker Engine required):
@@ -8,12 +8,22 @@ Quick run (Docker Engine required):
2. App will be available on host port 8002 -> container 8080 (http://localhost:8002).
-Development in VS Code (F5):
+Docker persistence:
-- The repository uses `docker-compose.yml` plus `docker-compose.override.yml` as the single development stack.
-- The override file switches the app container to the SDK-based `dev` image, mounts the workspace into `/workspace`, installs `vsdbg`, and runs `dotnet watch`.
-- Press F5 in VS Code with the `Docker: Attach .NET in Compose - F5` configuration selected.
-- VS Code starts Docker Compose, waits for `http://localhost:8002`, opens the browser, and attaches the debugger to the `worktracker-dev` container.
+- The app now uses an embedded Couchbase Lite database stored under `/data/couchbase` inside the container.
+- The compose file mounts that path from `${WORKTRACKER_DATA_PATH:-./.docker-data/couchbase}` on the host.
+- Set `WORKTRACKER_DATA_PATH` before `docker compose up` if you want to move the database elsewhere.
+
+Development in VS Code without Docker:
+
+- Use the `WorkTracker: Debug Local` launch configuration.
+- VS Code builds the project, starts the app directly with `ASPNETCORE_ENVIRONMENT=Development`, and opens the local URL when the server is ready.
+- Local development data is stored in `App_Data/couchbase-dev` by default.
+
+Development in Docker:
+
+- The override file keeps a containerized `dotnet watch` flow for cases where you still want Docker.
+- The development container mounts the workspace into `/workspace` and stores its Couchbase Lite files under `./.docker-data/couchbase-dev` on the host.
Manual development start:
@@ -23,13 +33,8 @@ Manual shutdown:
- `docker compose down`
-MongoDB:
-
-- The compose stack includes a `mongo` service and a named volume `mongo_data` for persistence.
-- The app uses `MongoDb__ConnectionString: mongodb://mongo:27017` inside the compose network.
-
Notes:
-- The base compose file remains production-oriented; the override file is the development/debug layer that VS Code uses automatically.
+- The base compose file remains production-oriented; the override file is the optional containerized development layer.
- The first container build takes longer because the dev image installs the .NET debugger.
-- The Dockerfile pins the .NET 10 images to the `*-noble` tags because the generic `10.0` SDK tag does not provide a usable SDK in this environment.
+- The Dockerfile uses the .NET 9 `*-noble` images so local builds and container builds stay aligned with the SDK available in VS Code.
diff --git a/Services/Auth/MongoAuthUser.cs b/Services/Auth/AuthUser.cs
similarity index 51%
rename from Services/Auth/MongoAuthUser.cs
rename to Services/Auth/AuthUser.cs
index 8b4af75..15042ca 100644
--- a/Services/Auth/MongoAuthUser.cs
+++ b/Services/Auth/AuthUser.cs
@@ -1,20 +1,12 @@
-using MongoDB.Bson;
-using MongoDB.Bson.Serialization.Attributes;
-
namespace WorkTracker.Services.Auth;
-public sealed class MongoAuthUser
+public sealed class AuthUser
{
- [BsonId]
- [BsonRepresentation(BsonType.ObjectId)]
public string Id { get; init; } = string.Empty;
- [BsonElement("email")]
public string Email { get; init; } = string.Empty;
- [BsonElement("emailNormalized")]
public string EmailNormalized { get; init; } = string.Empty;
- [BsonElement("passwordHash")]
public string PasswordHash { get; init; } = string.Empty;
-}
+}
\ No newline at end of file
diff --git a/Services/Auth/CouchbaseLiteAuthService.cs b/Services/Auth/CouchbaseLiteAuthService.cs
new file mode 100644
index 0000000..0e19485
--- /dev/null
+++ b/Services/Auth/CouchbaseLiteAuthService.cs
@@ -0,0 +1,102 @@
+using Couchbase.Lite;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Options;
+using WorkTracker.Configuration;
+using WorkTracker.Services.Storage;
+
+namespace WorkTracker.Services.Auth;
+
+public sealed class CouchbaseLiteAuthService : IAuthService
+{
+ private readonly Collection users;
+ private readonly PasswordHasher passwordHasher = new();
+ private readonly IOptions options;
+ private readonly ILogger logger;
+
+ public CouchbaseLiteAuthService(
+ CouchbaseLiteDatabaseProvider databaseProvider,
+ IOptions options,
+ ILogger logger)
+ {
+ users = databaseProvider.Users;
+ this.options = options;
+ this.logger = logger;
+ }
+
+ public Task EnsureSeedUserAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var email = options.Value.Email.Trim();
+ var normalizedEmail = NormalizeEmail(email);
+
+ if (users.GetDocument(normalizedEmail) is not null)
+ {
+ return Task.CompletedTask;
+ }
+
+ var user = new AuthUser
+ {
+ Id = normalizedEmail,
+ Email = email,
+ EmailNormalized = normalizedEmail,
+ PasswordHash = string.Empty
+ };
+
+ var passwordHash = passwordHasher.HashPassword(user, options.Value.Password);
+ var userToCreate = new AuthUser
+ {
+ Id = normalizedEmail,
+ Email = email,
+ EmailNormalized = normalizedEmail,
+ PasswordHash = passwordHash
+ };
+
+ SaveUser(userToCreate);
+ logger.LogInformation("Seeded single user account {Email} in Couchbase Lite", email);
+ return Task.CompletedTask;
+ }
+
+ public Task ValidateCredentialsAsync(string email, string password, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var user = GetUser(NormalizeEmail(email));
+ if (user is null)
+ {
+ return Task.FromResult(null);
+ }
+
+ var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
+ return Task.FromResult(result == PasswordVerificationResult.Failed ? null : user);
+ }
+
+ private AuthUser? GetUser(string normalizedEmail)
+ {
+ var document = users.GetDocument(normalizedEmail);
+ if (document is null)
+ {
+ return null;
+ }
+
+ return new AuthUser
+ {
+ Id = document.Id,
+ Email = document.GetString("email") ?? string.Empty,
+ EmailNormalized = document.GetString("emailNormalized") ?? string.Empty,
+ PasswordHash = document.GetString("passwordHash") ?? string.Empty
+ };
+ }
+
+ private void SaveUser(AuthUser user)
+ {
+ var document = new MutableDocument(user.Id);
+ document.SetString("email", user.Email);
+ document.SetString("emailNormalized", user.EmailNormalized);
+ document.SetString("passwordHash", user.PasswordHash);
+
+ users.Save(document);
+ }
+
+ private static string NormalizeEmail(string email) => email.Trim().ToUpperInvariant();
+}
\ No newline at end of file
diff --git a/Services/Auth/IAuthService.cs b/Services/Auth/IAuthService.cs
new file mode 100644
index 0000000..bbb2ad1
--- /dev/null
+++ b/Services/Auth/IAuthService.cs
@@ -0,0 +1,8 @@
+namespace WorkTracker.Services.Auth;
+
+public interface IAuthService
+{
+ Task EnsureSeedUserAsync(CancellationToken cancellationToken);
+
+ Task ValidateCredentialsAsync(string email, string password, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/Services/Auth/IMongoAuthService.cs b/Services/Auth/IMongoAuthService.cs
deleted file mode 100644
index b60a406..0000000
--- a/Services/Auth/IMongoAuthService.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Threading;
-
-namespace WorkTracker.Services.Auth;
-
-public interface IMongoAuthService
-{
- Task EnsureSeedUserAsync(CancellationToken cancellationToken);
-
- Task ValidateCredentialsAsync(string email, string password, CancellationToken cancellationToken);
-}
diff --git a/Services/Auth/MongoAuthService.cs b/Services/Auth/MongoAuthService.cs
deleted file mode 100644
index e95dfca..0000000
--- a/Services/Auth/MongoAuthService.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using Microsoft.AspNetCore.Identity;
-using Microsoft.Extensions.Options;
-using MongoDB.Driver;
-using WorkTracker.Configuration;
-
-namespace WorkTracker.Services.Auth;
-
-public sealed class MongoAuthService : IMongoAuthService
-{
- private const string UsersCollectionName = "users";
-
- private readonly IMongoCollection users;
- private readonly PasswordHasher passwordHasher = new();
- private readonly IOptions options;
- private readonly ILogger logger;
-
- public MongoAuthService(
- IMongoDatabase database,
- IOptions options,
- ILogger logger)
- {
- users = database.GetCollection(UsersCollectionName);
- this.options = options;
- this.logger = logger;
- }
-
- public async Task EnsureSeedUserAsync(CancellationToken cancellationToken)
- {
- var email = options.Value.Email.Trim();
- var normalizedEmail = NormalizeEmail(email);
-
- var existingUser = await users
- .Find(x => x.EmailNormalized == normalizedEmail)
- .FirstOrDefaultAsync(cancellationToken);
-
- if (existingUser is not null)
- {
- return;
- }
-
- var user = new MongoAuthUser
- {
- Email = email,
- EmailNormalized = normalizedEmail,
- PasswordHash = string.Empty
- };
-
- var passwordHash = passwordHasher.HashPassword(user, options.Value.Password);
- var userToCreate = new MongoAuthUser
- {
- Email = email,
- EmailNormalized = normalizedEmail,
- PasswordHash = passwordHash
- };
-
- await users.InsertOneAsync(userToCreate, cancellationToken: cancellationToken);
- logger.LogInformation("Seeded single user account {Email} in MongoDB", email);
- }
-
- public async Task ValidateCredentialsAsync(string email, string password, CancellationToken cancellationToken)
- {
- var normalizedEmail = NormalizeEmail(email);
- var user = await users
- .Find(x => x.EmailNormalized == normalizedEmail)
- .FirstOrDefaultAsync(cancellationToken);
-
- if (user is null)
- {
- return null;
- }
-
- var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
- return result == PasswordVerificationResult.Failed ? null : user;
- }
-
- private static string NormalizeEmail(string email) => email.Trim().ToUpperInvariant();
-}
diff --git a/Services/Auth/SingleUserSeedService.cs b/Services/Auth/SingleUserSeedService.cs
index bcf8434..b696175 100644
--- a/Services/Auth/SingleUserSeedService.cs
+++ b/Services/Auth/SingleUserSeedService.cs
@@ -5,12 +5,12 @@ namespace WorkTracker.Services.Auth;
public sealed class SingleUserSeedService : IHostedService
{
- private readonly IMongoAuthService authService;
+ private readonly IAuthService authService;
private readonly IOptions options;
private readonly ILogger logger;
public SingleUserSeedService(
- IMongoAuthService authService,
+ IAuthService authService,
IOptions options,
ILogger logger)
{
@@ -32,7 +32,7 @@ public sealed class SingleUserSeedService : IHostedService
}
catch (Exception ex)
{
- logger.LogError(ex, "Unable to seed MongoDB single user account {Email}", options.Value.Email);
+ logger.LogError(ex, "Unable to seed Couchbase Lite single user account {Email}", options.Value.Email);
}
}
diff --git a/Services/Settings/CouchbaseLiteAppSettingsService.cs b/Services/Settings/CouchbaseLiteAppSettingsService.cs
new file mode 100644
index 0000000..66e8040
--- /dev/null
+++ b/Services/Settings/CouchbaseLiteAppSettingsService.cs
@@ -0,0 +1,96 @@
+using Couchbase.Lite;
+using WorkTracker.Domain;
+using WorkTracker.Services.Storage;
+
+namespace WorkTracker.Services.Settings;
+
+public sealed class CouchbaseLiteAppSettingsService : IAppSettingsService
+{
+ private const string DefaultSettingsId = "global";
+
+ private readonly Collection appSettingsCollection;
+
+ public CouchbaseLiteAppSettingsService(CouchbaseLiteDatabaseProvider databaseProvider)
+ {
+ appSettingsCollection = databaseProvider.AppSettings;
+ }
+
+ public Task GetAsync(CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var document = appSettingsCollection.GetDocument(DefaultSettingsId);
+ if (document is not null)
+ {
+ return Task.FromResult(Map(document));
+ }
+
+ var defaults = new AppSettingsDocument();
+ SaveDocument(defaults);
+ return Task.FromResult(defaults);
+ }
+
+ public async Task SaveAsync(AppSettingsDocument settings, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var existing = await GetAsync(cancellationToken);
+
+ settings.Id = DefaultSettingsId;
+ settings.CreatedAtUtc = existing.CreatedAtUtc;
+ settings.UpdatedAtUtc = DateTimeOffset.UtcNow;
+
+ SaveDocument(settings);
+ return settings;
+ }
+
+ private void SaveDocument(AppSettingsDocument settings)
+ {
+ var document = new MutableDocument(DefaultSettingsId);
+ document.SetDouble("standardWorkHoursPerDay", Decimal.ToDouble(settings.StandardWorkHoursPerDay));
+ document.SetDouble("lunchBreakHours", Decimal.ToDouble(settings.LunchBreakHours));
+ document.SetDouble("hourlyGrossRate", Decimal.ToDouble(settings.HourlyGrossRate));
+ document.SetDouble("profitabilityCoefficient", Decimal.ToDouble(settings.ProfitabilityCoefficient));
+ document.SetDouble("inpsRate", Decimal.ToDouble(settings.InpsRate));
+ document.SetDouble("substituteTaxRate", Decimal.ToDouble(settings.SubstituteTaxRate));
+ document.SetString("currency", settings.Currency);
+ document.SetString("locale", settings.Locale);
+ document.SetString("createdAtUtc", settings.CreatedAtUtc.ToString("O"));
+ document.SetString("updatedAtUtc", settings.UpdatedAtUtc.ToString("O"));
+
+ appSettingsCollection.Save(document);
+ }
+
+ private static AppSettingsDocument Map(Document document)
+ {
+ return new AppSettingsDocument
+ {
+ Id = document.Id,
+ StandardWorkHoursPerDay = ReadDecimal(document, "standardWorkHoursPerDay", 8m),
+ LunchBreakHours = ReadDecimal(document, "lunchBreakHours", 1m),
+ HourlyGrossRate = ReadDecimal(document, "hourlyGrossRate", 17.5m),
+ ProfitabilityCoefficient = ReadDecimal(document, "profitabilityCoefficient", 0.67m),
+ InpsRate = ReadDecimal(document, "inpsRate", 0.2607m),
+ SubstituteTaxRate = ReadDecimal(document, "substituteTaxRate", 0.15m),
+ Currency = document.GetString("currency") ?? "EUR",
+ Locale = document.GetString("locale") ?? "it-IT",
+ CreatedAtUtc = ReadDateTimeOffset(document, "createdAtUtc"),
+ UpdatedAtUtc = ReadDateTimeOffset(document, "updatedAtUtc")
+ };
+ }
+
+ private static decimal ReadDecimal(Document document, string key, decimal defaultValue)
+ {
+ return document.Contains(key)
+ ? Convert.ToDecimal(document.GetDouble(key))
+ : defaultValue;
+ }
+
+ private static DateTimeOffset ReadDateTimeOffset(Document document, string key)
+ {
+ var value = document.GetString(key);
+ return DateTimeOffset.TryParse(value, out var parsed)
+ ? parsed
+ : DateTimeOffset.UtcNow;
+ }
+}
\ No newline at end of file
diff --git a/Services/Settings/MongoAppSettingsService.cs b/Services/Settings/MongoAppSettingsService.cs
deleted file mode 100644
index dcaf6fa..0000000
--- a/Services/Settings/MongoAppSettingsService.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using MongoDB.Driver;
-using WorkTracker.Domain;
-
-namespace WorkTracker.Services.Settings;
-
-public sealed class MongoAppSettingsService : IAppSettingsService
-{
- private const string DefaultSettingsId = "global";
- private readonly IMongoCollection appSettingsCollection;
-
- public MongoAppSettingsService(IMongoDatabase database)
- {
- appSettingsCollection = database.GetCollection("app_settings");
- }
-
- public async Task GetAsync(CancellationToken cancellationToken = default)
- {
- var filter = Builders.Filter.Eq(x => x.Id, DefaultSettingsId);
- var settings = await appSettingsCollection.Find(filter).FirstOrDefaultAsync(cancellationToken);
-
- if (settings is not null)
- {
- return settings;
- }
-
- var defaults = new AppSettingsDocument();
- await appSettingsCollection.InsertOneAsync(defaults, cancellationToken: cancellationToken);
- return defaults;
- }
-
- public async Task SaveAsync(AppSettingsDocument settings, CancellationToken cancellationToken = default)
- {
- var existing = await GetAsync(cancellationToken);
-
- settings.Id = DefaultSettingsId;
- settings.CreatedAtUtc = existing.CreatedAtUtc;
- settings.UpdatedAtUtc = DateTimeOffset.UtcNow;
-
- var filter = Builders.Filter.Eq(x => x.Id, DefaultSettingsId);
- await appSettingsCollection.ReplaceOneAsync(filter, settings, new ReplaceOptions { IsUpsert = true }, cancellationToken);
-
- return settings;
- }
-}
diff --git a/Services/Storage/CouchbaseLiteDatabaseProvider.cs b/Services/Storage/CouchbaseLiteDatabaseProvider.cs
new file mode 100644
index 0000000..99d4e1e
--- /dev/null
+++ b/Services/Storage/CouchbaseLiteDatabaseProvider.cs
@@ -0,0 +1,52 @@
+using Couchbase.Lite;
+using Microsoft.Extensions.Options;
+using WorkTracker.Configuration;
+
+namespace WorkTracker.Services.Storage;
+
+public sealed class CouchbaseLiteDatabaseProvider : IDisposable
+{
+ private const string AppSettingsCollectionName = "app_settings";
+ private const string UsersCollectionName = "users";
+
+ private readonly Database database;
+
+ public CouchbaseLiteDatabaseProvider(IOptions options, IHostEnvironment environment)
+ {
+ var configuredOptions = options.Value;
+ var databaseDirectory = ResolveDirectory(configuredOptions.Directory, environment.ContentRootPath);
+ Directory.CreateDirectory(databaseDirectory);
+
+ database = new Database(
+ configuredOptions.DatabaseName,
+ new DatabaseConfiguration
+ {
+ Directory = databaseDirectory
+ });
+
+ AppSettings = database.GetCollection(AppSettingsCollectionName) ?? database.CreateCollection(AppSettingsCollectionName);
+ Users = database.GetCollection(UsersCollectionName) ?? database.CreateCollection(UsersCollectionName);
+ }
+
+ public Collection AppSettings { get; }
+
+ public Collection Users { get; }
+
+ public void Dispose()
+ {
+ database.Close();
+ database.Dispose();
+ }
+
+ private static string ResolveDirectory(string configuredDirectory, string contentRootPath)
+ {
+ if (string.IsNullOrWhiteSpace(configuredDirectory))
+ {
+ return Path.Combine(contentRootPath, "App_Data", "couchbase");
+ }
+
+ return Path.IsPathRooted(configuredDirectory)
+ ? configuredDirectory
+ : Path.GetFullPath(Path.Combine(contentRootPath, configuredDirectory));
+ }
+}
\ No newline at end of file
diff --git a/WorkTracker.csproj b/WorkTracker.csproj
index 2cd72f4..4f2e010 100644
--- a/WorkTracker.csproj
+++ b/WorkTracker.csproj
@@ -1,14 +1,14 @@
- net10.0
+ net9.0
enable
enable
aspnet-WorkTracker-28f934c3-03b2-413d-afbf-a5edbadc5530
-
+
diff --git a/appsettings.Development.json b/appsettings.Development.json
index 0c208ae..5a832b8 100644
--- a/appsettings.Development.json
+++ b/appsettings.Development.json
@@ -1,4 +1,8 @@
{
+ "CouchbaseLite": {
+ "Directory": "App_Data/couchbase-dev"
+ },
+ "UseHttpsRedirection": false,
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/appsettings.json b/appsettings.json
index 1923c69..5c197d0 100644
--- a/appsettings.json
+++ b/appsettings.json
@@ -1,7 +1,7 @@
{
- "MongoDb": {
- "ConnectionString": "mongodb://localhost:27017",
- "DatabaseName": "worktracker"
+ "CouchbaseLite": {
+ "DatabaseName": "worktracker",
+ "Directory": "App_Data/couchbase"
},
"SingleUser": {
"SeedOnStartup": true,
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
index 4b0b120..cec5fc3 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -9,15 +9,14 @@ services:
environment:
ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://+:8080
- MongoDb__ConnectionString: mongodb://mongo:27017
+ CouchbaseLite__Directory: /data/couchbase
UseHttpsRedirection: "false"
DOTNET_USE_POLLING_FILE_WATCHER: "1"
volumes:
- ./:/workspace:cached
+ - ./.docker-data/couchbase-dev:/data/couchbase
working_dir: /workspace
entrypoint: ["dotnet"]
command: ["watch", "run", "--no-launch-profile", "--project", "WorkTracker.csproj", "--urls", "http://+:8080"]
ports:
- "8002:8080"
- depends_on:
- - mongo
diff --git a/docker-compose.yml b/docker-compose.yml
index 2b5e861..715abe3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,23 +7,14 @@ services:
environment:
ASPNETCORE_ENVIRONMENT: Production
UseHttpsRedirection: "false"
- MongoDb__ConnectionString: mongodb://mongo:27017
+ CouchbaseLite__Directory: /data/couchbase
ports:
- "8002:8080"
- depends_on:
- - mongo
+ volumes:
+ - ${WORKTRACKER_DATA_PATH:-./.docker-data/couchbase}:/data/couchbase
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 5s
retries: 3
-
- mongo:
- image: mongo:7
- restart: unless-stopped
- volumes:
- - mongo_data:/data/db
-
-volumes:
- mongo_data:
diff --git a/plan.md b/plan.md
index 0c7dc56..db4962f 100644
--- a/plan.md
+++ b/plan.md
@@ -1,13 +1,13 @@
# WorkTracker Implementation Plan (AI-Ready)
## 1) Chosen stack
-- **Frontend + backend host**: ASP.NET Core + Blazor Web App (.NET 8)
-- **Database**: MongoDB
+- **Frontend + backend host**: ASP.NET Core + Blazor Web App (.NET 9)
+- **Database**: Couchbase Lite (local embedded database)
- **Auth approach (single user)**: ASP.NET Core Identity configured and enabled now; registration can be disabled later and one seeded account can be used.
Why this stack:
- Fits CRUD-heavy workflow with strong typing and server-side calculations.
-- MongoDB maps well to immutable daily snapshots (store coefficients used for each day).
+- Couchbase Lite keeps the app self-contained while still storing immutable daily snapshots locally.
- Blazor provides responsive desktop/mobile UI in one codebase.
---
@@ -31,7 +31,7 @@ Required outputs:
---
-## 3) Data model design (MongoDB)
+## 3) Data model design (Couchbase Lite)
### 3.1 `AppSettings` (configurable defaults)
- `id`
@@ -138,7 +138,7 @@ For Blazor Server/Web App, these can be implemented as internal services first a
---
## 8) Delivery roadmap
-1. Scaffold Blazor app + Identity + Mongo wiring
+1. Scaffold Blazor app + Identity + Couchbase Lite wiring
2. Implement Settings and defaults
3. Implement WorkDay model and calculation service
4. Build daily entry page