Skip to content

chore: Release 2.0.0-beta.1 Ailanthus - #101

Merged
nfebe merged 20 commits into
mainfrom
dev
Jul 14, 2026
Merged

chore: Release 2.0.0-beta.1 Ailanthus#101
nfebe merged 20 commits into
mainfrom
dev

Conversation

@nfebe

@nfebe nfebe commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

First beta of the v2.0.0 line, codenamed Ailanthus, the tree of heaven and the first in the tree-name series (fittingly, its name starts with "AI" for this AI-forward beta). Promotes the accumulated dev work to main for a beta.

Headline changes since the last release:

  • Connected AI clients: a settings panel to issue, copy, and revoke the token an external AI client needs for the MCP server, and the endpoint URL to paste in.
  • Assistant: live streaming progress with a polling fallback, a dashboard assistant hub, and richer chat output.
  • Extension slots that render plugin-described UI.
  • Admin console UI, financial-position and holdings views, and faster transaction reports.

The release workflow generates the full changelog when the tag is cut.

nfebe added 19 commits June 16, 2026 01:03
The dashboard and reports pages were slow because stats-heavy views issued
many redundant requests and refetched already-cached data on every visit.

Statistics are now shared across the components that display them, so the
stats endpoint is requested once per view instead of once per component, with
a short client cache and request de-duplication. They refresh automatically
when a transaction is added, edited, or deleted.

Reports now render from server-computed aggregates immediately and load the
detailed transaction data needed for the calendar and trends in the
background, and no longer fetch a full previous period just to compare.

Initial sign-in data loading no longer throws away a warm cache and loads
shared data and transactions at the same time rather than one after another.
The dashboard waited for one large statistics response before anything
appeared, so the first load felt slow.

It now requests the statistics in sections and fills the dashboard in as each
arrives: the headline totals paint first from the cheapest section, then the
category breakdown, then the insights, each keeping its own skeleton until its
data lands. Repeat requests and concurrent consumers still share one fetch per
section.
One chat surface: history, attachments, widget-rendered answers, and a side canvas for reports.
List recent imports to open or delete, and link the chat card to a specific live review.
Chat is the default landing after login, with a setting to use the dashboard instead.
Neutral, unified nav and top-bar icons; the assistant moves to /assistant with a redirect.
Locale strings for the assistant, imports and canvas across supported languages.
The assistant landing now greets you with a snapshot of your finances
instead of a blank box. A swipeable panel surfaces this period's net
flow, savings rate, top categories and recent activity, with a rotating
highlight that doubles as a question you can ask. It carves out the
content area against a softly coloured background and fills the card
edge to edge.

Starter prompts are now drawn at random from a large pool on each visit
so the empty state stays fresh, and each prompt carries a matching icon.

The message composer reads as one clean box whether it sits in the
middle or docked at the bottom, an ongoing conversation gains a subtle
finance backdrop instead of a flat page, and the chat history sidebar
picks up a light tint.
The dashboard now opens with an assistant section: a greeting, a free text
ask box, and quick actions to log a transaction, transfer money, build a
report, or import a document. Logging and transfers save directly and stay
on the dashboard; reports and imports hand the request to the chat. The
wallet and period controls moved into a stats bar below it, and the
assistant section can be dismissed for the session and returns on refresh.

In the chat, the assistant can present more report widgets (callouts,
timelines, progress, comparisons, lists, quick actions, and a question with
selectable answers), and a generated report now shows a preview inline
instead of only a card. Quick action popups always show their submit
button, and slow responses keep showing progress instead of prematurely
claiming the response did not finish. The advanced importer uses the shared
icon set for visual consistency.
Reports now load noticeably faster and no longer hide charts while data
is still arriving.

- Report transaction pages are fetched concurrently instead of one at a
  time, so wide date ranges render much sooner.
- Per-category spending trends are built in a single pass rather than
  rescanning every transaction once per category.
- The charts and other report tabs stay visible whenever chart data
  exists, with a loading state instead of a flash of the empty screen.
- A notice appears when a period has more transactions than can be
  charted at once, so partial figures are not mistaken for complete ones.
- Amounts that could not be converted to the default currency are now
  flagged, so cross-currency totals are no longer silently understated.

The transactions spreadsheet streams rows in chunks: the first rows and
running totals appear immediately and grow as the rest load. Columns can
be sorted, the current view can be exported to a CSV file, and search is
debounced so typing stays smooth on large sets.
- Add the Financial Position screen: net worth, a money in/out ledger
  by intent, and a drill-down into the underlying transactions.
- Add a Holdings screen and form backed by the holdings API, with live
  crypto pricing and manual assets; holdings feed total net worth.
- Add an intent selector to the transaction form so income and expenses
  can be tagged as loans, debt, investments or gifts.
- Refine navigation: the dashboard is the default landing, the AI
  assistant is one entry with a Discussions sub-item, and less-used
  items move down.
- Let budgets be created in any supported currency, not just the
  currencies of existing wallets.
- Use Solar duotone icons (via Iconify) for the assistant landing
  stat cards so the large art reads richly instead of as thin lines.
Add an admin area, reached from the avatar menu when the signed-in user
is an administrator. It has its own left-rail shell with an overview
dashboard (KPIs, charts and rankings), demographics, a searchable user
table whose rows open a detailed profile, and outreach: a history of
sent emails plus a composer with a formatted (markdown) body, a header
image, attachments, audience or specific-user targeting, and a live
preview of the rendered email.

Refresh the marketing surfaces to match the assistant-first direction:
the login slides now lead with the assistant and bank integrations, the
onboarding flow gets a brand-washed background and illustrated steps with
a country step, and the emoji and stripe cards in onboarding and the
learning modal are replaced with vector icons and clean surfaces.
The user country preference now uses the key "country" instead of
"default-country". Nothing reads from a non-default country, so the
default- prefix was misleading next to the other locale preferences.
The client now renders UI that backend integrations describe, with no
per-plugin code for the common cases. Installed integrations are loaded after
login and surfaced into named slots (settings integrations panel and tab bar,
onboarding steps, dashboard widgets, sidebar nav). A descriptor renderer draws
the common cases (card, connect, gated panel, onboarding step) from a fixed
catalog; when a descriptor names a component, a plugin Nuxt layer can register a
real one for that slot as an escape hatch.

Onboarding is now a data-driven step list: built-in steps and plugin steps
merge by order instead of hardcoded branches. Contributions are gated on the
server's entitlement decision and shown with a needs-setup state when not yet
configured.
Pie and donut charts silently came out as bars: the standard analytics rows
carry identifier and transaction-count columns alongside the value, and those
were counted as extra data series, which tipped the component into its
multi-series bar fallback (and could even have plotted the id).

Chart data now ignores identifier and count columns when a real value column
is present, so a single-total breakdown renders in the shape the assistant
asked for. Count-only data still charts, since the count is then the value.
A multi-step assistant turn showed only animated dots for its whole duration,
which on a slow turn reads as a stall.

The chat now lists the steps the assistant is taking (looking through records,
crunching the numbers, putting the report together) as they arrive on each
poll, so a long turn shows visible progress instead of a blank wait. Once the
answer lands the steps give way to the result as before.
The chat now subscribes to its session's private channel and applies each
progress step and the final answer the instant they are pushed, instead of
depending on the 3-second poll.

When Reverb is not configured or the socket is unavailable (including unit
tests), it falls back to the existing polling unchanged, so nothing regresses.
Printing a report came out unstyled, with the browser's page-title and URL
header in the margins and a blank page of space after the heading.

It now prints the live, already-styled report in place: a clone of the report
is shown only for printing while the rest of the page is hidden, so it uses the
styles already loaded on screen. The page margin is zeroed so the browser omits
its own header and footer, the report supplies its own padding, and the cloned
content's height is reset so it flows to its content instead of reserving a
whole empty page. It prints in the light theme regardless of the on-screen one.
Users can now generate, view, and revoke the access tokens an external AI
client needs to reach the MCP server, and copy the server endpoint URL. The
new token is shown once on creation.
@sourceant

sourceant Bot commented Jul 11, 2026

Copy link
Copy Markdown

Code Review Summary

A comprehensive update promoting v2.0.0-beta.1 (Ailanthus). This release focuses on AI integration, real-time streaming, and extension slots for plugin-based UI architecture.

🚀 Key Improvements

  • Introduction of ExtensionSlot for modular, plugin-described UI rendering in components/extensions/.
  • Live streaming for AI chats using Laravel Reverb with a robust polling fallback in composables/useAiChats.ts.
  • Progressive dashboard stats loading which prevents heavy data blocks from delaying the initial overview paint.
  • A new 'Financial Position' view with intent-based transaction tagging (regular vs investment vs loan).

💡 Minor Suggestions

  • Adjust popover width in ChatComposer.vue for mobile viewports.
  • Optimize search debounce in the spreadsheet view for empty inputs.
  • Centralize brand color variables for intent-based surfaces.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment thread composables/useAiChats.ts
// Real-time transport. When Reverb is configured, chat turns push their
// progress and completion over a private channel; polling stays on as the
// reconnect fallback.
let nuxtApp: ReturnType<typeof useNuxtApp> | null = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap the useNuxtApp call in process.client or a more specific check. While the try/catch handles the error, useNuxtApp can only be reliably called inside a Nuxt context. If this composable is used during SSR, $echo might be undefined anyway.

Suggested change
let nuxtApp: ReturnType<typeof useNuxtApp> | null = null;
let nuxtApp: ReturnType<typeof useNuxtApp> | null = null;
if (import.meta.client) {
try {
nuxtApp = useNuxtApp();
} catch (e) {
// Fallback handled via null check
}
}

Comment on lines +224 to +241
const first = await fetchPage(1);
const lastPage = Math.max(1, first.last_page || 1);
const targetPages = Math.min(lastPage, maxPages);

// Index pages by (page - 1) so the final concat preserves server order
// regardless of which request resolves first.
const pages: any[][] = new Array(targetPages);
pages[0] = first.data || [];

if (targetPages > 1) {
const queue: number[] = [];
for (let p = 2; p <= targetPages; p++) queue.push(p);

let cursor = 0;
const worker = async () => {
while (cursor < queue.length) {
const p = queue[cursor++];
const resp = await fetchPage(p);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concurrency limit of 6 is reasonable, but since this runs in a Promise.all inside a loop, ensure that if one page fails, the entire process doesn't halt unless intended. You might want to wrap fetchPage(p) in an individual try/catch within the worker.

Suggested change
const first = await fetchPage(1);
const lastPage = Math.max(1, first.last_page || 1);
const targetPages = Math.min(lastPage, maxPages);
// Index pages by (page - 1) so the final concat preserves server order
// regardless of which request resolves first.
const pages: any[][] = new Array(targetPages);
pages[0] = first.data || [];
if (targetPages > 1) {
const queue: number[] = [];
for (let p = 2; p <= targetPages; p++) queue.push(p);
let cursor = 0;
const worker = async () => {
while (cursor < queue.length) {
const p = queue[cursor++];
const resp = await fetchPage(p);
try {
const resp = await fetchPage(p);
pages[p - 1] = resp.data || [];
} catch (e) {
console.error(`Failed to fetch page ${p}`, e);
pages[p - 1] = [];
}

return /["\n,]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};

const exportCsv = () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generating a CSV export by concatenating strings in memory for potentially 6,000+ rows can be slow and cause UI jank. For larger datasets, consider using a Web Worker to build the Blob or a library that handles streaming.

Suggested change
const exportCsv = () => {
const exportCsv = () => {
// Consider moving this logic to a Worker if filteredRows.length > 2000
const headers = [ ... ];

@nfebe nfebe changed the title chore(release): Cut the v1.2.0-beta Aspen beta chore(release): Cut the v1.2.0-beta Ailanthus beta Jul 11, 2026
@nfebe nfebe changed the title chore(release): Cut the v1.2.0-beta Ailanthus beta chore: Release 2.0.0-beta.1 Ailanthus Jul 14, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploying trakli-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 69ccd0f
Status: ✅  Deploy successful!
Preview URL: https://847d465e.trakli-dev.pages.dev

View logs

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.


// Anchor the popover just above the + button (fixed-positioned so the composer's
// rounded overflow can't clip it).
const openSheet = () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The popover anchoring logic assumes window is available but doesn't handle viewport boundary collisions gracefully if the width exceeds 300px on very small screens. Also, using fixed positioning without a resize listener inside this function specifically might lead to the popover detaching if the keyboard appears/disappears on mobile.

Suggested change
const openSheet = () => {
const openSheet = () => {
const r = attachBtnRef.value?.getBoundingClientRect();
if (r && import.meta.client) {
const width = Math.min(300, window.innerWidth - 16);
const left = Math.max(8, Math.min(r.left, window.innerWidth - width - 8));
popoverStyle.value = {
position: 'fixed',
left: `${left}px`,
bottom: `${window.innerHeight - r.top + 8}px`,
width: `${width}px`,
maxWidth: 'calc(100vw - 16px)'
};
}
sheetOpen.value = true;
};

return Object.keys(first).find((k) => typeof first[k] === 'string') ?? null;
});

const numericKeys = computed<string[]>(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excluding keys ending in '_id' is good, but strictly excluding 'percentage' might be too restrictive if the AI intentionally provides a dataset where the percentage is the primary metric to be charted (e.g., probability distribution).

Suggested change
const numericKeys = computed<string[]>(() => {
const numericKeys = computed<string[]>(() => {
const first = rows.value[0];
if (!first) return [];
const numeric = Object.keys(first).filter(
(k) => typeof first[k] === 'number' && !/(^|_)id$/i.test(k)
);
const values = numeric.filter((k) => !/(^|_)count$/i.test(k) && k !== 'percentage');

Comment on lines +137 to +138
let searchTimer: ReturnType<typeof setTimeout> | null = null;
watch(searchInput, (value) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search watch fires on every keystroke with a 200ms debounce. If the user clears the input, we can skip the timer and reset immediately for better perceived performance.

Suggested change
let searchTimer: ReturnType<typeof setTimeout> | null = null;
watch(searchInput, (value) => {
watch(searchInput, (value) => {
if (searchTimer) clearTimeout(searchTimer);
if (!value) {
search.value = '';
return;
}
searchTimer = setTimeout(() => {
search.value = value;
}, 200);
});

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying webui with  Cloudflare Pages  Cloudflare Pages

Latest commit: 69ccd0f
Status: ✅  Deploy successful!
Preview URL: https://f791951b.webui-9fh.pages.dev
Branch Preview URL: https://dev.webui-9fh.pages.dev

View logs

@nfebe
nfebe merged commit a81f148 into main Jul 14, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant