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
4 changes: 4 additions & 0 deletions internal/webapp/e2e_serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ func seedE2E(t *testing.T, state, prefix, projectID string) {
// Read history for the file the seed deletes above: heat rows outlive
// their file, and the Dashboard must say so rather than drop them.
{"scratch.md", ReadKindHuman, "alice@x.io", 4},
// The only share reads in the seed, and deliberately on a path no
// other assertion counts: the Dashboard's share lens has to isolate
// exactly one file, which it can't prove if the file has other reads.
{"notes/deep/topic.md", ReadKindShare, "tok-e2e/203.0.113.7", 3},
} {
stats = append(stats, ReadStat{Project: projectID, Path: rd.path, Day: day,
Kind: rd.kind, Actor: rd.actor, Count: rd.n, Last: now})
Expand Down
62 changes: 62 additions & 0 deletions internal/webapp/frontend/e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,68 @@ test("the orphan footnote follows the lens", async ({ page }) => {
await expect(page.locator(".in-hp-row", { hasText: "scratch.md" })).toHaveCount(1);
});

// BEA-62: the page named three read types and filtered two. Share reads are
// the ones an owner most wants to isolate — traffic from links they minted.
test("the shared lens isolates share-link reads and paints them share-colored", async ({
page,
}) => {
await login(page);
const pid = await wikiId(page);
await page.goto(`/${pid}/dashboard`);
await expect(page.locator(".in-lens-btn")).toHaveText([
"All reads",
"Human reads",
"Agent reads",
"Shared reads",
]);

await page.getByRole("button", { name: "Shared reads" }).click();
// notes/deep/topic.md carries the seed's only share reads. Other specs mint
// and open their own links against this shared hub, so assert the filter's
// invariant — share reads only — not a row count that moves with test order.
const topic = page.locator(".in-hp-row", { hasText: "notes/deep/topic.md" });
await expect(topic).toHaveCount(1);
await expect(topic.locator(".in-hp-count")).toHaveText("3");

// The bar is the whole point: under a share lens the reads must not be
// painted as somebody else's traffic.
await expect(topic.locator(".in-hp-share")).not.toHaveCSS("width", "0px");
for (const cls of [".in-hp-agent", ".in-hp-human"]) {
await expect(topic.locator(cls)).toHaveCSS("width", "0px");
}
await expect(page.locator(".in-sw.share")).toBeVisible();

// Files read only by people or agents drop out entirely — the lens filters,
// it does not merely re-sort. Neither path is shared by any spec.
for (const p of ["archive/retired-spec.md", "scratch.md"]) {
await expect(page.locator(".in-hp-row", { hasText: p })).toHaveCount(0);
}

// "All reads" still means human + agent + share.
await page.getByRole("button", { name: "All reads" }).click();
await expect(page.locator(".in-hp-row", { hasText: "archive/retired-spec.md" })).toHaveCount(1);
await expect(topic).toHaveCount(1);
});

// The empty scope must stay empty: falling back to every file would be worse
// than showing nothing, because the number would silently mean something else.
test("a project with no share reads says so under the shared lens", async ({ page }) => {
await login(page);
const made = await (await page.request.post("/api/projects", { data: { name: "noshare" } })).json();
try {
await page.request.put(
`/api/p/${made.project.id}/upload/content?path=a.md`,
{ data: "# A\n", headers: { "content-type": "text/markdown" } },
);
await page.goto(`/${made.project.id}/dashboard`);
await page.getByRole("button", { name: "Shared reads" }).click();
await expect(page.locator(".dl-empty")).toContainText("No reads in the window yet.");
await expect(page.locator(".in-hp-row", { hasText: "a.md" })).toHaveCount(0);
} finally {
await page.request.delete("/api/projects/" + made.project.id);
}
});

// A brand-new project used to draw ~840px of empty frames with the quadrant
// labels floating over nothing — the first screen every project shows.
// Created at runtime and deleted: a permanent fixture sorting before "wiki"
Expand Down
30 changes: 21 additions & 9 deletions internal/webapp/frontend/src/components/Insights.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,24 @@ interface Pt {
orphan?: boolean;
}

type Lens = "all" | "human" | "agent";
type Lens = "all" | "human" | "agent" | "share";

const LENS_ORDER = ["all", "human", "agent", "share"] as const;
const LENS_LABEL: Record<Lens, string> = {
all: "All reads",
human: "Human reads",
agent: "Agent reads",
share: "Shared reads",
};

/* A pure lens paints one color by definition — its own. Without a share
entry the bar would fall back to the real split and show share-only reads
in somebody else's color. */
const PURE: Partial<Record<Lens, { agent: number; human: number; share: number }>> = {
agent: { agent: 1, human: 0, share: 0 },
human: { agent: 0, human: 1, share: 0 },
share: { agent: 0, human: 0, share: 1 },
};

export function Insights(props: {
flatFiles: Node[];
Expand Down Expand Up @@ -175,13 +192,13 @@ export function Insights(props: {
: "Reads over the last 30 days × how long since each file changed. Hot but stale knowledge — read a lot, maintained by nobody — is the danger zone."}
</p>
<div className="in-lens">
{(["all", "human", "agent"] as const).map((l) => (
{LENS_ORDER.map((l) => (
<button
key={l}
className={"in-lens-btn" + (l === lens ? " active" : "")}
onClick={() => setLens(l)}
>
{l === "all" ? "All reads" : l === "human" ? "Human reads" : "Agent reads"}
{LENS_LABEL[l]}
</button>
))}
</div>
Expand Down Expand Up @@ -578,12 +595,7 @@ function HotPath({
<div className="in-hotpath">
{top.map((p) => {
// Split of the lens reads: pure lenses are single-color by definition.
const f =
lens === "agent"
? { agent: 1, human: 0, share: 0 }
: lens === "human"
? { agent: 0, human: 1, share: 0 }
: hotPathSplit(p);
const f = PURE[lens] ?? hotPathSplit(p);
const pct = (p.reads / max) * 100;
// The file view would land on the not-found page for a path that
// left the project; History still has the content.
Expand Down
1 change: 0 additions & 1 deletion internal/webapp/static/assets/index-BhUNiSiq.css

This file was deleted.

1 change: 1 addition & 0 deletions internal/webapp/static/assets/index-Cb0dESCp.css

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions internal/webapp/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
<script type="module" crossorigin src="/assets/index-Cj99Pu7j.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BhUNiSiq.css">
<script type="module" crossorigin src="/assets/index-Cw2mk7qH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cb0dESCp.css">
</head>
<body>
<div id="root"></div>
Expand Down
Loading