From a4193b295aa95838005ea158c28e177495f26626 Mon Sep 17 00:00:00 2001 From: Hein Date: Mon, 27 Apr 2026 00:04:08 +0200 Subject: [PATCH] fix(ui): update AMCS references and add status handling * Corrected "Advanced Module Control System" to "Avalon Memory Control Service" in documentation and UI components. * Added status handling to the LoginInfoPanel and LoginPage components. * Implemented new endpoints for robots.txt and llms.txt. --- README.md | 4 +- internal/app/app.go | 4 + internal/app/llm.go | 73 ++++++ internal/app/llm_test.go | 68 ++++++ internal/app/status.go | 2 +- internal/app/status_test.go | 20 ++ llm/memory.md | 2 +- ui/src/App.svelte | 7 +- ui/src/components/auth/LoginInfoPanel.svelte | 220 +++++++++++++++++-- ui/src/components/auth/LoginPage.svelte | 57 ++++- ui/src/components/auth/LoginPanel.svelte | 4 +- 11 files changed, 427 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index ff97098..5d396b7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # AMCS Directory -This is the AMCS (Advanced Module Control System) directory. +This is the AMCS (Avalon Memory Control Service) directory. ## Purpose -The AMCS directory is used to store configuration and code for the Advanced Module Control System, which handles... +The AMCS directory is used to store configuration and code for the Avalon Memory Control Service, which handles... ## Structure diff --git a/internal/app/app.go b/internal/app/app.go index f5b82ca..da0932b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -243,7 +243,11 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st mux.HandleFunc("/images/project.jpg", serveHomeImage) mux.HandleFunc("/images/icon.png", serveIcon) mux.HandleFunc("/llm", serveLLMInstructions) + mux.HandleFunc("/llms.txt", serveLLMSTXT) + mux.HandleFunc("/.well-known/llms.txt", serveLLMSTXT) + mux.HandleFunc("/robots.txt", serveRobotsTXT) mux.HandleFunc("/api/status", statusAPIHandler(info, accessTracker, oauthEnabled)) + mux.HandleFunc("/status", statusAPIHandler(info, accessTracker, oauthEnabled)) mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) diff --git a/internal/app/llm.go b/internal/app/llm.go index 79492e0..ddccc35 100644 --- a/internal/app/llm.go +++ b/internal/app/llm.go @@ -1,7 +1,9 @@ package app import ( + "fmt" "net/http" + "strings" amcsllm "git.warky.dev/wdevs/amcs/llm" ) @@ -20,3 +22,74 @@ func serveLLMInstructions(w http.ResponseWriter, r *http.Request) { } _, _ = w.Write(amcsllm.MemoryInstructions) } + +func serveRobotsTXT(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/robots.txt" { + http.NotFound(w, r) + return + } + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.Header().Set("Allow", "GET, HEAD") + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=300") + w.WriteHeader(http.StatusOK) + if r.Method == http.MethodHead { + return + } + + body := fmt.Sprintf("User-agent: *\nAllow: /\n\n# LLM-friendly docs\nLLM: %s/llm\nLLMS: %s/llms.txt\n", requestBaseURL(r), requestBaseURL(r)) + _, _ = w.Write([]byte(body)) +} + +func serveLLMSTXT(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/llms.txt" && r.URL.Path != "/.well-known/llms.txt" { + http.NotFound(w, r) + return + } + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.Header().Set("Allow", "GET, HEAD") + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=300") + w.WriteHeader(http.StatusOK) + if r.Method == http.MethodHead { + return + } + + base := requestBaseURL(r) + body := fmt.Sprintf( + "# AMCS\n\n> A memory server for AI assistants (MCP tools, semantic retrieval, and structured project memory).\n\n## Endpoints\n- %s/llm\n- %s/status\n- %s/mcp\n- %s/.well-known/oauth-authorization-server\n", + base, + base, + base, + base, + ) + _, _ = w.Write([]byte(body)) +} + +func requestBaseURL(r *http.Request) string { + scheme := "http" + if r != nil && r.TLS != nil { + scheme = "https" + } + if r != nil { + if proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); proto != "" { + scheme = proto + } + } + + host := "localhost" + if r != nil { + if v := strings.TrimSpace(r.Host); v != "" { + host = v + } + } + return scheme + "://" + host +} diff --git a/internal/app/llm_test.go b/internal/app/llm_test.go index 26891a5..44483c4 100644 --- a/internal/app/llm_test.go +++ b/internal/app/llm_test.go @@ -3,6 +3,7 @@ package app import ( "net/http" "net/http/httptest" + "strings" "testing" amcsllm "git.warky.dev/wdevs/amcs/llm" @@ -29,3 +30,70 @@ func TestServeLLMInstructions(t *testing.T) { t.Fatalf("body = %q, want embedded instructions", body) } } + +func TestServeRobotsTXT(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/robots.txt", nil) + req.Host = "amcs.example.com" + req.Header.Set("X-Forwarded-Proto", "https") + rec := httptest.NewRecorder() + + serveRobotsTXT(rec, req) + + res := rec.Result() + defer func() { + _ = res.Body.Close() + }() + + if res.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want %d", res.StatusCode, http.StatusOK) + } + if got := res.Header.Get("Content-Type"); got != "text/plain; charset=utf-8" { + t.Fatalf("content-type = %q, want %q", got, "text/plain; charset=utf-8") + } + body := rec.Body.String() + if !strings.Contains(body, "LLM: https://amcs.example.com/llm") { + t.Fatalf("body = %q, want LLM link", body) + } + if !strings.Contains(body, "LLMS: https://amcs.example.com/llms.txt") { + t.Fatalf("body = %q, want LLMS link", body) + } +} + +func TestServeLLMSTXT(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/llms.txt", nil) + req.Host = "amcs.example.com" + req.Header.Set("X-Forwarded-Proto", "https") + rec := httptest.NewRecorder() + + serveLLMSTXT(rec, req) + + res := rec.Result() + defer func() { + _ = res.Body.Close() + }() + + if res.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want %d", res.StatusCode, http.StatusOK) + } + if got := res.Header.Get("Content-Type"); got != "text/plain; charset=utf-8" { + t.Fatalf("content-type = %q, want %q", got, "text/plain; charset=utf-8") + } + body := rec.Body.String() + if !strings.Contains(body, "https://amcs.example.com/llm") { + t.Fatalf("body = %q, want /llm link", body) + } + if !strings.Contains(body, "https://amcs.example.com/.well-known/oauth-authorization-server") { + t.Fatalf("body = %q, want oauth discovery link", body) + } +} + +func TestServeLLMSTXTWellKnownPath(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/.well-known/llms.txt", nil) + rec := httptest.NewRecorder() + + serveLLMSTXT(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } +} diff --git a/internal/app/status.go b/internal/app/status.go index 2f8c063..12a0d43 100644 --- a/internal/app/status.go +++ b/internal/app/status.go @@ -55,7 +55,7 @@ func fallback(value, defaultValue string) string { func statusAPIHandler(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/status" { + if r.URL.Path != "/api/status" && r.URL.Path != "/status" { http.NotFound(w, r) return } diff --git a/internal/app/status_test.go b/internal/app/status_test.go index 0736663..151f12c 100644 --- a/internal/app/status_test.go +++ b/internal/app/status_test.go @@ -86,6 +86,26 @@ func TestStatusAPIHandlerReturnsJSON(t *testing.T) { } } +func TestStatusAPIHandlerSupportsStatusPath(t *testing.T) { + handler := statusAPIHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), true) + req := httptest.NewRequest(http.MethodGet, "/status", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var payload statusAPIResponse + if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + t.Fatalf("json.Unmarshal() error = %v", err) + } + if payload.Version != "v1" { + t.Fatalf("version = %q, want %q", payload.Version, "v1") + } +} + func TestHomeHandlerAllowsHead(t *testing.T) { handler := homeHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), false) req := httptest.NewRequest(http.MethodHead, "/", nil) diff --git a/llm/memory.md b/llm/memory.md index 8775b9c..b72c3bc 100644 --- a/llm/memory.md +++ b/llm/memory.md @@ -1,6 +1,6 @@ # AMCS Memory Instructions -AMCS (Avalon Memory Crystal Server) is an MCP server for capturing and retrieving thoughts, memory, and project context. It is backed by Postgres with pgvector for semantic search. +AMCS (Avalon Memory Control Service) is an MCP server for capturing and retrieving thoughts, memory, and project context. It is backed by Postgres with pgvector for semantic search. `amcs-cli` is a pre-built CLI that connects to the AMCS MCP server so agents do not need to implement their own HTTP MCP client. Download it from https://git.warky.dev/wdevs/amcs/releases diff --git a/ui/src/App.svelte b/ui/src/App.svelte index d9d91d4..9913d59 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -153,9 +153,7 @@ await GlobalStateStore.getState().fetchData(); - if (GlobalStateStore.getState().isLoggedIn()) { - await loadStatus(); - } + await loadStatus(); }); @@ -171,6 +169,9 @@ {authBusy} {authError} {authMessage} + statusData={data} + statusLoading={loading} + statusError={error} onlogin={handleCredentialLogin} /> {:else} diff --git a/ui/src/components/auth/LoginInfoPanel.svelte b/ui/src/components/auth/LoginInfoPanel.svelte index befa29c..a179278 100644 --- a/ui/src/components/auth/LoginInfoPanel.svelte +++ b/ui/src/components/auth/LoginInfoPanel.svelte @@ -1,34 +1,226 @@ -
-
+
+
- AMCS Control Interface + Avalon Memory Control Service

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

- Origin-style operator access for the AMCS admin interface. ResolveSpec OAuth is the front door now, - not the old login shortcut. + AMCS is a Go MCP server for capturing project thoughts, semantic retrieval, + summaries, and linked memory workflows with Postgres + pgvector. +

+

+ It stores durable memory for assistants, supports project scoping, and + exposes tools over MCP for capture, search, context recall, and structured + operations.

-
+
-

Primary module

-

Projects

-

Projects are the first real admin screen in this rollout.

+

+ Server status +

+ {#if loading} +

Loading…

+ {:else if error} +

Unavailable

+

{error}

+ {:else if data} +

{data.version}

+

+ {data.connected_count} connected in {data.connected_window} +

+ + Status Endpoint + + {:else} +

No status snapshot yet.

+ {/if}
-

OAuth path

-

ResolveSpec

-

Client registration, authorize, callback, token exchange.

+

+ Memory stack +

+

Postgres + pgvector

+

+ Semantic search with full-text fallback when vectors are missing. +

+
+
+

+ Operator docs +

+ + Open LLM Instructions + +

+ Tool behavior, workflows, and MCP guidance for assistants. +

+
+
+ +
+

Project intelligence model

+

+ AMCS separates reusable behavior, safety constraints, and curated + knowledge so assistants can be guided consistently across sessions. +

+ +
+ {#each intelligenceCards as card} +
setActiveCard(card.id)} + onkeydown={(event) => handleCardKeydown(event, card.id)} + > +

+ {card.title} +

+

{card.summary}

+ {#if activeCard === card.id} +

{card.detail}

+ {/if} +

+ Tools: + {#each card.tools as tool, idx} + {tool}{idx < + card.tools.length - 1 + ? ", " + : ""} + {/each} +

+
+ {/each}
diff --git a/ui/src/components/auth/LoginPage.svelte b/ui/src/components/auth/LoginPage.svelte index 70e5e2b..e80a8b4 100644 --- a/ui/src/components/auth/LoginPage.svelte +++ b/ui/src/components/auth/LoginPage.svelte @@ -1,6 +1,7 @@ -
-
- - +
+
+ +
+ +
+
diff --git a/ui/src/components/auth/LoginPanel.svelte b/ui/src/components/auth/LoginPanel.svelte index 7718d12..7a28f1c 100644 --- a/ui/src/components/auth/LoginPanel.svelte +++ b/ui/src/components/auth/LoginPanel.svelte @@ -28,7 +28,7 @@ {#if isOAuthCallback}

Authorizing operator session

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

@@ -42,7 +42,7 @@
{:else}

Operator login

-

Authenticate with your ResolveSpec credentials.

+

Authenticate to access the AMCS admin interface.