Compare commits
2
Commits
fcb938b5a2
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54deae57b3 | ||
|
|
74f8a72ed5 |
@@ -1,5 +1,72 @@
|
|||||||
# Changelog
|
# 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)
|
## [0.4.0](https://git.personal.ksv741.ru/WealthLedger/frontend/compare/v0.3.0...v0.4.0) (2026-09-26)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wealth-ledger-frontend",
|
"name": "wealth-ledger-frontend",
|
||||||
"version": "0.4.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wealth-ledger-frontend",
|
"name": "wealth-ledger-frontend",
|
||||||
"version": "0.4.0",
|
"version": "1.0.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "wealth-ledger-frontend",
|
"name": "wealth-ledger-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "24.21.0"
|
"node": "24.21.0"
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ describe("page-owned screen synchronization", () => {
|
|||||||
it.each(["leave", "logout"])(
|
it.each(["leave", "logout"])(
|
||||||
"publishes completed empty coverage while history runs, then stops on %s",
|
"publishes completed empty coverage while history runs, then stops on %s",
|
||||||
async (boundary) => {
|
async (boundary) => {
|
||||||
vi.useFakeTimers({ toFake: ["Date"] });
|
vi.useFakeTimers();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auth();
|
auth();
|
||||||
@@ -429,15 +429,13 @@ describe("page-owned screen synchronization", () => {
|
|||||||
const modes: (null | string)[] = [];
|
const modes: (null | string)[] = [];
|
||||||
|
|
||||||
const snapshot = () =>
|
const snapshot = () =>
|
||||||
resource(
|
resource(isCompleted ? [] : null, {
|
||||||
isCompleted
|
dataRevision: isCompleted ? "covered" : "initial",
|
||||||
? []
|
sync: { errorCode: null, id: "history", state: "running" },
|
||||||
: null,
|
unavailableReason: isCompleted
|
||||||
{
|
? undefined
|
||||||
dataRevision: isCompleted ? "covered" : "initial",
|
: "CALENDAR_OR_ACCOUNT_HISTORY",
|
||||||
sync: { errorCode: null, id: "history", state: "running" },
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
server.use(
|
server.use(
|
||||||
http.get("*/api/v1/brokerage-accounts", ({ request }) => {
|
http.get("*/api/v1/brokerage-accounts", ({ request }) => {
|
||||||
@@ -472,31 +470,26 @@ describe("page-owned screen synchronization", () => {
|
|||||||
{ wrapper: createQueryWrapper().wrapper },
|
{ wrapper: createQueryWrapper().wrapper },
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() =>
|
await vi.waitFor(() => expect(modes).toEqual([null, "observe"]));
|
||||||
expect(result.current.accounts.meta?.sync.id).toBe("history"),
|
expect(polls).toBe(1);
|
||||||
);
|
|
||||||
await waitFor(() => expect(polls).toBeGreaterThan(0));
|
|
||||||
expect(result.current.sync.refreshing).toBe(true);
|
expect(result.current.sync.refreshing).toBe(true);
|
||||||
expect(result.current.accounts.data).toBeUndefined();
|
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();
|
isCompleted = true;
|
||||||
expect(modes.slice(1).every(mode => mode === "observe")).toBe(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();
|
if (boundary === "leave") unmount();
|
||||||
else act(() => configureAuthClient());
|
else act(() => configureAuthClient());
|
||||||
const stopped = polls;
|
|
||||||
await act(async () => {
|
await act(() => vi.advanceTimersByTimeAsync(60_000));
|
||||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
expect(polls).toBe(2);
|
||||||
});
|
expect(modes).toHaveLength(3);
|
||||||
expect(polls).toBe(stopped);
|
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
vi.useRealTimers();
|
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 () => {
|
it("clears private snapshots when status confirms access loss", async () => {
|
||||||
auth();
|
auth();
|
||||||
server.use(
|
server.use(
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type ScreenResource = {
|
|||||||
refetch: () => Promise<unknown>;
|
refetch: () => Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_INTERVAL_MS = 1000;
|
const STATUS_INTERVAL_MS = 5000;
|
||||||
const FIRST_SOURCE_RECOVERY_DELAY_MS = 5000;
|
const FIRST_SOURCE_RECOVERY_DELAY_MS = 5000;
|
||||||
const SECOND_SOURCE_RECOVERY_DELAY_MS = 15_000;
|
const SECOND_SOURCE_RECOVERY_DELAY_MS = 15_000;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user