Files
novelly/src/Novelly.Api/Program.cs
T
James Wampler 27c287b9c8 Add genre feature; extract ConfirmModal for reusable confirm dialogs
Genres seed on boot, are editable per project, and gate agent config
errors more gracefully. ConfirmModal replaces ad-hoc confirm prompts
across chapters, tags, and characters pages.
2026-08-15 11:25:13 -07:00

98 lines
2.8 KiB
C#

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Novelly.Api.Agent;
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Data;
using Novelly.Api.Genres;
using Novelly.Api.Imports;
using Novelly.Api.Projects;
using Novelly.Api.Questions;
using Novelly.Api.Tags;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSerilog((services, config) => config
.ReadFrom.Configuration(builder.Configuration)
.ReadFrom.Services(services)
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"));
builder.AddServiceDefaults();
builder.Services.AddNovelly(builder.Configuration);
builder.Services.AddOpenApi();
builder.Services.AddProblemDetails();
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
var corsOrigins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>()
?? ["http://localhost:5173"];
builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy
.WithOrigins(corsOrigins)
.AllowAnyHeader()
.AllowAnyMethod()));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<NovelDbContext>().Database.MigrateAsync();
}
app.UseSerilogRequestLogging();
app.UseExceptionHandler(handler => handler.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
var (status, title) = exception switch
{
AgentNotConfiguredException => (StatusCodes.Status503ServiceUnavailable, "Agent unavailable"),
ArgumentException or InvalidOperationException => (StatusCodes.Status400BadRequest, "Invalid request"),
_ => (StatusCodes.Status500InternalServerError, "Unexpected error")
};
if (status == StatusCodes.Status500InternalServerError)
{
app.Logger.LogError(exception, "Unhandled exception on {Path}", context.Request.Path);
}
else
{
app.Logger.LogWarning(exception, "Handled {StatusCode} on {Path}: {Title}", status, context.Request.Path, title);
}
await Results
.Problem(title: title, detail: exception?.Message, statusCode: status)
.ExecuteAsync(context);
}));
app.UseCors();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapDefaultEndpoints();
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" })).WithTags("Health");
app.MapProjectEndpoints()
.MapCharacterEndpoints()
.MapChapterEndpoints()
.MapBeatEndpoints()
.MapTagEndpoints()
.MapGenreEndpoints()
.MapOpenQuestionEndpoints()
.MapAgentEndpoints()
.MapImportEndpoints();
app.Run();
public partial class Program;