using Dapper; using Microsoft.Data.Sqlite; var builder = WebApplication.CreateBuilder(args); var dbPath = builder.Configuration["DbPath"] ?? Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "sessions.db"); builder.Services.AddSingleton>(() => new SqliteConnection($"Data Source={Path.GetFullPath(dbPath)}")); var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); // GET /api/cost-buckets?from=&to=&bucketMinutes= // Returns N buckets of aggregated cost app.MapGet("/api/cost-buckets", (Func factory, string from, string to, int bucketMinutes) => { using var db = factory(); var rows = db.Query(@" SELECT CAST((strftime('%s', timestamp) - strftime('%s', @from)) / (@bucketSec) AS INTEGER) AS bucket, SUM(CAST(json_extract(data, '$.message.usage.cost.total') AS REAL)) AS cost, COUNT(*) AS count, MIN(timestamp) AS bucketStart, MAX(timestamp) AS bucketEnd FROM entries WHERE json_extract(data, '$.message.usage.cost.total') IS NOT NULL AND timestamp >= @from AND timestamp < @to GROUP BY bucket ORDER BY bucket ASC", new { from, to, bucketSec = bucketMinutes * 60 }); return Results.Ok(rows); }); // GET /api/cost-entries?from=&to= // Returns all cost entries in a time range app.MapGet("/api/cost-entries", (Func factory, string from, string to) => { using var db = factory(); var rows = db.Query(@" SELECT id, session, type, timestamp, json_extract(data, '$.message.usage.cost.total') AS cost, json_extract(data, '$.message.usage.input') AS inputTokens, json_extract(data, '$.message.usage.output') AS outputTokens, json_extract(data, '$.message.model') AS model, data FROM entries WHERE json_extract(data, '$.message.usage.cost.total') IS NOT NULL AND timestamp >= @from AND timestamp < @to ORDER BY timestamp ASC", new { from, to }); return Results.Ok(rows); }); // GET /api/cost-summary?from=&to= app.MapGet("/api/cost-summary", (Func factory, string? from, string? to) => { using var db = factory(); var conditions = new List { "json_extract(data, '$.message.usage.cost.total') IS NOT NULL" }; if (from != null) conditions.Add("timestamp >= @from"); if (to != null) conditions.Add("timestamp < @to"); var where = string.Join(" AND ", conditions); var row = db.QuerySingle($@" SELECT COALESCE(SUM(CAST(json_extract(data, '$.message.usage.cost.total') AS REAL)), 0) AS totalCost, COUNT(*) AS totalEntries FROM entries WHERE {where}", new { from, to }); return Results.Ok(row); }); // POST /api/sync - run export.mjs to import new entries app.MapPost("/api/sync", async () => { var scriptPath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "export.mjs")); var psi = new System.Diagnostics.ProcessStartInfo("node", scriptPath) { RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = Path.GetDirectoryName(scriptPath)! }; var proc = System.Diagnostics.Process.Start(psi)!; var stdout = await proc.StandardOutput.ReadToEndAsync(); var stderr = await proc.StandardError.ReadToEndAsync(); await proc.WaitForExitAsync(); return Results.Ok(new { exitCode = proc.ExitCode, output = (stdout + stderr).Trim() }); }); app.Run();