Files
Wiki/src/Wiki.Host/Program.cs
T
claude-botandClaude Fable 5.1 fe6a284c6c
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
gitea: one repository adapter serving both the note-store and the fact-sink port
Two classes over the same backend showed as two adapters on the status
page. One GiteaRepository, one HttpClient, one health entry; the host
lists an adapter once even when it serves several ports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 13:30:17 +02:00

111 lines
4.5 KiB
C#

using Microsoft.AspNetCore.Authentication;
using MudBlazor.Services;
using Wiki.Adapters.FileIndex;
using Wiki.Adapters.Gitea;
using Wiki.Adapters.LlamaCpp;
using Wiki.Application.Retrieval;
using Wiki.Application.Status;
using Wiki.Application.Sync;
using Wiki.Core.Ports;
using Wiki.Host.Api;
using Wiki.Host.Auth;
using Wiki.Host.Components;
using Wiki.Host.Composition;
var builder = WebApplication.CreateBuilder(args);
var wiki = builder.Configuration.GetSection("Wiki").Get<WikiOptions>() ?? new WikiOptions();
builder.Services.AddSingleton(wiki);
builder.Services.AddSingleton(TimeProvider.System);
// ---- adapters, selected by configuration ------------------------------------------------
builder.Services.AddGiteaAdapters(o =>
{
o.BaseUrl = wiki.NoteStore.Gitea.BaseUrl;
o.Repository = wiki.NoteStore.Gitea.Repository;
o.Branch = wiki.NoteStore.Gitea.Branch;
o.Token = builder.Configuration["Wiki:NoteStore:Gitea:Token"] ?? string.Empty;
});
builder.Services.AddFileIndexAdapters(o => o.Directory = wiki.Index.File.Directory);
foreach (var (name, model) in wiki.Embeddings.Models)
{
switch (model.Adapter)
{
case "LlamaCpp":
builder.Services.AddLlamaCppEmbeddings(name, o =>
{
o.Url = model.Url;
o.Model = model.Model;
o.ModelId = name;
o.Dimension = model.Dimension;
o.BasicAuth = builder.Configuration[$"Wiki:Embeddings:Models:{name}:BasicAuth"];
});
break;
default:
throw new InvalidOperationException($"unknown embeddings adapter '{model.Adapter}' for model '{name}'");
}
}
builder.Services.AddSingleton<IReadOnlyList<IEmbeddingModel>>(sp =>
wiki.Embeddings.Models.Keys.Select(sp.GetRequiredKeyedService<IEmbeddingModel>).ToList());
builder.Services.AddSingleton<IEmbeddingModel>(sp => sp.GetRequiredKeyedService<IEmbeddingModel>(wiki.Embeddings.Active));
// One adapter may serve several ports (Gitea is note store and fact sink); list it once.
builder.Services.AddSingleton<IReadOnlyList<IAdapterHealth>>(sp =>
{
List<IAdapterHealth> all =
[
sp.GetRequiredService<INoteStore>(),
sp.GetRequiredService<IFactSink>(),
sp.GetRequiredService<IVectorIndex>(),
sp.GetRequiredService<IDocumentCatalog>(),
.. sp.GetRequiredService<IReadOnlyList<IEmbeddingModel>>(),
];
return all.Distinct().ToList();
});
// ---- use cases ---------------------------------------------------------------------------
builder.Services.AddSingleton(new SyncOptions());
builder.Services.AddSingleton<SyncUseCase>();
builder.Services.AddSingleton<SyncCoordinator>();
builder.Services.AddSingleton<RankUseCase>();
builder.Services.AddSingleton<StatusQuery>();
builder.Services.AddSingleton<SyncBackgroundService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<SyncBackgroundService>());
// ---- auth: humans arrive through the Authentik outpost, machines with an API key ---------
builder.Services.AddAuthentication(ProxyHeaderAuthenticationHandler.SchemeName)
.AddScheme<AuthenticationSchemeOptions, ProxyHeaderAuthenticationHandler>(ProxyHeaderAuthenticationHandler.SchemeName, null)
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationHandler.SchemeName, null);
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(Policies.Write, p => p.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName).RequireAuthenticatedUser());
});
// ---- UI ----------------------------------------------------------------------------------
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddMudServices();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
// The not-found page is for the UI only: an API 401/404 must stay a JSON status, not be
// re-executed as a POST against a Blazor page (which fails antiforgery with an empty 400).
app.UseWhen(
ctx => HttpMethods.IsGet(ctx.Request.Method) && !ctx.Request.Path.StartsWithSegments("/api"),
branch => branch.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true));
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapHealthEndpoints();
app.MapIndexEndpoints();
app.MapApiV1();
app.MapStaticAssets();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.Run();
public partial class Program;