Compare commits

...
4 Commits
Author SHA1 Message Date
ksv741 54deae57b3 chore(release): 1.0.0
Frontend image / publish (push) Successful in 7m39s
Frontend verify / commits (pull_request) Successful in 39s
Frontend verify / security (pull_request) Successful in 12s
Frontend verify / verify (pull_request) Successful in 6m39s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 11s
Frontend verify / verify (push) Successful in 7m34s
2026-10-07 18:14:21 +03:00
ksv741 74f8a72ed5 perf(tbank-connection): poll screen sync every five seconds
Frontend verify / commits (pull_request) Successful in 48s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 6m57s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 11s
Frontend verify / verify (push) Successful in 8m21s
During active screen sync, status checks and observe reads run every 5s
instead of 1s; terminal state observes resources immediately.
OpenSpec change: WealthLedger/specs#21
2026-10-07 14:03:55 +03:00
ksv741 fcb938b5a2 chore: remove unused exports
Frontend verify / commits (pull_request) Successful in 42s
Frontend verify / security (pull_request) Successful in 11s
Frontend verify / verify (pull_request) Successful in 7m16s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 14s
Frontend verify / verify (push) Failing after 6m28s
2026-10-07 09:10:03 +03:00
ksv741 f873db7b02 fix(deps): resolve npm audit advisories
Frontend verify / commits (pull_request) Successful in 36s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Failing after 32m34s
2026-10-07 07:27:16 +03:00
9 changed files with 241 additions and 48 deletions
+67
View File
@@ -1,5 +1,72 @@
# Changelog
## [1.0.0](https://git.personal.ksv741.ru/WealthLedger/frontend/compare/v0.4.0...v1.0.0) (2026-10-07)
### ⚠ BREAKING CHANGES
* drop investment stateVersion/progress/resources
from the client contract and use data/metadata readiness instead of
rewritten query status. Cross-tab auth may require re-login.
### Features
* add quiet reading with rare confirmations ([5b30bb8](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/5b30bb802a4ec43c036066f9a7e178f37e747a0b))
* default journal period to last 7 days ([0ea8d7b](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/0ea8d7b62d50a4b7c92e9a9934aead6246ac97f9))
### Bug Fixes
* **deps:** remove npm dependency on Nexus ([c096fc7](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/c096fc70ef442474c9b7766fdf5772b645166b92))
* **deps:** resolve npm audit advisories ([f873db7](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/f873db7b027b6dee0c7382b4bd3df5198c7a4198))
* **deps:** resolve npm audit advisories ([10baefd](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/10baefda790e7fb491c893724ddc4da7992103f0))
* resolve frontend audit findings ([f6db28d](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/f6db28defa51bd9d8fe932ceae4c3c720aba7eb3))
* sort stock and bond events by payment date ([f5f4447](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/f5f4447d60661aa92c80b50a16e8cb194dfa161f))
### Performance
* **tbank-connection:** poll screen sync every five seconds ([74f8a72](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/74f8a72ed55af2348fa3668f57312f4fdfc1e8a8)), references [WealthLedger/specs#21](https://git.personal.ksv741.ru/WealthLedger/specs/issues/21)
### Refactoring
* address frontend review feedback ([a4046db](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/a4046dbe87bd7dc949df32da3b6aaf174e6f5e8f))
* clarify frontend model ownership ([0865b35](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/0865b35f93865b4052181d918cee80c995312930))
* clarify frontend module ownership ([f9f0f69](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/f9f0f6921e86f6c81510373c680b136e4580f386))
* encapsulate instrument chart data ([64fe902](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/64fe902192b60a72440635f5610ebe02cf8543f8))
* enforce enum machine codes [#18](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/18) ([5b69003](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/5b6900351612b6ba34c694fc99612733e7901c79))
* enforce frontend view model boundaries ([ca0ad4f](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/ca0ad4f824a47fa47e32d4717e27b15c8a985f7d))
* move PortfolioStructure to widgets layer ([c25f6bd](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/c25f6bd85b88c4c7acb2f5ae6fc013aec77005b2))
* remove redundant page view models ([fb3a5a6](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/fb3a5a6eaa0223a15ea2c9f585a06459371f0a25))
* share instrument search presentation ([32a7786](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/32a77860c8eaea4a59a26e55b2d66976fd8f9484))
* share source sync status presentation ([0bc8bc3](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/0bc8bc36b7263b7f0120a08df840b290b40d8116))
* simplify auth and instrument pages ([b404c18](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/b404c1802a74f5f369173fdfcb086a3d5760e35a))
* simplify calendar and form state ([94adbaf](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/94adbafa344b23d7f95c31db975d99d990bfcb8b))
* simplify errors and focus tests ([7e976c3](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/7e976c330c54715018eda4bff0764c5d4de9c8b0))
* simplify finance view models ([2844b22](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/2844b22dbace41a02c1b5a07b3801e84c66b3528))
* simplify frontend state and mutation boundaries ([a7537da](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/a7537da34452ab1a15138ab0c1cd93d53d06c517))
* simplify query and session ownership ([6999707](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/69997075e569da3edd43e242ef36f09311262412))
* simplify resource queries and layout tests ([c1b0873](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/c1b08735e3a45d565c3836ae37ad76186bf48f26))
* tighten types and reuse instrument UI ([7c28a42](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/7c28a423dd8a3cc4a5bdcb49b8de7018979df413))
* unify bank form types and actions ([ff67a76](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/ff67a768addc0c4ba55247f5e2d9d5d5d5aec734))
### CI
* reduce duplicate actions and serialize checks ([95fdc87](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/95fdc8759e20f9baa4ac3d00e703752c79010e01))
### Tests
* remove redundant frontend checks ([320ea59](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/320ea5922a70ab81ce4f05c3c90eb7b6a664fa39))
### Styles
* pin ternary operator formatting [#18](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/18) ([12c3b72](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/12c3b72bfb32dff5f1ffdb36519c90f7ea852488))
### Maintenance
* add typed ESLint checks [#18](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/18) ([5763010](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/57630102e4bcc6b555dd546ba373a14a88256eed))
* finish frontend ESLint migration [#18](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/18) ([72d7835](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/72d7835227340e349d99f57db134a635f276f6e9))
* guard presentation data boundaries ([6a89b08](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/6a89b08ee571562fea349801169c40f1f993e9b9))
* guard unsafe DOM HTML with ESLint [#18](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/18) ([061ec70](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/061ec708c651029cd7d61d22319d11e7ac9f074e))
* remove unused exports ([fcb938b](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/fcb938b5a29fc82f00bbaa8b2ee776ca57dad010))
## [0.4.0](https://git.personal.ksv741.ru/WealthLedger/frontend/compare/v0.3.0...v0.4.0) (2026-09-26)
### Features
+8 -6
View File
@@ -1,12 +1,12 @@
{
"name": "wealth-ledger-frontend",
"version": "0.4.0",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wealth-ledger-frontend",
"version": "0.4.0",
"version": "1.0.0",
"workspaces": [
"packages/*"
],
@@ -13980,8 +13980,9 @@
}
},
"node_modules/smol-toml": {
"version": "1.8.0",
"integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz",
"integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -14038,8 +14039,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wealth-ledger-frontend",
"private": true,
"version": "0.4.0",
"version": "1.0.0",
"type": "module",
"engines": {
"node": "24.21.0"
-4
View File
@@ -1,6 +1,2 @@
export { useScreenSync } from "./model/screen-sync";
export {
InvestmentRefreshState,
type InvestmentRefreshStateProps,
} from "./ui/InvestmentRefreshState/InvestmentRefreshState.view";
export { TbankConnectionSection } from "./ui/TbankConnectionSection/TbankConnectionSection";
@@ -420,7 +420,7 @@ describe("page-owned screen synchronization", () => {
it.each(["leave", "logout"])(
"publishes completed empty coverage while history runs, then stops on %s",
async (boundary) => {
vi.useFakeTimers({ toFake: ["Date"] });
vi.useFakeTimers();
try {
auth();
@@ -429,15 +429,13 @@ describe("page-owned screen synchronization", () => {
const modes: (null | string)[] = [];
const snapshot = () =>
resource(
isCompleted
? []
: null,
{
dataRevision: isCompleted ? "covered" : "initial",
sync: { errorCode: null, id: "history", state: "running" },
},
);
resource(isCompleted ? [] : null, {
dataRevision: isCompleted ? "covered" : "initial",
sync: { errorCode: null, id: "history", state: "running" },
unavailableReason: isCompleted
? undefined
: "CALENDAR_OR_ACCOUNT_HISTORY",
});
server.use(
http.get("*/api/v1/brokerage-accounts", ({ request }) => {
@@ -472,31 +470,26 @@ describe("page-owned screen synchronization", () => {
{ wrapper: createQueryWrapper().wrapper },
);
await waitFor(() =>
expect(result.current.accounts.meta?.sync.id).toBe("history"),
);
await waitFor(() => expect(polls).toBeGreaterThan(0));
await vi.waitFor(() => expect(modes).toEqual([null, "observe"]));
expect(polls).toBe(1);
expect(result.current.sync.refreshing).toBe(true);
expect(result.current.accounts.data).toBeUndefined();
isCompleted = true;
vi.setSystemTime(Date.now() + 61_000);
await waitFor(() => expect(result.current.accounts.data).toEqual([]), {
timeout: 3000,
});
expect(result.current.sync.refreshing).toBe(true);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 1100));
});
expect(modes[0]).toBeNull();
expect(modes.slice(1).every(mode => mode === "observe")).toBe(true);
isCompleted = true;
await act(() => vi.advanceTimersByTimeAsync(5000));
await vi.waitFor(() =>
expect(result.current.accounts.data).toEqual([]),
);
expect(result.current.sync.refreshing).toBe(true);
expect(modes).toEqual([null, "observe", "observe"]);
if (boundary === "leave") unmount();
else act(() => configureAuthClient());
const stopped = polls;
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 1100));
});
expect(polls).toBe(stopped);
await act(() => vi.advanceTimersByTimeAsync(60_000));
expect(polls).toBe(2);
expect(modes).toHaveLength(3);
}
finally {
vi.useRealTimers();
@@ -504,6 +497,144 @@ describe("page-owned screen synchronization", () => {
},
);
it("runs the next status poll and observe read only after five seconds", async () => {
auth();
const modes: (null | string)[] = [];
const pollGaps: number[] = [];
let polls = 0;
let previousPollAt = 0;
server.use(
http.get("*/api/v1/brokerage-accounts", ({ request }) => {
modes.push(new URL(request.url).searchParams.get("sync"));
return HttpResponse.json(
resource([], {
sync: { errorCode: null, id: "history", state: "running" },
}),
);
}),
http.get("*/api/v1/tbank-sync/:id", () => {
const now = Date.now();
if (previousPollAt) pollGaps.push(now - previousPollAt);
previousPollAt = now;
polls++;
return HttpResponse.json({
errorCode: null,
id: "history",
now: "2026-09-25T00:00:00Z",
scope: null,
state: "running",
});
}),
);
vi.useFakeTimers();
try {
const { unmount } = renderHook(
() => {
const accounts = useBrokerageAccounts();
return {
accounts,
sync: useScreenSync({ screen: "accounts" }, [accounts]),
};
},
{ wrapper: createQueryWrapper().wrapper },
);
await vi.waitFor(() => expect(modes).toEqual([null, "observe"]));
expect(polls).toBe(1);
await act(() => vi.advanceTimersByTimeAsync(2500));
expect(polls).toBe(1);
expect(modes).toEqual([null, "observe"]);
await act(() => vi.advanceTimersByTimeAsync(5000));
await vi.waitFor(() => expect(polls).toBe(2));
expect(modes).toEqual([null, "observe", "observe"]);
expect(pollGaps).toHaveLength(1);
expect(pollGaps[0]).toBeGreaterThanOrEqual(5000);
unmount();
await act(() => vi.advanceTimersByTimeAsync(60_000));
expect(polls).toBe(2);
expect(modes).toHaveLength(3);
}
finally {
vi.useRealTimers();
}
});
it("observes resources immediately when a status response turns terminal", async () => {
auth();
const modes: (null | string)[] = [];
let isCompleted = false;
let polls = 0;
const meta = () =>
resource([], {
dataRevision: isCompleted ? "covered" : "initial",
sync: { errorCode: null, id: "history", state: "running" },
});
server.use(
http.get("*/api/v1/brokerage-accounts", ({ request }) => {
modes.push(new URL(request.url).searchParams.get("sync"));
return HttpResponse.json(meta());
}),
http.get("*/api/v1/tbank-sync/:id", () => {
polls++;
return HttpResponse.json({
errorCode: null,
id: "history",
now: "2026-09-25T00:00:00Z",
scope: null,
state: isCompleted ? "succeeded" : "running",
});
}),
);
vi.useFakeTimers();
try {
const { result } = renderHook(
() => {
const accounts = useBrokerageAccounts();
return {
accounts,
sync: useScreenSync({ screen: "accounts" }, [accounts]),
};
},
{ wrapper: createQueryWrapper().wrapper },
);
await vi.waitFor(() => expect(modes).toEqual([null, "observe"]));
expect(polls).toBe(1);
expect(result.current.sync.refreshing).toBe(true);
isCompleted = true;
await act(() => vi.advanceTimersByTimeAsync(5000));
await vi.waitFor(() => expect(modes).toEqual([null, "observe", "observe"]));
expect(polls).toBe(2);
expect(result.current.accounts.meta?.dataRevision).toBe("covered");
expect(result.current.sync.refreshing).toBe(false);
await act(() => vi.advanceTimersByTimeAsync(60_000));
expect(polls).toBe(2);
expect(modes).toHaveLength(3);
}
finally {
vi.useRealTimers();
}
});
it("clears private snapshots when status confirms access loss", async () => {
auth();
server.use(
@@ -29,7 +29,7 @@ type ScreenResource = {
refetch: () => Promise<unknown>;
};
const STATUS_INTERVAL_MS = 1000;
const STATUS_INTERVAL_MS = 5000;
const FIRST_SOURCE_RECOVERY_DELAY_MS = 5000;
const SECOND_SOURCE_RECOVERY_DELAY_MS = 15_000;
@@ -58,4 +58,4 @@ const InvestmentRefreshState: FC<InvestmentRefreshStateProps> = (props) => {
);
};
export { InvestmentRefreshState, type InvestmentRefreshStateProps };
export { InvestmentRefreshState };
+1 -4
View File
@@ -8,8 +8,5 @@ export {
formatSnapshotTime,
instrumentLogoUrl,
} from "./display";
export {
type InitialAvailability,
useInitialAvailability,
} from "./initial-availability";
export { useInitialAvailability } from "./initial-availability";
export { useUrlPeriod } from "./useUrlPeriod";
+1 -1
View File
@@ -46,4 +46,4 @@ function useInitialAvailability(
return unavailableIdentity === input.identity ? "unavailable" : "loading";
}
export { type InitialAvailability, useInitialAvailability };
export { useInitialAvailability };