Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -406,11 +406,12 @@ components:
StatusResponse:
type: object
additionalProperties: false
required: [status, reason, timezone, auth_enabled, capabilities]
required: [status, reason, timezone, version, auth_enabled, capabilities]
properties:
status: { type: string, enum: [ok, degraded, failed] }
reason: { type: string }
timezone: { type: string }
version: { type: string }
auth_enabled: { type: boolean }
capabilities: { $ref: "#/components/schemas/Capabilities" }
Capabilities:
Expand Down
5 changes: 4 additions & 1 deletion internal/httpapi/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

"github.com/Willxup/flowlens/internal/buildinfo"
"github.com/Willxup/flowlens/internal/clashapi"
"github.com/Willxup/flowlens/internal/collector"
flowstatus "github.com/Willxup/flowlens/internal/status"
Expand Down Expand Up @@ -307,6 +308,7 @@ func (h *handler) statusResponse(writer http.ResponseWriter, request *http.Reque
Status flowstatus.Level `json:"status"`
Reason string `json:"reason"`
Timezone string `json:"timezone"`
Version string `json:"version"`
AuthEnabled bool `json:"auth_enabled"`
Capabilities struct {
ConnectionID bool `json:"connection_id"`
Expand All @@ -317,7 +319,8 @@ func (h *handler) statusResponse(writer http.ResponseWriter, request *http.Reque
Domain bool `json:"domain"`
} `json:"capabilities"`
}{
Status: snapshot.Level, Reason: snapshot.Reason, Timezone: h.timezone, AuthEnabled: !h.authDisabled,
Status: snapshot.Level, Reason: snapshot.Reason, Timezone: h.timezone,
Version: buildinfo.Version, AuthEnabled: !h.authDisabled,
Capabilities: struct {
ConnectionID bool `json:"connection_id"`
Source bool `json:"source"`
Expand Down
5 changes: 4 additions & 1 deletion internal/httpapi/test/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"
"time"

"github.com/Willxup/flowlens/internal/buildinfo"
"github.com/Willxup/flowlens/internal/clashapi"
"github.com/Willxup/flowlens/internal/httpapi"
flowstatus "github.com/Willxup/flowlens/internal/status"
Expand Down Expand Up @@ -58,13 +59,15 @@ func TestLoginStatusAndLogoutFlow(t *testing.T) {
Status string `json:"status"`
Reason string `json:"reason"`
Timezone string `json:"timezone"`
Version string `json:"version"`
AuthEnabled bool `json:"auth_enabled"`
Capabilities map[string]bool `json:"capabilities"`
}
if err := json.Unmarshal(statusResponse.Body.Bytes(), &body); err != nil {
t.Fatalf("status JSON: %v", err)
}
if body.Status != "ok" || body.Reason != "ready" || body.Timezone != "Asia/Shanghai" || !body.AuthEnabled ||
if body.Status != "ok" || body.Reason != "ready" || body.Timezone != "Asia/Shanghai" ||
body.Version != buildinfo.Version || !body.AuthEnabled ||
len(body.Capabilities) != 6 || !body.Capabilities["connection_id"] {
t.Errorf("status body = %#v", body)
}
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion internal/webassets/dist/assets/index-CKrnZdaT.css

This file was deleted.

1 change: 1 addition & 0 deletions internal/webassets/dist/assets/index-NUtcblDI.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions internal/webassets/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<script src="/theme-init.js"></script>
<title>FlowLens</title>
<script type="module" crossorigin src="/assets/index-TEbLqC9v.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CKrnZdaT.css">
<script type="module" crossorigin src="/assets/index-B9YDBDoG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-NUtcblDI.css">
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/stage4-demo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ test("offline Demo exposes one rich responsive statistics dashboard", async ({
"data-source-mode",
"demo",
);
await expect(page.getByText("FlowLens")).toBeVisible();
await expect(
page.getByRole("link", { name: "FlowLens GitHub 仓库" }),
).toBeVisible();
await expect(page.getByText("采集正常")).toBeVisible();
await expect(page.locator(".live-status.ok i")).toHaveCSS(
"animation-name",
Expand Down
16 changes: 16 additions & 0 deletions test/e2e/stage4-production-csp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ test("production bundle works with the shipped CSP and named SSE events", async
"app",
);
await expect(page.getByRole("heading", { name: "实时吞吐" })).toBeVisible();
const footer = page.locator("footer.app-footer");
await expect(footer.getByText("© 2026")).toBeVisible();
await expect(footer.getByRole("link", { name: "FlowLens" })).toHaveAttribute(
"href",
"https://github.com/Willxup/flowlens",
);
await expect(footer.getByRole("link", { name: "License" })).toHaveAttribute(
"href",
"https://github.com/Willxup/flowlens/blob/main/LICENSE",
);
await expect(
footer.getByRole("link", { name: "Willxup GitHub 主页" }),
).toHaveAttribute("href", "https://github.com/Willxup");
await expect(footer.getByText("Version: v0.2.5")).toBeVisible();
await expect(footer.locator(".app-footer-version-separator")).toBeHidden();
await expect.poll(() => apiRequests).toContain("/api/v1/connections/live");
await expect(
page.getByText("Fixture · 198.51.100.20:443").first(),
Expand Down Expand Up @@ -127,6 +142,7 @@ function fixtureResponse(path: string): unknown {
status: "ok",
reason: "ready",
timezone: "UTC",
version: "v0.2.5",
auth_enabled: true,
capabilities: {
connection_id: true,
Expand Down
1 change: 1 addition & 0 deletions web/src/api/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface StatusResponse {
status: ServiceLevel;
reason: string;
timezone: string;
version: string;
auth_enabled: boolean;
capabilities: Capabilities;
}
Expand Down
1 change: 1 addition & 0 deletions web/src/api/production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ function validResponse(value: unknown, shape: string | undefined): boolean {
typeof value.status === "string" &&
typeof value.reason === "string" &&
typeof value.timezone === "string" &&
typeof value.version === "string" &&
typeof value.auth_enabled === "boolean" &&
isRecord(value.capabilities)
);
Expand Down
19 changes: 19 additions & 0 deletions web/src/api/test/production.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ describe("ProductionDataSource", () => {
status: "ok",
reason: "ready",
timezone: "Asia/Shanghai",
version: "v0.2.5",
capabilities: {},
}),
) as typeof fetch,
);

await expect(source.status()).rejects.toThrow(
"FlowLens response is invalid",
);
});

it("requires the injected build version in status responses", async () => {
const source = new ProductionDataSource(
vi.fn(async () =>
Response.json({
status: "ok",
reason: "ready",
timezone: "Asia/Shanghai",
auth_enabled: true,
capabilities: {},
}),
) as typeof fetch,
Expand Down
4 changes: 4 additions & 0 deletions web/src/app/Shell.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { ReactNode } from "react";
import type { ServiceLevel } from "../api/contracts";
import { AppFooter } from "../components/AppFooter";
import { Tooltip } from "../components/Tooltip";
import { ThemeSelect } from "../features/theme/ThemeSelect";

export function Shell({
status,
version,
sourceMode,
authEnabled,
onLogout,
logoutFailed,
children,
}: {
status: ServiceLevel;
version: string;
sourceMode: "app" | "demo";
authEnabled: boolean;
onLogout: () => void;
Expand Down Expand Up @@ -63,6 +66,7 @@ export function Shell({
</div>
</header>
{children}
<AppFooter version={version} />
</main>
);
}
70 changes: 70 additions & 0 deletions web/src/components/AppFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const REPOSITORY_URL = "https://github.com/Willxup/flowlens";
const PROFILE_URL = "https://github.com/Willxup";

export function footerVersionLabel(version?: string): string | undefined {
const trimmed = version?.trim();
return trimmed ? `Version: ${trimmed}` : undefined;
}

export function AppFooter({ version }: { version?: string }) {
const versionLabel = footerVersionLabel(version);

return (
<footer className="app-footer">
<div className="app-footer-line app-footer-meta">
<span>© 2026</span>
<a href={REPOSITORY_URL} target="_blank" rel="noreferrer">
FlowLens
</a>
<span aria-hidden="true">·</span>
<a
href={`${REPOSITORY_URL}/blob/main/LICENSE`}
target="_blank"
rel="noreferrer"
>
License
</a>
</div>
<div className="app-footer-line app-footer-powered">
<span>Powered By</span>
<a
href={PROFILE_URL}
target="_blank"
rel="noreferrer"
aria-label="Willxup GitHub 主页"
>
<GitHubIcon />
<span>Willxup</span>
</a>
{versionLabel ? (
<>
<span className="app-footer-version-separator" aria-hidden="true">
·
</span>
<span className="app-footer-version">{versionLabel}</span>
</>
) : null}
</div>
</footer>
);
}

function GitHubIcon() {
return (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
<path d="M9 18c-4.51 2-5-2-7-2" />
</svg>
);
}
32 changes: 32 additions & 0 deletions web/src/components/test/AppFooter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { render, screen } from "@testing-library/react";
import { AppFooter, footerVersionLabel } from "../AppFooter";

describe("AppFooter", () => {
it("renders the project, license, author, and injected version links", () => {
render(<AppFooter version="v0.2.5" />);

expect(screen.getByText("© 2026")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "FlowLens" })).toHaveAttribute(
"href",
"https://github.com/Willxup/flowlens",
);
expect(screen.getByRole("link", { name: "License" })).toHaveAttribute(
"href",
"https://github.com/Willxup/flowlens/blob/main/LICENSE",
);
expect(
screen.getByRole("link", { name: "Willxup GitHub 主页" }),
).toHaveAttribute("href", "https://github.com/Willxup");
expect(screen.getByText("Powered By")).toBeInTheDocument();
expect(screen.getByText("Version: v0.2.5")).toBeInTheDocument();
});

it("omits the version label until a non-empty build version is available", () => {
const { rerender } = render(<AppFooter />);
expect(screen.queryByText(/Version:/)).not.toBeInTheDocument();

rerender(<AppFooter version=" " />);
expect(screen.queryByText(/Version:/)).not.toBeInTheDocument();
expect(footerVersionLabel(" v0.2.5 ")).toBe("Version: v0.2.5");
});
});
1 change: 1 addition & 0 deletions web/src/demo/fixture.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"status": "ok",
"reason": "ready",
"timezone": "Asia/Shanghai",
"version": "v0.2.5",
"auth_enabled": true,
"capabilities": {
"connection_id": true,
Expand Down
2 changes: 2 additions & 0 deletions web/src/features/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const initialStatus: StatusResponse = {
status: "degraded",
reason: "starting",
timezone: "UTC",
version: "",
auth_enabled: false,
capabilities: {
connection_id: false,
Expand Down Expand Up @@ -159,6 +160,7 @@ export function DashboardPage({
return (
<Shell
status={status.status}
version={status.version}
sourceMode={source.demo ? "demo" : "app"}
authEnabled={source.demo || status.auth_enabled}
onLogout={() => void logout()}
Expand Down
21 changes: 17 additions & 4 deletions web/src/features/dashboard/test/DashboardPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,24 @@ describe("DashboardPage", () => {
render(
<DashboardPage source={new DemoDataSource()} onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText("FlowLens")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "FlowLens GitHub 仓库" }),
).toHaveAttribute("href", "https://github.com/Willxup/flowlens");
const brand = await screen.findByRole("link", {
name: "FlowLens GitHub 仓库",
});
expect(brand).toHaveTextContent("FlowLens");
expect(brand).toHaveAttribute(
"href",
"https://github.com/Willxup/flowlens",
);
expect(screen.getByText("采集正常")).toBeInTheDocument();
expect(await screen.findByText("© 2026")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "License" })).toHaveAttribute(
"href",
"https://github.com/Willxup/flowlens/blob/main/LICENSE",
);
expect(
screen.getByRole("link", { name: "Willxup GitHub 主页" }),
).toHaveAttribute("href", "https://github.com/Willxup");
expect(await screen.findByText("Version: v0.2.5")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "跟随系统" }),
).toBeInTheDocument();
Expand Down
1 change: 1 addition & 0 deletions web/src/features/live/test/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const status: StatusResponse = {
status: "ok",
reason: "ready",
timezone: "UTC",
version: "v0.2.5",
auth_enabled: true,
capabilities: {
connection_id: true,
Expand Down
Loading
Loading