diff --git a/internal/app/admin.go b/internal/app/admin.go new file mode 100644 index 0000000..7928068 --- /dev/null +++ b/internal/app/admin.go @@ -0,0 +1,268 @@ +package app + +import ( + "encoding/json" + "log/slog" + "net/http" + "strconv" + "strings" + + "git.warky.dev/wdevs/amcs/internal/store" + ext "git.warky.dev/wdevs/amcs/internal/types" + + "github.com/google/uuid" +) + +type adminHandlers struct { + db *store.DB + logger *slog.Logger +} + +func newAdminHandlers(db *store.DB, logger *slog.Logger) *adminHandlers { + return &adminHandlers{db: db, logger: logger} +} + +func (h *adminHandlers) register(mux *http.ServeMux, middleware func(http.Handler) http.Handler) { + handle := func(pattern string, fn http.HandlerFunc) { + mux.Handle(pattern, middleware(fn)) + } + + handle("GET /api/admin/projects", h.listProjects) + handle("POST /api/admin/projects", h.createProject) + handle("GET /api/admin/thoughts", h.listThoughts) + handle("GET /api/admin/thoughts/{id}", h.getThought) + handle("DELETE /api/admin/thoughts/{id}", h.deleteThought) + handle("POST /api/admin/thoughts/{id}/archive", h.archiveThought) + handle("GET /api/admin/skills", h.listSkills) + handle("DELETE /api/admin/skills/{id}", h.deleteSkill) + handle("GET /api/admin/guardrails", h.listGuardrails) + handle("DELETE /api/admin/guardrails/{id}", h.deleteGuardrail) + handle("GET /api/admin/files", h.listFiles) + handle("GET /api/admin/stats", h.stats) +} + +// --- Projects --- + +func (h *adminHandlers) listProjects(w http.ResponseWriter, r *http.Request) { + projects, err := h.db.ListProjects(r.Context()) + if err != nil { + h.internalError(w, "list projects", err) + return + } + writeJSON(w, projects) +} + +func (h *adminHandlers) createProject(w http.ResponseWriter, r *http.Request) { + var body struct { + Name string `json:"name"` + Description string `json:"description"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if strings.TrimSpace(body.Name) == "" { + http.Error(w, "name is required", http.StatusBadRequest) + return + } + project, err := h.db.CreateProject(r.Context(), body.Name, body.Description) + if err != nil { + h.internalError(w, "create project", err) + return + } + w.WriteHeader(http.StatusCreated) + writeJSON(w, project) +} + +// --- Thoughts --- + +func (h *adminHandlers) listThoughts(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + limit := 50 + if l := q.Get("limit"); l != "" { + if n, err := strconv.Atoi(l); err == nil && n > 0 { + limit = min(n, 200) + } + } + + query := strings.TrimSpace(q.Get("q")) + includeArchived := q.Get("include_archived") == "true" + + var projectID *uuid.UUID + if pid := q.Get("project_id"); pid != "" { + if id, err := uuid.Parse(pid); err == nil { + projectID = &id + } + } + + if query != "" { + results, err := h.db.SearchThoughtsText(r.Context(), query, limit, projectID, nil) + if err != nil { + h.internalError(w, "search thoughts", err) + return + } + writeJSON(w, results) + return + } + + thoughts, err := h.db.ListThoughts(r.Context(), ext.ListFilter{ + Limit: limit, + ProjectID: projectID, + IncludeArchived: includeArchived, + }) + if err != nil { + h.internalError(w, "list thoughts", err) + return + } + writeJSON(w, thoughts) +} + +func (h *adminHandlers) getThought(w http.ResponseWriter, r *http.Request) { + id, ok := parseUUID(w, r.PathValue("id")) + if !ok { + return + } + thought, err := h.db.GetThought(r.Context(), id) + if err != nil { + h.internalError(w, "get thought", err) + return + } + writeJSON(w, thought) +} + +func (h *adminHandlers) deleteThought(w http.ResponseWriter, r *http.Request) { + id, ok := parseUUID(w, r.PathValue("id")) + if !ok { + return + } + if err := h.db.DeleteThought(r.Context(), id); err != nil { + h.internalError(w, "delete thought", err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (h *adminHandlers) archiveThought(w http.ResponseWriter, r *http.Request) { + id, ok := parseUUID(w, r.PathValue("id")) + if !ok { + return + } + if err := h.db.ArchiveThought(r.Context(), id); err != nil { + h.internalError(w, "archive thought", err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// --- Skills --- + +func (h *adminHandlers) listSkills(w http.ResponseWriter, r *http.Request) { + tag := r.URL.Query().Get("tag") + skills, err := h.db.ListSkills(r.Context(), tag) + if err != nil { + h.internalError(w, "list skills", err) + return + } + writeJSON(w, skills) +} + +func (h *adminHandlers) deleteSkill(w http.ResponseWriter, r *http.Request) { + id, ok := parseUUID(w, r.PathValue("id")) + if !ok { + return + } + if err := h.db.RemoveSkill(r.Context(), id); err != nil { + h.internalError(w, "delete skill", err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// --- Guardrails --- + +func (h *adminHandlers) listGuardrails(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + guardrails, err := h.db.ListGuardrails(r.Context(), q.Get("tag"), q.Get("severity")) + if err != nil { + h.internalError(w, "list guardrails", err) + return + } + writeJSON(w, guardrails) +} + +func (h *adminHandlers) deleteGuardrail(w http.ResponseWriter, r *http.Request) { + id, ok := parseUUID(w, r.PathValue("id")) + if !ok { + return + } + if err := h.db.RemoveGuardrail(r.Context(), id); err != nil { + h.internalError(w, "delete guardrail", err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// --- Files --- + +func (h *adminHandlers) listFiles(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + limit := 100 + if l := q.Get("limit"); l != "" { + if n, err := strconv.Atoi(l); err == nil && n > 0 { + limit = min(n, 500) + } + } + + filter := ext.StoredFileFilter{Limit: limit} + if pid := q.Get("project_id"); pid != "" { + if id, err := uuid.Parse(pid); err == nil { + filter.ProjectID = &id + } + } + if tid := q.Get("thought_id"); tid != "" { + if id, err := uuid.Parse(tid); err == nil { + filter.ThoughtID = &id + } + } + filter.Kind = q.Get("kind") + + files, err := h.db.ListStoredFiles(r.Context(), filter) + if err != nil { + h.internalError(w, "list files", err) + return + } + writeJSON(w, files) +} + +// --- Stats --- + +func (h *adminHandlers) stats(w http.ResponseWriter, r *http.Request) { + stats, err := h.db.Stats(r.Context()) + if err != nil { + h.internalError(w, "stats", err) + return + } + writeJSON(w, stats) +} + +// --- Helpers --- + +func (h *adminHandlers) internalError(w http.ResponseWriter, op string, err error) { + h.logger.Error("admin handler error", slog.String("op", op), slog.String("error", err.Error())) + http.Error(w, "internal server error", http.StatusInternalServerError) +} + +func writeJSON(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(v) +} + +func parseUUID(w http.ResponseWriter, s string) (uuid.UUID, bool) { + id, err := uuid.Parse(s) + if err != nil { + http.Error(w, "invalid id", http.StatusBadRequest) + return uuid.UUID{}, false + } + return id, true +} + diff --git a/internal/app/app.go b/internal/app/app.go index 305727f..0b63611 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -92,12 +92,12 @@ func Run(ctx context.Context, configPath string) error { return err } } + tokenStore = auth.NewTokenStore(0) if len(cfg.Auth.OAuth.Clients) > 0 { oauthRegistry, err = auth.NewOAuthRegistry(cfg.Auth.OAuth.Clients) if err != nil { return err } - tokenStore = auth.NewTokenStore(0) } authCodes := auth.NewAuthCodeStore() dynClients := auth.NewDynamicClientStore() @@ -186,7 +186,7 @@ func Run(ctx context.Context, configPath string) error { func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *store.DB, embeddings *ai.EmbeddingRunner, metadata *ai.MetadataRunner, bgEmbeddings *ai.EmbeddingRunner, bgMetadata *ai.MetadataRunner, keyring *auth.Keyring, oauthRegistry *auth.OAuthRegistry, tokenStore *auth.TokenStore, authCodes *auth.AuthCodeStore, dynClients *auth.DynamicClientStore, activeProjects *session.ActiveProjects) (http.Handler, error) { mux := http.NewServeMux() accessTracker := auth.NewAccessTracker() - oauthEnabled := oauthRegistry != nil && tokenStore != nil + oauthEnabled := oauthRegistry != nil authMiddleware := auth.Middleware(cfg.Auth, keyring, oauthRegistry, tokenStore, accessTracker, logger) filesTool := tools.NewFilesTool(db, activeProjects) enrichmentRetryer := tools.NewEnrichmentRetryer(context.Background(), db, bgMetadata, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger) @@ -227,16 +227,13 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st mux.Handle(cfg.MCP.SSEPath, authMiddleware(mcpHandlers.SSE)) logger.Info("SSE transport enabled", slog.String("sse_path", cfg.MCP.SSEPath)) } + newAdminHandlers(db, logger).register(mux, authMiddleware) mux.Handle("/files", authMiddleware(fileHandler(filesTool))) mux.Handle("/files/{id}", authMiddleware(fileHandler(filesTool))) - if oauthEnabled { - mux.HandleFunc("/.well-known/oauth-authorization-server", oauthMetadataHandler()) - mux.HandleFunc("/oauth-authorization-server", oauthMetadataHandler()) - mux.HandleFunc("/oauth/register", oauthRegisterHandler(dynClients, logger)) - mux.HandleFunc("/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger)) - mux.HandleFunc("/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger)) - mux.HandleFunc("/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger)) - } + mux.HandleFunc("/.well-known/oauth-authorization-server", oauthMetadataHandler()) + mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger)) + mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger)) + mux.HandleFunc("/api/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger)) mux.HandleFunc("/favicon.ico", serveFavicon) mux.HandleFunc("/images/project.jpg", serveHomeImage) mux.HandleFunc("/images/icon.png", serveIcon) diff --git a/internal/app/oauth.go b/internal/app/oauth.go index 6a8731b..9dbe787 100644 --- a/internal/app/oauth.go +++ b/internal/app/oauth.go @@ -67,9 +67,9 @@ func oauthMetadataHandler() http.HandlerFunc { base := serverBaseURL(r) meta := oauthServerMetadata{ Issuer: base, - AuthorizationEndpoint: base + "/authorize", - TokenEndpoint: base + "/oauth/token", - RegistrationEndpoint: base + "/oauth/register", + AuthorizationEndpoint: base + "/api/oauth/authorize", + TokenEndpoint: base + "/api/oauth/token", + RegistrationEndpoint: base + "/api/oauth/register", ScopesSupported: []string{"mcp"}, ResponseTypesSupported: []string{"code"}, GrantTypesSupported: []string{"authorization_code", "client_credentials"}, @@ -244,6 +244,10 @@ func oauthTokenHandler(oauthRegistry *auth.OAuthRegistry, tokenStore *auth.Token switch r.FormValue("grant_type") { case "client_credentials": + if oauthRegistry == nil { + writeTokenError(w, "unsupported_grant_type", http.StatusBadRequest) + return + } handleClientCredentials(w, r, oauthRegistry, tokenStore, log) case "authorization_code": handleAuthorizationCode(w, r, authCodes, tokenStore, log) @@ -334,7 +338,7 @@ button{padding:.5rem 1.2rem;margin-right:.5rem;cursor:pointer;font-size:1rem}

Authorize Access

%s is requesting access to this AMCS server.

-
+ diff --git a/internal/app/status.go b/internal/app/status.go index 63a590a..2021bb3 100644 --- a/internal/app/status.go +++ b/internal/app/status.go @@ -90,8 +90,6 @@ func homeHandler(_ buildinfo.Info, _ *auth.AccessTracker, _ bool) http.HandlerFu if serveUIAsset(w, r, requestPath) { return } - http.NotFound(w, r) - return } serveUIIndex(w, r) diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 520588e..e113ad2 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -1,67 +1,18 @@ + +
+
+ + AMCS Control Interface +
+

+ {#if isOAuthCallback} + Completing login + {:else} + Login + {/if} +

+

+ Origin-style operator access for the AMCS admin interface. ResolveSpec OAuth is the front door now, + not the old login shortcut. +

+ +
+
+

Primary module

+

Projects

+

Projects are the first real admin screen in this rollout.

+
+
+

OAuth path

+

ResolveSpec

+

Client registration, authorize, callback, token exchange.

+
+
+
diff --git a/ui/src/components/auth/LoginPage.svelte b/ui/src/components/auth/LoginPage.svelte new file mode 100644 index 0000000..b9a6f47 --- /dev/null +++ b/ui/src/components/auth/LoginPage.svelte @@ -0,0 +1,34 @@ + + +
+
+ + +
+
diff --git a/ui/src/components/auth/LoginPanel.svelte b/ui/src/components/auth/LoginPanel.svelte new file mode 100644 index 0000000..8fc8aaf --- /dev/null +++ b/ui/src/components/auth/LoginPanel.svelte @@ -0,0 +1,72 @@ + + +
+ {#if isOAuthCallback} +

Authorizing operator session

+

+ Finishing the ResolveSpec handshake and exchanging the returned code for an AMCS token. +

+ +
+ {#if callbackBusy} + Working the callback doohickey… + {:else if authError} + Callback failed. Fix the route or try the login run again. + {:else} + Callback processed. + {/if} +
+ {:else} +

Operator login

+

Authenticate through AMCS ResolveSpec OAuth endpoints.

+ +
+ + +
+

Routes in play

+
    +
  • • discovery: /.well-known/oauth-authorization-server
  • +
  • • registration: /api/oauth/register
  • +
  • • authorize: {oauthAuthorizeURL}
  • +
  • • callback: /oauth/callback
  • +
  • • token: /api/oauth/token
  • +
+
+ + {#if authError} +

{authError}

+ {/if} + {#if authMessage} +

{authMessage}

+ {/if} +
+ {/if} +
diff --git a/ui/src/components/dashboard/AccessTable.svelte b/ui/src/components/dashboard/AccessTable.svelte new file mode 100644 index 0000000..bd52a28 --- /dev/null +++ b/ui/src/components/dashboard/AccessTable.svelte @@ -0,0 +1,39 @@ + + +
+

Recent access

+
+
+ + + + + + + + + + + + {#each entries as entry} + + + + + + + + {/each} + +
PrincipalLast accessedLast pathAgentRequests
{entry.key_id}{formatDate(entry.last_accessed_at)}{entry.last_path}{entry.user_agent ?? '—'}{entry.request_count}
+
+
+
diff --git a/ui/src/components/dashboard/DashboardPage.svelte b/ui/src/components/dashboard/DashboardPage.svelte new file mode 100644 index 0000000..68eb39a --- /dev/null +++ b/ui/src/components/dashboard/DashboardPage.svelte @@ -0,0 +1,49 @@ + + +
+
+
+

System overview

+

Current AMCS status behind the admin shell.

+
+ +
+ + {#if loading} +
+ Loading status… +
+ {:else if error} +
+

Couldn't load the status snapshot.

+

{error}

+
+ {:else if data} + + {/if} +
+ +{#if data && data.entries.length > 0} + +{/if} diff --git a/ui/src/components/dashboard/StatusCards.svelte b/ui/src/components/dashboard/StatusCards.svelte new file mode 100644 index 0000000..f8ad1a9 --- /dev/null +++ b/ui/src/components/dashboard/StatusCards.svelte @@ -0,0 +1,20 @@ + + +
+
+

Connected users

+

{data.connected_count}

+
+
+

Known principals

+

{data.total_known}

+
+
+

Version

+

{data.version}

+
+
diff --git a/ui/src/components/files/FilesPage.svelte b/ui/src/components/files/FilesPage.svelte new file mode 100644 index 0000000..a66e492 --- /dev/null +++ b/ui/src/components/files/FilesPage.svelte @@ -0,0 +1,92 @@ + + +
+
+
+

Files

+

{files.length} file{files.length !== 1 ? 's' : ''}

+
+ +
+ + {#if error} +
{error}
+ {/if} + + {#if loading} +
Loading…
+ {:else if files.length === 0} +
No files stored.
+ {:else} +
+ + + + + + + + + + + + + {#each files as f} + + + + + + + + + {/each} + +
NameTypeKindSizeUploadedDownload
{f.name}{f.media_type} + {f.kind || '—'} + {formatBytes(f.size_bytes)}{formatDate(f.created_at)} + +
+
+ {/if} +
diff --git a/ui/src/components/guardrails/GuardrailsPage.svelte b/ui/src/components/guardrails/GuardrailsPage.svelte new file mode 100644 index 0000000..cd28f28 --- /dev/null +++ b/ui/src/components/guardrails/GuardrailsPage.svelte @@ -0,0 +1,103 @@ + + +
+
+
+

Guardrails

+

{guardrails.length} guardrail{guardrails.length !== 1 ? 's' : ''}

+
+ +
+ + {#if error} +
{error}
+ {/if} + + {#if loading} +
Loading…
+ {:else if guardrails.length === 0} +
No guardrails registered.
+ {:else} +
+ {#each guardrails as g} +
+
+
+
+

{g.name}

+ + {g.severity} + +
+ {#if g.description} +

{g.description}

+ {/if} + {#if g.tags?.length} +
+ {#each g.tags as tag} + {tag} + {/each} +
+ {/if} +
+ +
+
+ View content +
{g.content}
+
+
+ {/each} +
+ {/if} +
diff --git a/ui/src/components/projects/ProjectsPage.svelte b/ui/src/components/projects/ProjectsPage.svelte new file mode 100644 index 0000000..cbf37c5 --- /dev/null +++ b/ui/src/components/projects/ProjectsPage.svelte @@ -0,0 +1,137 @@ + + +
+
+
+

Projects

+

{projects.length} project{projects.length !== 1 ? 's' : ''}

+
+
+ + +
+
+ + {#if showCreate} +
+

Create project

+
+ + + {#if createError}

{createError}

{/if} +
+ + +
+
+
+ {/if} + + {#if loading} +
+ Loading… +
+ {:else if error} +
{error}
+ {:else if projects.length === 0} +
+ No projects yet. +
+ {:else} +
+ + + + + + + + + + + + {#each projects as p} + + + + + + + + {/each} + +
NameDescriptionThoughtsLast activeCreated
{p.name}{p.description || '—'}{p.thought_count}{formatDate(p.last_active_at)}{formatDate(p.created_at)}
+
+ {/if} +
diff --git a/ui/src/components/shell/AdminShell.svelte b/ui/src/components/shell/AdminShell.svelte new file mode 100644 index 0000000..ce8dbac --- /dev/null +++ b/ui/src/components/shell/AdminShell.svelte @@ -0,0 +1,48 @@ + + +
+ + +
+ {#if currentPage === 'dashboard'} + + {:else if currentPage === 'projects'} + + {:else if currentPage === 'thoughts'} + + {:else if currentPage === 'skills'} + + {:else if currentPage === 'guardrails'} + + {:else if currentPage === 'files'} + + {/if} +
+
diff --git a/ui/src/components/shell/AppSidebar.svelte b/ui/src/components/shell/AppSidebar.svelte new file mode 100644 index 0000000..730d23f --- /dev/null +++ b/ui/src/components/shell/AppSidebar.svelte @@ -0,0 +1,49 @@ + + + diff --git a/ui/src/components/skills/SkillsPage.svelte b/ui/src/components/skills/SkillsPage.svelte new file mode 100644 index 0000000..e797c8d --- /dev/null +++ b/ui/src/components/skills/SkillsPage.svelte @@ -0,0 +1,91 @@ + + +
+
+
+

Skills

+

{skills.length} skill{skills.length !== 1 ? 's' : ''}

+
+ +
+ + {#if error} +
{error}
+ {/if} + + {#if loading} +
Loading…
+ {:else if skills.length === 0} +
No skills registered.
+ {:else} +
+ {#each skills as skill} +
+
+
+

{skill.name}

+ {#if skill.description} +

{skill.description}

+ {/if} + {#if skill.tags?.length} +
+ {#each skill.tags as tag} + {tag} + {/each} +
+ {/if} +
+ +
+
+ View content +
{skill.content}
+
+
+ {/each} +
+ {/if} +
diff --git a/ui/src/components/thoughts/ThoughtsPage.svelte b/ui/src/components/thoughts/ThoughtsPage.svelte new file mode 100644 index 0000000..ea1895a --- /dev/null +++ b/ui/src/components/thoughts/ThoughtsPage.svelte @@ -0,0 +1,165 @@ + + +
+
+
+

Thoughts

+

{rows.length} result{rows.length !== 1 ? 's' : ''}

+
+
+ + +
+
+ + + + {#if actionError} +

{actionError}

+ {/if} + + {#if loading} +
Loading…
+ {:else if error} +
{error}
+ {:else if rows.length === 0} +
No thoughts found.
+ {:else} +
+ + + + + + + + + + + + {#each rows as row} + + + + + + + + {/each} + +
ContentTypeStatusCreatedActions
+

{content(row)}

+ {#if row.metadata.topics?.length} +

{row.metadata.topics.slice(0,3).join(', ')}

+ {/if} +
+ + {row.metadata.type || '—'} + + + {isArchived(row) ? 'archived' : (row.metadata.metadata_status || 'active')} + {formatDate(row.created_at)} +
+ {#if !isArchived(row)} + + {/if} + +
+
+
+ {/if} +
diff --git a/ui/src/shellState.ts b/ui/src/shellState.ts index 3a0bbcc..e05a101 100644 --- a/ui/src/shellState.ts +++ b/ui/src/shellState.ts @@ -1,4 +1,4 @@ -import { GlobalStateStore } from '@warkypublic/svelix'; +import { GlobalStateStore, isLoggedInStore } from '@warkypublic/svelix'; const normalizeApiURL = (url: string): string => url.replace(/\/+$/, ''); @@ -21,7 +21,7 @@ const resolveApiURL = (envURL?: string): string => { return ''; }; -export { GlobalStateStore }; +export { GlobalStateStore, isLoggedInStore }; export type OAuthClientRegistration = { client_id: string; @@ -174,8 +174,8 @@ async function sha256(input: string): Promise { } export async function fetchOAuthMetadata(): Promise { - const apiURL = ensureApiURL(); - const response = await fetch(`${apiURL}/.well-known/oauth-authorization-server`); + const base = getPublicBaseURL(); + const response = await fetch(`${base}/.well-known/oauth-authorization-server`); if (!response.ok) { throw new Error(`Failed to load OAuth metadata (${response.status})`); } @@ -185,10 +185,6 @@ export async function fetchOAuthMetadata(): Promise { export async function ensureOAuthClientRegistration(metadata: OAuthServerMetadata): Promise { const redirectURI = getOAuthRedirectURI(); - const existing = readOAuthClient(); - if (existing?.client_id && existing.redirect_uris?.includes(redirectURI)) { - return existing; - } const response = await fetch(metadata.registration_endpoint, { method: 'POST', @@ -214,6 +210,7 @@ export async function ensureOAuthClientRegistration(metadata: OAuthServerMetadat } export async function buildOAuthAuthorizationURL(): Promise { + removeStorage(OAUTH_CLIENT_KEY); const metadata = await fetchOAuthMetadata(); const client = await ensureOAuthClientRegistration(metadata); const codeVerifier = createRandomString(96); diff --git a/ui/src/types.ts b/ui/src/types.ts new file mode 100644 index 0000000..9348682 --- /dev/null +++ b/ui/src/types.ts @@ -0,0 +1,111 @@ +export type AccessEntry = { + key_id: string; + last_accessed_at: string; + last_path: string; + user_agent: string; + request_count: number; +}; + +export type StatusResponse = { + title: string; + description: string; + version: string; + build_date: string; + commit: string; + connected_count: number; + total_known: number; + connected_window: string; + oauth_enabled: boolean; + entries: AccessEntry[]; +}; + +export type NavItem = { + id: string; + label: string; + description: string; + disabled?: boolean; +}; + +export type ShellPage = 'dashboard' | 'projects' | 'thoughts' | 'skills' | 'guardrails' | 'files'; + +export type Project = { + id: string; + name: string; + description: string; + created_at: string; + last_active_at: string; +}; + +export type ProjectSummary = Project & { + thought_count: number; +}; + +export type ThoughtMetadata = { + people: string[]; + action_items: string[]; + dates_mentioned: string[]; + topics: string[]; + type: string; + source: string; + metadata_status: string; + metadata_error?: string; +}; + +export type Thought = { + id: string; + content: string; + metadata: ThoughtMetadata; + project_id?: string; + archived_at?: string; + created_at: string; + updated_at: string; +}; + +export type SearchResult = { + id: string; + content: string; + metadata: ThoughtMetadata; + similarity: number; + created_at: string; +}; + +export type AgentSkill = { + id: string; + name: string; + description: string; + content: string; + tags: string[]; + created_at: string; + updated_at: string; +}; + +export type AgentGuardrail = { + id: string; + name: string; + description: string; + content: string; + severity: 'low' | 'medium' | 'high' | 'critical'; + tags: string[]; + created_at: string; + updated_at: string; +}; + +export type StoredFile = { + id: string; + thought_id?: string; + project_id?: string; + name: string; + media_type: string; + kind: string; + size_bytes: number; + sha256: string; + created_at: string; + updated_at: string; +}; + +export type ThoughtStats = { + total_count: number; + type_counts: Record; + top_topics: { key: string; count: number }[]; + top_people: { key: string; count: number }[]; +}; diff --git a/ui/vite.config.ts b/ui/vite.config.ts index ac21268..783578e 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -18,9 +18,6 @@ export default defineConfig({ '/favicon.ico': backendTarget, '/mcp': backendTarget, '/files': backendTarget, - '/oauth-authorization-server': backendTarget, - '/authorize': backendTarget, - '/oauth': backendTarget, '/.well-known': backendTarget } },