Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
/d:sonar.javascript.lcov.reportPaths="coverage/web-plan.lcov,coverage/staging-pinger.lcov,coverage/web-routing.lcov" \
/d:sonar.exclusions="tools/**,**/Migrations/**,**/bin/**,**/obj/**,**/OAuth/OAuthLoginPage.cs,**/.claude/**,**/Email/Templates/**,**/load-tests/**,**/bench/**" \
/d:sonar.cpd.exclusions="**/Chat/Tools/Implementations/CreateHabitTool.cs,**/Chat/Tools/Implementations/UpdateHabitTool.cs,**/Services/AgentCatalogService.UserDataCatalog.cs,**/Services/SlipAlertSchedulerService.cs,**/Services/ProactiveCheckinSchedulerService.cs,**/Social/Commands/AcceptFriendRequestCommand.cs" \
/d:sonar.coverage.exclusions="**/Program.cs,**/Extensions/ServiceCollectionExtensions*.cs,**/Extensions/WebApplicationExtensions.cs,**/Middleware/**,**/OpenApi/**,**/OAuth/**,**/Persistence/OrbitDbContext.cs,**/Persistence/OrbitDbContextFactory.cs,**/Persistence/GenericRepository.cs,**/Persistence/UnitOfWork.cs,**/Persistence/AccountResetRepository.cs,**/Configuration/**,**/Services/ReminderSchedulerService.cs,**/Services/SlipAlertSchedulerService.cs,**/Services/GoalDeadlineNotificationService.cs,**/Services/HabitDueDateAdvancementService.cs,**/Services/DataEncryptionMigrationService.cs,**/Services/PushNotificationService.cs,**/Services/AccountDeletionService.cs,**/Services/GoogleTokenService.cs,**/Services/BackgroundServiceHealthCheck.cs,**/Services/StripeCouponRewardService.cs,**/Services/StripeBillingService.cs,**/Services/GooglePlayBillingService.cs,**/Services/Calendar/**,**/AI/AiCompletionClient.cs,**/AI/AiBatchClient.cs,**/AI/AudioTranscriptionService.cs,**/Services/GeoLocationService.cs,**/BackgroundJobs/HangfireRecurringJobRegistrar.cs,**/Orbit.Analyzers/**,**/Orbit.Analyzers.CodeFixes/**,**/.github/**,**/bench/**"
/d:sonar.coverage.exclusions="**/Program.cs,**/Extensions/ServiceCollectionExtensions*.cs,**/Extensions/WebApplicationExtensions.cs,**/Middleware/**,**/OpenApi/**,**/OAuth/**,**/Persistence/OrbitDbContext.cs,**/Persistence/OrbitDbContextFactory.cs,**/Persistence/GenericRepository.cs,**/Persistence/UnitOfWork.cs,**/Persistence/AccountResetRepository.cs,**/Configuration/**,**/Services/ReminderSchedulerService.cs,**/Services/SlipAlertSchedulerService.cs,**/Services/GoalDeadlineNotificationService.cs,**/Services/HabitDueDateAdvancementService.cs,**/Services/DataEncryptionMigrationService.cs,**/Services/PushNotificationService.cs,**/Services/AccountDeletionService.cs,**/Services/GoogleTokenService.cs,**/Services/BackgroundServiceHealthCheck.cs,**/Services/StripeCouponRewardService.cs,**/Services/StripeBillingService.cs,**/Services/GooglePlayBillingService.cs,**/Services/Calendar/**,**/AI/AiCompletionClient.cs,**/AI/AudioTranscriptionService.cs,**/Services/GeoLocationService.cs,**/BackgroundJobs/HangfireRecurringJobRegistrar.cs,**/Orbit.Analyzers/**,**/Orbit.Analyzers.CodeFixes/**,**/.github/**,**/bench/**"

- name: Build
run: dotnet build --no-restore
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ The backend for **Orbit** -- an AI-powered habit tracker. Provides a REST API fo
- **Social** -- Friends, public profiles, and referrals
- **Checklist Templates** -- Reusable habit/checklist templates
- **Tags** -- Colored tags for habit organization
- **User Facts** -- Personal context facts that enhance AI responses (soft-deleted)
- **Push Notifications** -- Dual delivery: FCM for native Android, VAPID Web Push for browsers, plus background schedulers for reminders, goal deadlines, slip alerts, and check-ins
- **Email Verification** -- Passwordless code-based login via Amazon SES
- **Subscription Billing** -- Stripe (web) and Google Play Billing (native) with webhook / RTDN processing; backend is the source of truth for entitlements
Expand Down Expand Up @@ -110,7 +109,7 @@ src/
Goals/ Gamification/ # Goals, XP/streaks/achievements
Calendar/ # Google Calendar sync
Social/ Referrals/ # Social graph
ChecklistTemplates/ Tags/ UserFacts/ Uploads/ # Content
ChecklistTemplates/ Tags/ Uploads/ # Content
Auth/ Profile/ ApiKeys/ Subscriptions/ Support/ Notifications/ Waitlist/
Behaviors/ # MediatR pipeline (ValidationBehavior, ...)

Expand All @@ -119,7 +118,7 @@ src/

Orbit.Infrastructure/ # Infrastructure layer
Persistence/ # OrbitDbContext, GenericRepository<T>, UnitOfWork
Services/ # AI (AiIntentService, AiSummaryService, OpenAiBatchPollerService, ...),
Services/ # AI (AiIntentService, AiSummaryService, ...),
# Agent/MCP (AgentCatalogService, AgentOperationExecutor, AgentPolicyEvaluator, ...),
# billing (StripeBillingService, GooglePlayBillingService),
# push + schedulers (PushNotificationService, ReminderSchedulerService, ...),
Expand All @@ -134,7 +133,7 @@ tests/

### Controllers

`Achievements`, `Ai`, `ApiKeys`, `Auth`, `Calendar`, `Chat`, `ChecklistTemplates`, `Config`, `Friends`, `Gamification`, `Goals`, `Habits`, `Notification`, `OAuth`, `Profile`, `PublicProfile`, `Referral`, `Subscription`, `Support`, `Sync`, `Tags`, `Uploads`, `UserFacts`, `Waitlist`.
`Achievements`, `Ai`, `ApiKeys`, `Auth`, `Calendar`, `Chat`, `ChecklistTemplates`, `Config`, `Friends`, `Gamification`, `Goals`, `Habits`, `Notification`, `OAuth`, `Profile`, `PublicProfile`, `Referral`, `Subscription`, `Support`, `Sync`, `Tags`, `Uploads`, `Waitlist`.

### Key Patterns

Expand Down
4 changes: 3 additions & 1 deletion src/Orbit.Api/Controllers/ConfigController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public partial class ConfigController(
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public async Task<IActionResult> GetConfig(CancellationToken cancellationToken)
{
var limits = await configService.GetAllAsync(cancellationToken);
var limits = (await configService.GetAllAsync(cancellationToken))
.Where(config => config.Key != "MaxUserFacts")
.ToDictionary(config => config.Key, config => config.Value);

var featureFlags = await featureFlagService.ResolveForUserAsync(HttpContext.GetUserId(), cancellationToken);

Expand Down
22 changes: 0 additions & 22 deletions src/Orbit.Api/Controllers/ProfileController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public partial class ProfileController(
{
public record SetTimezoneRequest(string TimeZone);
public record SetNameRequest(string Name);
public record SetAiMemoryRequest([property: JsonRequired] bool Enabled);
public record SetAiSummaryRequest([property: JsonRequired] bool Enabled);
public record SetProactiveAstraEnabledRequest([property: JsonRequired] bool Enabled);
public record SetLanguageRequest(string Language);
Expand Down Expand Up @@ -104,24 +103,6 @@ public async Task<IActionResult> SetName(
return result.ToPayGateAwareResult(() => NoContent());
}

[HttpPut("ai-memory")]
[Obsolete("AI memory is deprecated and will be removed.")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetAiMemory(
[FromBody] SetAiMemoryRequest request,
CancellationToken cancellationToken)
{
var command = new SetAiMemoryCommand(HttpContext.GetUserId(), request.Enabled);
var result = await mediator.Send(command, cancellationToken);

if (result.IsSuccess)
LogAiMemoryToggled(logger, ToggleLabel(request.Enabled), HttpContext.GetUserId());

return result.ToPayGateAwareResult(() => NoContent());
}

[HttpPut("ai-summary")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
Expand Down Expand Up @@ -420,9 +401,6 @@ public async Task<IActionResult> ExportUserData(CancellationToken cancellationTo
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Timezone changed to {Timezone} for user {UserId}")]
private static partial void LogTimezoneChanged(ILogger logger, string timezone, Guid userId);

[LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "AI memory {State} for user {UserId}")]
private static partial void LogAiMemoryToggled(ILogger logger, string state, Guid userId);

[LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "AI summary {State} for user {UserId}")]
private static partial void LogAiSummaryToggled(ILogger logger, string state, Guid userId);

Expand Down
68 changes: 0 additions & 68 deletions src/Orbit.Api/Controllers/UserFactsController.cs

This file was deleted.

2 changes: 0 additions & 2 deletions src/Orbit.Api/Extensions/ResultActionResultExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public static class ResultActionResultExtensions
[ErrorCodes.ParentHabitNotFound] = StatusCodes.Status404NotFound,
[ErrorCodes.TargetParentNotFound] = StatusCodes.Status404NotFound,
[ErrorCodes.TagNotFound] = StatusCodes.Status404NotFound,
[ErrorCodes.FactNotFound] = StatusCodes.Status404NotFound,
[ErrorCodes.GoalNotFound] = StatusCodes.Status404NotFound,
[ErrorCodes.ApiKeyNotFound] = StatusCodes.Status404NotFound,
[ErrorCodes.NotificationNotFound] = StatusCodes.Status404NotFound,
Expand All @@ -40,7 +39,6 @@ public static class ResultActionResultExtensions
[ErrorCodes.StepUpChallengeNotFound] = StatusCodes.Status404NotFound,

[ErrorCodes.DuplicateTagName] = StatusCodes.Status409Conflict,
[ErrorCodes.DuplicateFact] = StatusCodes.Status409Conflict,
[ErrorCodes.AlreadyReferred] = StatusCodes.Status409Conflict,
[ErrorCodes.SkipUndoConflict] = StatusCodes.Status409Conflict,
[ErrorCodes.ConcurrentUpdateConflict] = StatusCodes.Status409Conflict,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ private static void AddAiPlatformServices(WebApplicationBuilder builder)
builder.Configuration.GetSection(AgentPlatformSettings.SectionName));
builder.Services.AddSingleton<AiCompletionClient>();
builder.Services.AddSingleton<IAiUsageRecorder, AiUsageRecorder>();
builder.Services.AddSingleton<IAiBatchClient, AiBatchClient>();
builder.Services.AddScoped<IAudioTranscriptionService, AudioTranscriptionService>();
builder.Services.AddScoped<IAiIntentService, AiIntentService>();
builder.Services.AddScoped<IFactExtractionService, AiFactExtractionService>();
builder.Services.AddScoped<IHabitEmojiInferenceService, AiHabitEmojiInferenceService>();
builder.Services.AddScoped<ISummaryService, AiSummaryService>();
builder.Services.AddScoped<IRescheduleSuggestionService, AiRescheduleSuggestionService>();
Expand Down Expand Up @@ -82,7 +80,6 @@ private static void AddAiChatTools(WebApplicationBuilder builder)
builder.Services.AddScoped<IAiTool, GoalReviewTool>();
builder.Services.AddScoped<IAiTool, GetProfileTool>();
builder.Services.AddScoped<IAiTool, UpdateProfilePreferencesTool>();
builder.Services.AddScoped<IAiTool, SetAiMemoryTool>();
builder.Services.AddScoped<IAiTool, SetAiSummaryTool>();
builder.Services.AddScoped<IAiTool, SetProactiveAstraTool>();
builder.Services.AddScoped<IAiTool, GetNotificationsTool>();
Expand All @@ -95,8 +92,6 @@ private static void AddAiChatTools(WebApplicationBuilder builder)
builder.Services.AddScoped<IAiTool, GetChecklistTemplatesTool>();
builder.Services.AddScoped<IAiTool, CreateChecklistTemplateTool>();
builder.Services.AddScoped<IAiTool, DeleteChecklistTemplateTool>();
builder.Services.AddScoped<IAiTool, GetUserFactsTool>();
builder.Services.AddScoped<IAiTool, DeleteUserFactsTool>();
builder.Services.AddScoped<IAiTool, GetGamificationOverviewTool>();
builder.Services.AddScoped<IAiTool, GetReferralOverviewTool>();
builder.Services.AddScoped<IAiTool, GetSubscriptionOverviewTool>();
Expand Down Expand Up @@ -183,7 +178,6 @@ private static void AddChatCommandDependencies(WebApplicationBuilder builder)
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.Habit>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.Goal>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.User>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.UserFact>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.Tag>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.ChecklistTemplate>>(),
sp.GetRequiredService<IFeatureFlagService>()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ private static void AddBackgroundServices(WebApplicationBuilder builder)
builder.Services.AddHostedService<XpAwardLogBackfillHostedService>();

AddDurableJobQueue(builder);
builder.Services.AddHostedService<HangfireRecurringJobRegistrar>();

if (useDurableQueue)
AddDurableRecurringJobs(builder);
Expand Down Expand Up @@ -58,7 +59,6 @@ internal static void AddInProcessSchedulers(WebApplicationBuilder builder)
builder.Services.AddHostedService<PlayNotificationCleanupService>();
builder.Services.AddHostedService<CalendarAutoSyncService>();
builder.Services.AddHostedService<NotionAutoSyncService>();
builder.Services.AddHostedService<OpenAiBatchPollerService>();
builder.Services.AddHostedService<AiUsageSummaryService>();
builder.Services.AddHostedService<FoundingAchievementReconciliationService>();
}
Expand Down Expand Up @@ -109,11 +109,9 @@ internal static void AddDurableRecurringJobs(WebApplicationBuilder builder)
AddScheduledJob<PlayNotificationCleanupService>(builder);
AddScheduledJob<CalendarAutoSyncService>(builder);
AddScheduledJob<NotionAutoSyncService>(builder);
AddScheduledJob<OpenAiBatchPollerService>(builder);
AddScheduledJob<AiUsageSummaryService>(builder);
AddScheduledJob<FoundingAchievementReconciliationService>(builder);

builder.Services.AddHostedService<HangfireRecurringJobRegistrar>();
}

private static void AddScheduledJob<TJob>(WebApplicationBuilder builder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@
.WithTools<NotificationTools>()
.WithTools<RecordPageTools>()
.WithTools<SubscriptionTools>()
.WithTools<UserFactTools>()
.WithTools<CalendarTools>()
.WithTools<NotionTools>()
.WithTools<AccountTools>()
Expand Down Expand Up @@ -316,7 +315,7 @@
builder.Services.AddApiVersioning(options =>
{
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
options.AssumeDefaultVersionWhenUnspecified = true;

Check warning on line 318 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Build

AssumeDefaultVersionWhenUnspecified = true is only necessary for existing APIs that do not have an explicit API version (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0016.html)

Check warning on line 318 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

AssumeDefaultVersionWhenUnspecified = true is only necessary for existing APIs that do not have an explicit API version (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0016.html)

Check warning on line 318 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Unit Tests

AssumeDefaultVersionWhenUnspecified = true is only necessary for existing APIs that do not have an explicit API version (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0016.html)
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
options.UnsupportedApiVersionStatusCode = StatusCodes.Status400BadRequest;
Expand All @@ -324,8 +323,8 @@
.AddMvc()
.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV");

builder.Services.AddOpenApi("v1", options => ConfigureOrbitOpenApi(options, "1.0"));

Check warning on line 326 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Build

AddApiVersioning().AddOpenApi() registers the OpenAPI services that describe API versions (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0029.html)

Check warning on line 326 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

AddApiVersioning().AddOpenApi() registers the OpenAPI services that describe API versions (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0029.html)

Check warning on line 326 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Unit Tests

AddApiVersioning().AddOpenApi() registers the OpenAPI services that describe API versions (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0029.html)
builder.Services.AddOpenApi("v2", options => ConfigureOrbitOpenApi(options, "2.0"));

Check warning on line 327 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Build

AddApiVersioning().AddOpenApi() registers the OpenAPI services that describe API versions (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0029.html)

Check warning on line 327 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

AddApiVersioning().AddOpenApi() registers the OpenAPI services that describe API versions (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0029.html)

Check warning on line 327 in src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs

View workflow job for this annotation

GitHub Actions / Unit Tests

AddApiVersioning().AddOpenApi() registers the OpenAPI services that describe API versions (https://dotnet.github.io/aspnet-api-versioning/diagnostic/av0029.html)
}

private static void ConfigureOrbitOpenApi(Microsoft.AspNetCore.OpenApi.OpenApiOptions options, string version)
Expand Down
1 change: 0 additions & 1 deletion src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ public static WebApplicationBuilder AddOrbitDatabase(this WebApplicationBuilder
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.Goal>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.GoalProgressLog>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.Tag>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.UserFact>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.Notification>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.ChecklistTemplate>>(),
sp.GetRequiredService<IGenericRepository<Orbit.Domain.Entities.UserAchievement>>(),
Expand Down
4 changes: 2 additions & 2 deletions src/Orbit.Api/Mcp/Tools/FeatureTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ namespace Orbit.Api.Mcp.Tools;
[McpServerToolType]
public class FeatureTools(IFeatureExplanationService features)
{
[McpServerTool(Name = "describe_feature"), Description("Return the authoritative explanation of how an Orbit feature works (streaks, freezes, frequencies, gamification/XP/levels, notifications, free-vs-pro paygate, schedule/overdue math, AI memory). Call before explaining any of these mechanics.")]
[McpServerTool(Name = "describe_feature"), Description("Return the authoritative explanation of how an Orbit feature works (streaks, freezes, frequencies, gamification/XP/levels, notifications, free-vs-pro paygate, schedule/overdue math). Call before explaining any of these mechanics.")]
public string DescribeFeature(
ClaimsPrincipal user,
[Description("Which Orbit feature to explain: ai-memory, freezes, frequencies, gamification, notifications, paygate, schedule-math, or streaks.")] string featureKey)
[Description("Which Orbit feature to explain: freezes, frequencies, gamification, notifications, paygate, schedule-math, or streaks.")] string featureKey)
{
var explanation = features.Get(featureKey);
if (explanation is null)
Expand Down
17 changes: 0 additions & 17 deletions src/Orbit.Api/Mcp/Tools/ProfileTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,6 @@ public async Task<string> SetLanguage(
return result.Succeeded ? $"Language set to {language}" : result.Message;
}

[McpServerTool(Name = "set_ai_memory"), Description("Enable or disable AI memory (remembering user facts from conversations).")]
public async Task<string> SetAiMemory(
ClaimsPrincipal user,
[Description("True to enable, false to disable")] bool enabled,
CancellationToken cancellationToken = default)
{
var result = await executorBridge.ExecuteAsync(user, "set_ai_memory", new
{
enabled
}, confirmationToken: null, cancellationToken);

if (!result.Succeeded)
return result.Message;

return enabled ? "AI memory enabled" : "AI memory disabled";
}

[McpServerTool(Name = "set_ai_summary"), Description("Enable or disable AI daily summary on the Today page.")]
public async Task<string> SetAiSummary(
ClaimsPrincipal user,
Expand Down
Loading
Loading