Skip to content
This repository was archived by the owner on Oct 28, 2026. It is now read-only.
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: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All notable changes to Allthing are recorded here. The format follows [Keep a Ch

### Fixed

- Explain that complete 24-hour activity totals require a gap-free window instead of promising a first count from its start time alone.
- Count active accounts from checked, consecutive Hypersnap events with canonical block times carried across pages, declared event totals and both shards' coverage. The new verification epoch keeps old rows without certifying their totals. Full account and 32-byte signing-key identities prevent prefix collisions; incomplete attribution stays unknown, and a fenced atomic save rejects stale concurrent runs. Action totals are labelled as merge events because the same message can appear on both shards.
- Recover one confirmed missing entry-loader request without reloading or changing drafts and sessions; persistent failures show an accessible Reload button even if early Home data arrives later. Healthy startup adds no recovery traffic.

Expand All @@ -33,6 +34,8 @@ All notable changes to Allthing are recorded here. The format follows [Keep a Ch

### Development

- Make attachment handoff fixtures verify file-selection order under deliberately reversed upload completion, and wait for the current finite animations when a transition is canceled or replaced.

- Pause the search fixture's clock before navigation so an old real-time timer cannot move it backward; preserve the exact timer-cancellation and 250 ms debounce checks.
- Keep the live front-page check's exact navigation contract in step with the Home and Library links; first-paint, layout and interaction limits remain unchanged.
- Library Back checks compare with the card position at the actual click, after browser actionability scrolling, while retaining the two-pixel restoration limit and exact cached-page checks.
Expand Down
8 changes: 6 additions & 2 deletions crates/allthing-web/src/pages/dau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,15 @@ fn Meter(counted: ActivityPayload, today: i64) -> impl IntoView {
Some(to) => format!("Unique accounts active in the 24 hours to {}", moment(to)),
None => "Unique accounts active in a rolling 24 hours".to_owned(),
};
// The count and how it compares, or when it comes: the first window
// ends 24 hours after counting began, on the hour.
// The count and how it compares, or what coverage it still needs.
// A start-time estimate cannot promise a complete window across gaps.
let active = counted.rolling_to.and(counted.rolling_24h);
let pending = match (counted.rolling_to, active) {
(_, Some(_)) => None,
(_, None) if !counted.gaps.is_empty() => Some(
"A 24-hour count requires a whole window without recorded gaps. Partial totals remain lower bounds."
.to_owned(),
),
(Some(_), None) => Some(match counted.since.map(|since| (since + 86_400 + 3_599).div_euclid(3_600) * 3_600) {
Some(at) => format!(
"The first 24-hour count comes once the 24 hours to {}, {} have been read.",
Expand Down
27 changes: 22 additions & 5 deletions e2e/composer-attachments.fixture.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,18 @@ class Media {
posts = 0;
held = false;
refuse: [number, string][] = [];
private waiting: (() => void)[] = [];
private waiting: { type: string; go: () => void }[] = [];
constructor(public limits: object | null = LIMITS) {}

release() {
this.held = false;
this.waiting.splice(0).forEach((go) => go());
this.waiting.splice(0).forEach(({ go }) => go());
}

releaseType(type: string) {
const index = this.waiting.findIndex((upload) => upload.type === type);
if (index < 0) throw new Error(`No held ${type} upload`);
this.waiting.splice(index, 1)[0].go();
}

async serve(page: Page) {
Expand All @@ -85,7 +91,7 @@ class Media {
this.posts += 1;
const body = request.postDataBuffer() ?? Buffer.alloc(0);
const type = request.headers()["content-type"];
if (this.held) await new Promise<void>((go) => this.waiting.push(go));
if (this.held) await new Promise<void>((go) => this.waiting.push({ type, go }));
const refused = this.refuse.shift();
// The page may have stopped it meanwhile.
if (refused) return route.fulfill({ status: refused[0], json: { error: refused[1] } }).catch(() => {});
Expand Down Expand Up @@ -725,22 +731,33 @@ test("a photo's Exif, GPS position, XMP and IPTC are taken out before it's uploa

test("where Allthing doesn't post, the draft goes to the Farcaster app with its files as embeds", async ({ page }) => {
const media = new Media();
await ready(page, media, { key: false });
const calls = await ready(page, media, { key: false });
await page.getByRole("button", { name: "Write a cast" }).click();
const dialog = page.getByRole("dialog", { name: "New cast" });
await dialog.getByRole("textbox").fill("From my phone's camera roll");
media.held = true;
await attach(page, dialog, [await drawn(page, "one.png", "image/png", 0), await drawn(page, "two.jpg", "image/jpeg", 120)]);
await expect.poll(() => media.posts).toBe(2);
await expect(dialog.getByRole("button", { name: "Uploading…" })).toBeDisabled();
// Finish the second selected file first: embeds follow selection order,
// while the fixture's uploads are recorded in completion order.
media.releaseType("image/jpeg");
await expect.poll(() => media.uploads.map((upload) => upload.type)).toEqual(["image/jpeg"]);
await expect(dialog.getByRole("progressbar")).toHaveCount(1);
await expect(dialog.getByRole("button", { name: "Uploading…" })).toBeDisabled();
media.release();
const post = dialog.getByRole("button", { name: "Post in Farcaster" });
await expect(post).toBeEnabled();
expect(media.uploads.map((upload) => upload.type)).toEqual(["image/jpeg", "image/png"]);
await post.click();
const handoff = page.getByRole("dialog", { name: "Post in Farcaster" });
const href = (await handoff.getByRole("link", { name: /Open on farcaster\.xyz/ }).getAttribute("href"))!;
const url = new URL(href);
expect(url.searchParams.get("text")).toBe("From my phone's camera roll");
expect(url.searchParams.getAll("embeds[]")).toEqual(media.uploads.map((u) => u.url));
const of = (type: string) => media.uploads.find((upload) => upload.type === type)!;
expect(url.searchParams.getAll("embeds[]")).toEqual([of("image/png").url, of("image/jpeg").url]);
expect(calls.sent).toHaveLength(0);
expect(calls.scheduled).toHaveLength(0);
});

test("a scheduled cast keeps its files, on /scheduled and when it's changed", async ({ page }) => {
Expand Down
11 changes: 11 additions & 0 deletions e2e/dau.fixture.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,17 @@ test.describe("with the Worker", () => {
await expect(meter.locator(".dau-what")).toHaveText(`The first 24-hour count comes once the 24 hours to ${short(Math.floor(first / DAY))}, ${hh(first)} UTC have been read.`);
await expect(meter.locator(".dau-change")).toHaveCount(0);
await expect(page.locator(".dau-behind")).toHaveCount(0);
// Reaching the first possible end time is not sufficient if source
// coverage has a gap: do not promise a complete count at that time.
await serve(page, payload({
rolling_24h: undefined, previous_24h: undefined, since: to - 3_600,
gaps: [[to - 1_800, to - 1_798]],
days: [days[29]], hourly: [], active_30d: undefined, apps_24h: undefined,
}));
await page.reload();
await expect(meter.locator(".dau-big")).toHaveText("—");
await expect(meter.locator(".dau-what")).toHaveText("A 24-hour count requires a whole window without recorded gaps. Partial totals remain lower bounds.");
await expect(meter.locator(".dau-change")).toHaveCount(0);
});

const invalidProofs = [
Expand Down
8 changes: 6 additions & 2 deletions e2e/wallet-shares.fixture.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,13 @@ const six = [
evm("zora", "Zora", 200, 2),
];
const shown = async (list: Locator) => (await list.locator("li:visible .pct").allTextContents()).reduce((sum, pct) => sum + parseInt(pct, 10), 0);
/** Every animation that ends has ended: the screenshot shows the page at rest. */
/** Wait for the current finite animations, including replacements, to finish.
* CSS transitions can be canceled as a popover settles; their old `finished`
* promises reject even though the replacement still needs to run. Polling
* the live set waits for the page at rest without hiding an evaluation error.
*/
const settled = (page: Page) =>
page.evaluate(() => Promise.all(document.getAnimations().filter((a) => a.effect?.getComputedTiming().iterations !== Infinity).map((a) => a.finished)));
page.waitForFunction(() => document.getAnimations().every((a) => a.effect?.getComputedTiming().endTime === Infinity || a.playState === "finished"));

for (const width of [1280, 390]) {
test(`at ${width}, the shares shown add up to 100 on the profile and its card, and both say how they're counted`, async ({ browser }, testInfo) => {
Expand Down
Loading