NovuHub changelog
Dates are the day the wave was pushed to main. Each entry names the environment switches it introduced; the deploy notes under docs/deploy-notes/ hold the step-by-step instructions.
2026-10-14 — v1.55.2: an AI answer in the chat is no longer shown as an error
Fixed
- In the My Assistant chat, every answer the AI gave was shown as “I could not answer that: the NovuHub server answered with an error (HTTP 200)”. The server had answered and saved the answer — it appeared after reopening the chat — but the chat route did not mark it as a good answer the way
/api/assistant/ask does, so the browser treated it as a failure; “Try again” then asked the AI again and used another AI answer of the day. Since v1.54.0 (the chat); answers from the rules (no AI) were not affected (backend/app/routes/assistant_chat._ai_out, tests/test_assistant_chat.py::test_a_direct_ai_answer_in_the_chat_is_marked_ok).
2026-10-14 — v1.55.1: My Assistant answers a P&L line you name
Fixed
- “What is the Gross Product Sales for October?” was answered with the Sales & Marketing team card (the word “sales”) — or, in other companies, with the items (“product”) — and “Gross Service & Consulting Revenue in July” with total revenue. A line of the company's own P&L named in full now wins: its amount for the month (or quarter, year …), its share of revenue, and the link to Profit & Loss; a month that is not imported says so; someone without Finance is told it needs Finance, by name, without a figure. Lines called like a standard figure (“Cost of sales”) and one-word lines are answered as before (
app/assistant_pnl.line_named, tests/test_assistant_pnl_lines.py). - Measured: the 316 questions of the earlier question banks give exactly the same answers as v1.55.0 on the full test company.
Housekeeping
.gitignore: the server's own .env.bak* / .env.before-* copies and its older backups/ folder no longer show up in git status.docs/deploy-notes/dependabot-v1.55.yml: Dependabot reads the Python dependencies from backend/ (paste over .github/dependabot.yml).
2026-10-14 — v1.55.0: back end and front end are two separate projects
Nothing changes for the people using NovuHub: the same screens, the same data, the same addresses. The code is now arranged the way the MVP blueprint shows it — the back end builds the API, the front end reads it.
The layout
backend/ — the Flask API (the former app/, migrations/, tests/ and the entry points). backend/app/paths.py is the one place every folder comes from; the server's own files stay in instance/ at the root, where they always were.frontend/ — everything the browser runs: src/ (the screen modules, formerly app/static/src), static/ (served at /static/), index.html (the page), build/ (the bundler, formerly tools/build), tests/ (formerly tests/frontend), package.json.contracts/openapi.json — the agreement between the two: all 331 API addresses (374 operations), generated from the routes by backend/scripts/export_openapi.py. backend/tests/test_api_contract.py fails when routes and contract drift apart; frontend/tests/api-contract.test.js fails when a screen calls an address that does not exist (today: all 325 addresses the screens use are in it).- The root keeps thin hand-over files (
wsgi.py, worker.py, lab_worker.py, scan_mailboxes.py, gunicorn.conf.py, requirements.txt, build_frontend.sh), so the server's services and the usual deploy command work unchanged.
The page is static
frontend/index.html is the same file for everyone, with no data and no inline script in it. Who is signed in and their company come from the new GET /api/session (exactly the values the server used to write into the page); frontend/static/boot/early.js asks for it at the very start, boot/start.js starts the platform when the answer is in. Offline, the last answer kept in the browser opens the platform as before.- The build writes the finished page (
frontend/static/index.html) and build.json (which bundle, which version); the service worker and the Test Lab use the same values. Without a build the server works the values out as before. - nginx serves
/static/ from frontend/static (deploy/nginx-novuhub.conf); without nginx, Flask serves it as before.
Measured — the minified bundles the browser downloads are byte-for-byte the same as v1.54.2 (37 of 37); all 1,376 earlier tests pass plus 6 new; the front-end suite 544 of 544 plus 8 new; every sidebar screen (45) shows the same content on the same data; a full server rehearsal (nginx, the real start commands, upgrade, rollback, upgrade again, trial copy) kept every upload and screen.
Deploy (once): cd /var/www/novuhub && git pull && bash deploy/upgrade-v1.55.0.sh — see docs/deploy-notes/DEPLOY-v1.55.0-backend-frontend.md (backup first, optional trial, rollback). CI: paste docs/deploy-notes/ci-v1.55.0.yml into .github/workflows/ci.yml (the steps now run in backend/ and frontend/). No new environment switches, no database migration.
2026-10-14 — v1.54.2: a failing AI call says why — and the API key can be tested
A new Anthropic key was saved but My Assistant still answered "the AI service could not be reached". The key was stored; the real cause was hidden: the chat showed that sentence for any failure, including an error page from NovuHub's own server.
Fixed / added
- The chat names the real reason: a server error with its HTTP status, a rejected key, no credit on the Anthropic account, an unavailable model, rate limit, DNS / TLS / timeout. Anthropic's own message is carried along (
AIError.detail). - If the AI step fails or breaks unexpectedly, the chat gives the rules' answer from your data with a visible note ("The AI did not answer: …") instead of an error. The cause goes to the server log (
assistant chat: AI …). - Settings → Integrations → AI: a Test key button (and an automatic test after Save) makes one tiny live call and reports "The key works" or exactly why not.
POST /api/finance/config/test, admins only. - Saving removes spaces and line breaks a copy from the web brings along; a key with characters an API key never has (…, quotes, non-ASCII) is refused with a reason instead of failing every later call.
- No new environment switches, no migration.
Fixed
- Opening the My Assistant chat panel widened the sidebar from the 88 px icon rail to the full 230 px. The panel's layout rule now keeps whatever width the sidebar has (full, icon rail, or hidden). Measured in Chromium: 88 px before and after opening.
2026-10-14 — v1.54.0: My Assistant as a chat — a ✦ button in the top bar, a panel on the right, history, access-aware
The button and the panel (app/static/src/98s-assistant-chat-button.js, 79d-assistant-chat.js)
- A ✦ My Assistant button right of the ⏱ working-time button, on every page; ⌘. / Ctrl+. opens and closes it.
- The panel is a column on the right of the platform, top to bottom; the page stays usable beside it and the panel stays open while you move between sections. Drag its left edge to resize (kept per browser). Esc or ✕ closes it. On a phone it fills the screen.
- It greets the person by first name and the local time of day, and suggests questions that fit what they may open.
- Messages, a typing indicator, the tab and period each answer was read from, links to the screens, a copy button, English and German.
A conversation (app/assistant_chat.py, app/routes/assistant_chat.py)
- Follow-ups: "And April?", "April 2026", "and in Q2", "by warehouse", "last year" are completed from the question before them; the answer shows "Understood as: …".
- Clarifying questions with options when a question could mean several things: the period of a P&L or revenue figure; which kind of overdue (one or more); staff turnover, revenue or stock turns; bank balance, balance sheet or leave balance; which of several similar records. A picked option is answered at once; several picked options are answered in one message.
- With AI, the model receives the chat so far, so it reads follow-ups itself; the report-tab pause (v1.53.0) works inside a chat.
- The rules' "answered without AI" line is a badge in the chat, not part of the answer.
History (assistant_chat, assistant_chat_message tables — migration d4f6a8c0e2b4)
- Every chat is saved per person: list by day, search, reopen and continue, rename, delete. At most 200 chats per person are kept.
- A chat belongs to the person who had it: every route checks the signed-in account and the workspace (another member gets 404).
Access — checked on every question
- A question about a section the person cannot open is refused kindly, by name, with who can look it up, and with no figure from that section: "Lena, I'm sorry — this needs HR reports, and you don't have access to that section in NovuHub, so I can't answer it. … can look it up for you, or an administrator can give you access."
- New in the engine: a report figure that only a closed section's tabs show (average tenure, sick-absence rate without HR; win rate without Sales) is refused instead of answered from the records everyone may see. What to reorder, what ran out, dead or expiring stock need Inventory — a person with Finance only still gets the stock value.
- A reopened old chat follows today's access: the engine is built from the current membership on every question.
Measured (Chromium, full test company, rules mode) — docs/assessments/NovuHub_v1.54.0_Chat_Panel.pdf
- 22 of 22 end-to-end checks pass: button position, panel geometry, greeting, EBITDA and follow-up, choices, report tab, navigation, links, history, German, reopen, keyboard, phone, and a Finance-only employee (answered for Finance, refused for HR and Inventory, cannot open the owner's chats).
- Earlier question banks unchanged: 115/115, 81/82, 47/48 (v1.52) and 45/57, 24/30, 23/30, 24/30 (v1.53 report banks).
- New tests:
tests/test_assistant_chat.py (10).
No new environment switches. The two tables are created on start (create_all) or by the migration.
2026-10-14 — v1.53.1: My Assistant reports the same lots as the item screen
Fixed
- My Assistant could name the wrong lots for an item. Example: "Silicone sealant, clear, 310 ml" (CH-2100).
- The Assistant said: 86 pcs of L2100-1230 and 480 pcs of L2100-0420.
- The item screen shows: 404 pcs of L2100-0420 and 162 pcs of the short-dated L2100-0525-SD, which expires on 30 Nov 2026.
- The total on hand (566 pcs) was right; only the split between lots was wrong.
- The cause: a sale booked without a lot takes the lot that expires first among the lots in stock at that moment. The Inventory screens walk the movements in booking order. The Assistant walked them in the order they are stored, so a sale could be taken from a lot that was received later.
- Now the Assistant walks the movements in booking order, like the Inventory screens. An issue from a lot that was never received no longer changes any lot, also like the screens.
- This also corrects the lots used for "lots expiring soon" and the expiry figures in Inventory answers.
Measured
- On the full test company, the Assistant's lots and on-hand quantities equal the item screen for every item. The same holds with the movements shuffled and 15 extra sales without a lot.
- New tests:
tests/test_assistant_lots.py (2). The first test fails on v1.53.0 and passes now.
No new environment switches.
2026-10-14 — v1.53.0: My Assistant reads every report tab — "How much was the EBITDA for March?"
Until now My Assistant answered from records. A question about a figure that only a report tab shows (EBITDA, win rate, cost per lead, stock turns, sick-absence rate …) got a wrong or empty answer: "Revenue in March" came back as €0. Now the Assistant reads the 77 report tabs of every section, with or without AI, and still never reads a tab the person may not open.
The P&L on the server (app/assistant_pnl.py)
- Revenue, cost of sales, gross profit, operating costs, EBITDA, D&A, EBIT, EBT, tax, net profit, total costs and the margins, for any month, quarter ("Q3", "drittes Quartal"), year or range.
- Same rule as the Profit & Loss screen (
assistant_facts.role_totals, parity-tested against the screen's pnlSub). - Each answer shows how the figure is built, the same length of time before, and the budget when there is one. A cost line can be asked by name ("Personnel in August").
- "Revenue in <period>" now uses the P&L when it covers the whole period, and shows what was invoiced in NovuHub below it.
- People without access to money get no P&L figures.
Every report tab (app/assistant_report_tabs.py, app/static/src/79c-assistant-reports.js)
- One catalogue of 77 tabs:
- Finance: 13 tabs. Sales reports: 11. Marketing reports: 13. Inventory reports: 14, plus the item list.
- HR: 8 tabs, plus the 8 HR reports (headcount bridge, turnover, absence, personnel cost, working time, HGB, pay gap, severely disabled employees).
- Projects (home, portfolios, workload, radar), the Dashboard, Learning, Tickets, and the Sustainability double materiality and ESRS tabs.
- The server names the tabs a question needs. The browser draws them out of sight with the tab's own code and filters set to the period asked, then sends back only the tiles and tables as text. The screen does not change; side effects (saving, notifications, snapshots) are switched off while a tab is drawn.
- At most six tabs per question; text is capped at 12,000 characters per tab.
- Only tabs the person may open are asked for and accepted. The pay gap and severely-disabled reports need HR access.
- Without AI the figure is picked from the tab: by the label, the month column, the tab's own figure names ("contractors", "write-offs", "net position") and initials ("CPL", "NRR"). The answer names the tab and the period, with a link.
- With AI the model gets two new look-ups:
pnl_statement, and read_report (offered only when a browser is asking, and only the person's own tabs). The conversation pauses while the browser reads, and is kept signed (15 minutes, bound to the person and workspace). A stolen or changed state is refused. Figures read from a tab count as checked figures.
Fixes found on the way
- A tile with its figure inside the label ("Net position €60,077.27 owed to you") is read correctly.
- Dates ("as of 14 Oct") and formulas are no longer taken as figures.
- Words that are only part of a record's name ("Website Relaunch") no longer send the question to a report tab.
Measured (rules mode, in the browser, on the full test company; evidence in docs/assessments/NovuHub_v1.53.0_Report_Reading_Evidence.zip)
- 147 report questions in four banks: v1.52.0 answered 11 correctly, v1.53.0 answers 116.
- Bank 4 (30 questions, written before any fix was measured and never tuned): 24 of 30. Bank 3, first run before its fixes: 13 of 30.
- All 77 tabs read. Each tab takes a median of 3.7 ms (slowest 84.7 ms); six tabs together take 69.7 ms. Answers take a median of 0.34 s (slowest 0.74 s).
- The v1.52.0 question banks are unchanged: 115/115, 81/82, 47/48.
- The AI path is tested with a simulated model only (unit and HTTP tests). It is not yet tested with the live Anthropic API.
No new environment switches.
2026-10-14 — v1.52.0: My Assistant answers questions about every section — with or without AI
Until now My Assistant could look up ten kinds of record. A question about an Inventory item, a campaign, a team, a learning path, a purchase order or a document was answered with "not in the data", even when the screen next to it showed the answer. Now every business section is covered, in English and German, and the Assistant still never reads beyond what the person may open.
One catalogue of what the Assistant may read (app/assistant_records.py)
- 36 kinds of record across Projects & Tasks, Sales & CRM, Marketing, Inventory, HR, Finance, Learning & Docs, Customer Service and Communication. Examples: items, stock per warehouse, warehouses, purchase orders, lots, supplier prices, suppliers, campaigns, leads, marketing costs, people, teams, leave, goals, time entries, courses, learning paths, certificates, SOPs, workflows, portfolios and messages waiting for a reply.
- Each kind of record names:
- the sections that open it;
- its safe fields only — no pay, tax or insurance numbers, no reason for another person's leave, and never the text of a message;
- its date and money fields;
- named filters (open, overdue, late, below the reorder point, out of stock, MQL …).
- One query engine filters, searches, counts, totals and groups the records. The AI's look-ups and the answers without AI both use it, so both give the same figure.
With AI: four new look-ups (app/assistant_tools.py)
find_records covers every kind of record. It filters by named state, by field and by date range, and returns up to 25 records with totals.summarize_records counts and totals by field or by month, for example stock value by warehouse, deals by stage or leads by source.section_overview returns a section's key figures and what needs attention there.lookup_record returns one record in full: an item's stock per warehouse, open orders, lots and supplier prices; a colleague's open work, hours and absences (within the person's people scope); a campaign's leads and costs; a supplier, warehouse, project, course or portfolio.- Each person is offered only the kinds of record they may open, with their fields described.
- The prompt lists the sections the person can open and tells the model to look things up before saying something is "not in the data". The platform map now includes Marketing and Inventory.
Without AI: the rules answer every section (app/assistant_answers_records.py, app/assistant_sections.py)
- The rules now read what the question asks:
- which records;
- count, total, average, list, top N, or per <field> ("per warehouse", "je Lager", "who owns the most deals");
- named states;
- values the question names (a warehouse, supplier, team, customer, person, channel);
- the period.
- A record named by its number or name gets its card: "How many units of GL-NIT-L do we have?", "When does PO-4181 arrive?", "Who leads the Delivery team?".
- "How is marketing doing?" and "Wie läuft das Lager?" get the section at a glance.
- The existing topic answers keep their questions: margin, overdue and who to call, cash, reorder, hours, leave and pipeline.
- A section the person cannot open is named, along with who can help, and none of its figures are shown.
Fixes found on the way
- The cash answer listed the items of its tightest week as raw data. They now read as a label, an amount and a date.
- "What did we sell most" now reaches the best-seller answer.
Measured (rules mode, a 28-person test company with the Inventory demo data):
- 82 unseen questions over 9 sections: 68 answered right on the first run, 81 after the fixes.
- A second set of 48 unseen questions, written after the fixes: 38 right on the first run, 47 after.
- The member test leaked no figure: 0 of 115 answers to someone without access contained one.
- 245 questions were run on messy, empty and full data. None broke and none came back empty. The slowest answer took 0.09 s.
The full report is in docs/assessments/NovuHub_v1.52.0_My_Assistant_All_Sections.pdf.
New: app/assistant_records.py, app/assistant_sections.py, app/assistant_answers_records.py, tests/test_assistant_all_sections.py. No new environment switches; normal deploy chain.
The 13 Oct 2026 test audit (v1.50.9) found real bugs and thin spots in the tests. This release fixes the ones in code that ships; the four sections removed before launch (Decision Intelligence, Business Plan, StartUp, Test) were left out, and the screenshot baseline waits until they are gone.
Bad input is a 400, never a server error
/api/assistant/undo answered 500 when the id was not a whole number. Numbers in JSON bodies are now read through app/inputs.as_int (undo, snooze days, connector sync hours, campaign connector id, Business Plan revisions).- A JSON array sent where an object is expected made 67 write routes answer 500. One guard (
install_json_object_guard) now answers 400 "Send a JSON object." for every write route; the bulk push APIs and the e-mail webhook that legitimately take arrays are marked @accepts_json_list. - Fields of the wrong type crashed 9 routes; each now checks its input: invites (role / e-mail), finance AI key, finance mailbox settings, push subscription, sales-radar settings, 2FA disable code, My Assistant library draft language.
- E-mail webhook: SendGrid bounces were never suppressed. SendGrid posts its events as an array; the handler read
body.get(...) first and answered 500 to every SendGrid call. Bounced and complained addresses are now suppressed.
A fresh start with several workers
- gunicorn starts its workers together; on an empty database each one migrated and created the tables at the same moment and the losers crashed ("table … already exists") — 7 of 10 fresh three-worker starts failed. The schema step now runs under a lock (PostgreSQL advisory lock; a file lock for SQLite). Measured: 10 of 10 fresh starts succeed.
The load test runs again
tests/load/locustfile.py posted the sign-up form without its CSRF token and expected a workspace per sign-up: every sign-up got 403 and nothing else ran. It now creates the owner (or signs in with LOAD_OWNER_EMAIL / LOAD_OWNER_PASSWORD), invites and approves one member per simulated user, and each user signs in with the token.
Tests that notice a wrong figure
tests/test_v151_mutation_kills.py: exact-value tests for bank CSV import, GoBD locking and the invoice ledger, leave balances, passwords and payment recognition. Planted bugs caught in those modules: 112 of 185 → 174 of 185; the 11 left are changes no test can tell apart (equivalent) or design limits, each explained in the before/after report.tests/test_v151_connectors.py: every connector against a faked service — HTTP layer, OAuth sign-in and refresh, HubSpot, Pipedrive, Brevo, Mailchimp, lexoffice, sevDesk, Shopify, Matomo, GA4, Search Console, Google Ads, Meta, LinkedIn, Xentral stock. Connector line coverage 68 % → 91 %.tests/test_v151_endpoints.py: the 35 endpoints no test reached (team, invites, roles, 2FA, compliance, backups, finance key and mailbox, GoBD ledger, agent API, SCIM discovery, public pages …). Every endpoint of the launch scope is now called by a test (332 of 332).tests/test_v151_robustness.py: every write route receives broken and wrong-typed input on every test run; tests/test_v151_boot.py: six processes start on one empty database.
Accessibility (axe-core, WCAG 2.1 A/AA): 54 problems on 10 screens → 0
- A clickable card that holds its own buttons (calendar days, directory cards, bills, folders, radar and learning rows) is now a named group instead of one big button, still opened with Enter / Space (
enhanceA11y, 98-wiring-modals.js). - Labels for the period date fields (Finance, VAT, cash flow, HR), the radar's "days ahead" field and the HR table's editable cells; sideways-scrolling tables are keyboard-reachable regions; the org chart's "Lead" badge meets 4.5:1 contrast.
New: app/inputs.py, five test files above, tests/frontend/a11y-v151.test.js. No new environment switches; normal deploy chain.
2026-10-13 — v1.50.9: StartUp — match score, industry layer, approach, Ready to send, your own text, and Send
The private founder section's Customers, Funding and Angels tabs (150 companies, 25 funding bodies, 15 investors) now help decide whom to contact first and how, and send the e-mails in one go.
In all three tabs
- Match score (0–100) on every entry — how likely they answer, from their needs × what NovuHub does for them, size, how reachable they are, signals in their own words and location (customers); programme fit, timing and reachability (funding, angels). Open an entry to see each part with its reason. Levels High / Medium / Low; sort by score (the new default).
- Category layer: customers by industry (11 groups from the 44 raw industries — wholesale & import/export, food & consumer goods, machinery, chemicals, aviation, logistics, maritime, construction, engineering, business & IT services, marketing & print), funding by programme type, angels by investor type. Filter chips with counts and a By industry / By category view with the average score per group. A new import with an unknown industry still finds its group.
- Recommended approach (phone call, LinkedIn, e-mail, web form, warm intro) with the reason and a second step, changeable per entry. For companies it is usually a call or LinkedIn first: in Germany a cold advertising e-mail to a business needs its prior consent (§ 7 UWG); funding bodies and investors are written to by e-mail.
- Ready to send on every row, a Ready filter, and “Mark the N shown as Ready” / “Clear Ready”. An entry without a public address gets a To field.
- Your e-mail text: write a subject and text once with fields —
{{greeting}} (their own salutation, e.g. “Dear Mr Sugita and Mr Grömer,”), {{company}}, {{contact}}, {{industry}}, {{category}}, {{city}}, {{employees}}, {{since}}, {{reason}} and your signature — preview it on the first entry, and Implement it for all or the shown entries: every e-mail changes to your text with its own information. Each entry can still be edited by hand or set back to the original. Your text is kept per tab and language.
New tab: Send
- Every Ready entry from the three tabs, with its recipient, subject and text (preview), a tick to leave one out, and Remove.
- Your mailbox: Gmail (app password), Google Workspace, IONOS, Strato, Microsoft 365 or any SMTP server; test login and “send me a test”. The password is stored encrypted (
secretbox), per person, and never returned. - CC (up to three, e.g. the co-founder), optional BCC to yourself, the language for entries without your own text.
- Send N e-mails → Approval 1 (count, sender, CC, pace, the first recipients, the § 7 UWG reminder for companies, a tick “I checked …”) → Approval 2 (final yes) → sending starts. The server accepts it only with the word SEND and the exact count the person saw.
- One e-mail every 30–60 seconds (spam filters punish bursts from a fresh mailbox), each from your address to one recipient; live progress, Stop, and the page may be closed. A sent entry moves to “E-mail sent” and leaves the Ready list; a failed address does not stop the rest; a refused login ends the run at once.
- Built to survive restarts: the run is a job in
AppState (startup:send:job), sent by whoever holds a short lease; every write is a compare-and-swap, so two processes never send the same message, and a message caught “sending” in a dying process is marked “unclear” instead of sent twice. The status poll, the next request and worker.py resume a run whose process was recycled.
New: app/startup_outreach_match.py, app/startup_sender.py; endpoints /api/startup/outreach/{ready,template,preview,apply}, /api/startup/mailbox[/test], /api/startup/send[/stop]. No new environment switches; outgoing SMTP (port 465 or 587) must be open on the server.
2026-10-13 — v1.50.8: the facility-services and packaging companies' findings, fixed — and why the Lab's clicks were slow
The 24/7 Lab's three newest companies (Alster and Dithmarscher Gebäudeservice, Hanse Verpackungen, all on v1.50.7) had every report figure right but found nine open points: four that earlier releases had fixed and that came back, and five new ones. Each was replayed in a local Lab run of the same companies (same seeds, CPU slowed 3×) before and after the fix; the people who found them now finish their day without a finding.
My Assistant can be asked at once
- The question box is there from the first moment — also while “Analysing your data…” is still on the screen, and when the analysis fails. The Lab asked while the analysis was loading, found no box and wrote “no answer within 45 seconds” (4× in three companies) and “the Assistant's own findings do not touch my question” (the serious finding at Alster). Questions are answered by their own request, so nobody waits for the analysis to ask.
- The analysis is three times faster (2.1 s → 0.66 s on a 135-person company): leave and logged hours are indexed once per analysis instead of read again for every person and day (
assistant_facts._leave_index, assistant_scn_sources._hours_week), the people's job descriptions are folded once for the owner search (assistant_owners._profiles), and the scoped copy of the document is a JSON round trip instead of copy.deepcopy (assistant_scope._copy). Tests prove the same answers as before. - What is typed in the question box is kept, with the cursor, when the page is drawn again meanwhile (the analysis arriving while a question was typed emptied the box — and the “Ask” went nowhere).
- “Ask” redraws only the question box and the answers, not the whole My Assistant page (0.3–1.1 s → 0.02–0.1 s per question on a slow PC).
- A card's buttons (👍 👎, Snooze, Done, Dismiss…, actions) find their card by its id; a card that changed meanwhile says so instead of nothing happening (the Lab: “Useful” did nothing).
- Weekly review (PDF): the button shows “⏳ Preparing the review…”; a second click while it is prepared says it downloads in a moment.
- “↻ Re-analyse now” with nothing changed leaves the page as it is (only the button comes back). It drew the whole page again, and a click made in that moment landed on a button that no longer existed (the Lab's “Weekly review” click after a re-analysis).
Every control answers at once
- Settings › My Assistant — “At”, “Use company defaults” (came back after v1.50.6): the choice shows on the screen before the server answers, with “Saving …” beside the heading, then “✓ Saved” and the time — or the old value back with the reason. The server sends the company defaults with the settings, so a reset shows at once.
- Settings › Background worker — “Refresh” says “Checking …” and then the result.
- Read-only link fields (the NPS survey link — came back after v1.48.2, 6 companies — the booking, calendar, portal and form links, 14 fields): a click copies the link and says so; a key or a long text is selected with a hint how to copy it. The NPS survey has a Preview ↗ that opens it as the customer sees it.
- Finance: “→ Payable” on a receivable that a colleague moved meanwhile says so; a bill row opens also when some text elsewhere on the page is selected (only a selection inside the row keeps it closed).
- A part of NovuHub loaded on first use (e-invoice, bills, inventory …): a click that waits for it says “One moment — opening this the first time …” after a quarter second (the Lab: “E-invoice (XRechnung XML)” did nothing).
- Settings › My Assistant failed with a server error when a P&L line or project was named with a number or a tick box (messy imports) — found by the local Lab run.
The management questions have their screens
- Projects › Workload › Hours vs plan (shown first): “Which projects and objects used more hours than planned?” — planned hours of each project's tasks against the approved time worked, pending time apart, how much of the plan is used, tasks done, tasks over their own hours, and “hours ahead of the work”. Same rule as My Assistant's answer. Excel and PDF.
- HR › Reports › Working hours opens with Who has the most overtime (worked − target in the period) and Teams with the highest absence (absence rate per team; the sick rate per team from three people up, in full to HR and administrators).
- Local Lab check: Alster's goal (3 questions) and Dithmarscher's (2 questions) are now answered on the screens of the sections that own the data.
Why the Lab's clicks were 2.5–3.9 s (P&L “Move … up/down”, “Save month”, “Continue →”, “Add Payable”)
- The welcome tour came back after every reload until “Skip” was pressed, and its full-screen layer took every click; the Lab's click then waited 1.5 s for the button and pressed it by script. The tour is now shown once, and a click on the dimmed page outside it closes it. The same clicks take 0.3–0.7 s in a Lab-like run (CPU 6× slower).
- GET /api/data keeps its finished answer per person and version: a reload, a second tab or the page after sign-in gets it without reading, redacting and encoding the whole document again. A new version, another person or another role never gets a kept answer (
routes/data.py, 48 MB per process at most).
A page left open across a deploy
- Every build has an id. A section loaded into a page of an older build never registers its button actions (they are numbered per build and would have landed on the old page's buttons — doing nothing, or the wrong thing); the page shows “NovuHub was updated while this page was open — Reload now” instead (
tools/build/bundle.py, csp_handlers.registration).
24/7 Lab
- A person waits up to 20 s for My Assistant's question box, as anyone would, and a finding says what the screen showed when none came; the goal question waits for the answer itself (up to 45 s), not a fixed 6 s. A question that is nowhere on the screen after 3 s is asked once more.
- The welcome tour's key is per person, so the Lab's old setting never matched: a person who is shown the tour now presses “Skip”.
- A link that opens a new tab is something a person sees — no longer “nothing happens”.
- A control whose screen was drawn again between choosing it and clicking it is clicked once more on the new screen, as a person would — not written down as “nothing happens”.
Tests: tests/test_v1508.py (8), tests/frontend/v1508-fixes.test.js (14); updated ck-fix-polish, audit-fix-ux, wh-fix-last. No migration, no new package, no new .env switch.
2026-10-12 — v1.50.7: Elbe Personal's 49 serious findings (the Lab's full /tmp), a quote loop, faster saves and reloads
Why Elbe Personal showed 49 serious findings. 48 were "⬇ Excel / ⬇ PDF hands out a file — 0 bytes" and one "The XRechnung was not handed out". All 49 had one cause: the test server's /tmp is a 1.9 GB RAM disk and it was 100 % full (leftovers of the Lab's browsers — Playwright folders and Chromium shared memory — from browsers killed by restarts and the memory limit). From the 11:23 restart on, the Lab's browser could not save any download. The platform's exports were fine (called directly on the live server: a full .xlsx and .pdf).
- The Lab's temporary files now live on disk (
<lab>/tmp, or NOVUHUB_LAB_TMP): lab_worker.py points TMPDIR / TMP / TEMP there before the browsers start, so the RAM disk is never used (app/lab/tmpspace.py). - Leftovers are removed: at start and in every housekeeping round, Playwright and Chromium leftovers of this user that are older than an hour; a download is deleted as soon as it has been measured.
- A full temp folder is the Lab's incident, never the platform's: a 0-byte download or a missing XRechnung while the Lab's temp folder has less than 64 MB free is written down as a Lab note, not as a "wrong figure".
- The 49 findings already recorded for Elbe Personal stay in its record — they describe the Lab's disk, not the platform.
A quote that looked changed on every save. A quote accepted through the customer's link is stored by the server without the defaults every browser fills in (status, payments, discount …); every later save looked like a change of the customer's decision and was put back — on the test server every few seconds, and since v1.50.6 with a message. Only the app's own defaults differing is now no change; a real change is still refused and reported (access_guard._qfill). A refusal the server repeats is now fetched at most once a minute and told once per visit.
Faster
- Saves after the frame:
save() turned the whole document into text inside the click (225 ms on a slow PC for a 2.8 MB company — the larger part of a P&L move, "Save month" and every edit). The click now returns, the screen is painted, and the text is made right after; whoever reads the stored copy first (the sync, a teammate's update, leaving the page) writes it at that moment. A task edit at 6× CPU throttling: click → painted 250 ms → 130 ms (median). - Reload / sign-in: the whole-document copy made before the first screen is only needed by sharded workspaces (delta sync) — no longer made for the others. Warm reload at 6× throttling: 967 ms → 840 ms (median of 5).
- Your own sections' code is fetched first after sign-in (your access list), including Finance's ⏰ Dunning, expense claims and e-invoice import (the "plus" chunk) — the Lab measured "Dunning" at 2.7 s, the chunk loading inside the click. Prefetch starts after 1.5 s (was 3 s), up to 7 chunks.
- The Lab's own pause is no longer counted: the explorer waited 450 ms after every click and counted it as the platform's time ("after clicking … took 2.7 s" was 2.25 s of platform time).
No migration, no new package. New optional switch: NOVUHUB_LAB_TMP (the Lab's temp folder; default instance/lab/tmp).
2026-10-12 — v1.50.6: the engineering assessment and the last four Lab industries, fixed
Two sources. (1) The engineering assessment of 12 October (Technical Brief pages 6–10): tenant isolation, capacity, backup restore, financial correctness, background-job recovery. (2) The 24/7 Lab's companies that had no fix release yet — Sachsenwald Consulting, Ostsee and Weser Ingenieure, Weser and Wandse Medizintechnik, Bille and Holstein Kreativ, Wandse Personal (v1.48.6 – v1.50.5).
From the assessment — figures that were wrong
- VAT tab: a receivable pushed from a sales invoice was taxed at 19 % of its gross instead of the invoice's own tax (7 %, 0 %) — output VAT too high in 678 of 1,000 random test documents. Now from the invoice (
82-journal.js). - Open receivables (Finance): a part payment recorded on the invoice did not reduce the mirrored receivable — too high in 248 of 500 documents with part payments (
66b-finance-links.js · finOpenClaims). - VAT return / My Assistant: the VAT of a credit-card statement was counted on top of the card charges (
assistant_de.py). - Vendors with "tax" inside a word (Taxi …, Syntax …) were read as the tax office: input VAT dropped, payment priority wrong (word-boundary rule in
assistant_facts.py / assistant_de.py). - Invoice totals on the server: the total the invoice was issued with wins; the recomputation used banker's rounding (1-cent differences).
- New permanent check:
tests/test_finance_parity.py + tests/finance_parity/ — 300 random company documents with a known answer in integer cents, compared with the browser's code (run in Node), the server's code and the journal it posts (4,000 documents and 112,007 journal entries in the assessment run: 0 differences).
From the assessment — security and recovery
- Invoice PDFs could be fetched by number by a member without Finance, Bills or Archive access — now 404 (
routes/finance.py; tests/test_authz_invoice_files.py). - Mail outbox: a worker killed mid-batch sent the whole batch again (up to 50 e-mails); now at most the one in flight (
emailer.py). - Invoice mailbox: e-mails were tagged "done" before their invoices were filed — a crash in between lost them for the next automatic run; now tagged after filing (
routes/finance_mailbox.py; tests/test_worker_recovery.py). ops/restore.sh carried on after an error, so a half-loaded restore could look finished; it now stops at the first error.
From the Lab — the server's corrections reach the screen
- When the server puts part of a save back — an issued invoice is locked (GoBD), approvals, payments and signatures only by the people allowed, a quote keeps the customer's decision — the save's answer now says so (
corrected), the browser fetches the stored copy at once and tells the person what was kept and what to do instead ("Invoice RE-… is issued and locked (GoBD) — the server kept the issued version. To correct it, issue a credit note (Storno) and a new invoice."). Before, the answer was a plain OK and the browser kept showing its own edit until a colleague saved (the Lab: an issued €9,720.96 invoice shown as €1 — in 5 companies). A default the app fills into an old invoice (type) is put back quietly, never reported as a refusal. (routes/data.py, gobd.py, wh-shell.js; tests/test_save_corrections.py, tests/frontend/save-corrections.test.js.)
From the Lab — My Assistant
- Ask never hangs: after 8 s the question says "Still working …", after 40 s it is given up with "↻ Try again", a network error is retried once by itself (the Lab: "no answer within 30 seconds", 6 companies).
- 👍 / 👎 on a card already rated (on another tab) answered nothing — now it says so and how to change it. Every Routine "Open →" also names where it went.
- Settings › My Assistant: a change made while the previous one was still saving was queued in silence and then drew no message (the Lab: "Use company defaults", "At", "Use 'Logistics'" — 8×); now "Saving …" at once and each stored change with its own message. A control used right after Save or Discard (before the page had its working copy) did nothing — the working copy is now made on first use.
- "Which clients are unprofitable?" — new Client profitability in Sales › Reports › Customers: per client net revenue − cost of goods − approved hours on the client's projects × the person's hourly rate − bills booked to those projects; unprofitable and thin-margin clients, the biggest cost and what to do about it; hours without a known rate are counted and named, not hidden. My Assistant answers the same question with the same rule (
36e-sales-profit.js, assistant_answers_fin.py · client_profit; tests/test_client_profit.py). - The notification bell no longer throws before the document is normalised (a page error at sign-in).
24/7 Lab
- Open / overdue receivables are now worked out as Finance counts them since v1.50.5 (receivables plus unpaid invoices not pushed to Receivables, each claim in cents) — the two "wrong figures" of Holstein Kreativ and Wandse Personal were the Lab's own outdated rule (
report_oracles.ar_claims). - My Assistant questions: a second person asking the same question waits for a new answer; when none comes, the finding says what the screen showed.
- The GoBD probe reads the invoice once the save has been answered, and checks that the person is told why.
No migration, no new package, no new .env switch.
2026-10-11 — v1.50.5: Finance — the tabs read from one another
An audit of the 13 Finance tabs found three separate worlds that nobody reconciled: the Receivables / Payables lists, the journal, and the imported P&L months. Each tab worked out "open receivables", "cash in bank" or the card liability its own way, and some things one tab wrote never reached another. New core file app/static/src/66b-finance-links.js (functions only) is the single place; app/bank_position.py is its server twin.
- One open-receivables figure —
finOpenClaims(): the open receivables plus the sales invoices that were not pushed to Receivables, never both (the rule My Assistant and the collections strip already used). Overview key figures and net line, Balance sheet, Consolidation and Burn now agree with Receivables and My Assistant; before, the Overview showed two different "owed to you" figures on one page. - One cash figure —
finBankBal(): the bank ledger when it is complete (an opening balance is set under Bank) and its lines are at least as new as the balance typed there; otherwise the typed balance. Overview, Balance sheet, Burn, Forecast, My Assistant's "Bank balance" KPI and the 13-week forecast all start from it (Overview preferred the typed balance, the Balance sheet the ledger). - Receivables reach the journal (Dr 1100 / Cr 4000, VAT to 2100; the receipt Dr 1000 / Cr 1100 when paid or part-paid) — on save, on "mark paid", on a move between the lists and in the Journal back-fill. A receivable that mirrors a sales invoice posts nothing (the invoice did). Marking a mirrored receivable paid now also marks its invoice.
- A bank match pays the document in the journal too: a bill's payment entry (bank, or the card account), a receivable's receipt; a receivable that mirrors a sales invoice gets a real payment record on the invoice (what Sales → Invoices and My Assistant read). The undo removes it. Before, a matched bill stayed open in the journal until someone re-saved it.
- Deleting a bill or receivable removes its journal lines (single and bulk delete, and the move between the lists — the lines move along). They were left behind.
- Card statement, continued: it is never posted as an expense (the charges were, Dr 6000 / Cr 2050 — paying the statement clears 2050); the Balance sheet shows "Credit card (to settle)" = open statements plus charges since the last statement, and leaves it out of Accounts payable; the VAT return leaves it out of purchases (the charges carry the VAT); the burn estimate does not count it as a second spend; the 13-week forecast no longer subtracts a card charge from the bank on the charge date (it leaves with the statement).
- VAT return now counts the sales invoices that were not pushed to Receivables, with their own tax, and deducts credit notes — the same base as the server's return (assistant_de).
- Tests:
tests/frontend/finance-links.test.js (6), tests/test_payment_recognition.py (bank position, card charge not twice), an end-to-end sweep of all 13 tabs in a browser with the sample invoices, a card statement and two bank matches (no script errors; Overview, Balance sheet, Consolidation, VAT and Burn show the same figures). - Unchanged by design: Profit & Loss, Budget and Forecast work from the imported monthly accounts (DATEV / LucaNet files), not from the bills — the Journal tab's "P&L from the journal" is the live view of the bookings.
2026-10-11 — v1.50.4: Finance — a credit-card statement is the issuer's bill, not a purchase paid by card
- Card statements are recognised (Kreditkartenabrechnung / Kartenabrechnung / Monatsabrechnung, "credit card statement", "new balance", "minimum payment" … together with a card word). Until now the rule "credit card → already paid" turned the issuer's monthly statement into a paid bill. A statement is filed open,
kind: card_statement, collected by direct debit on the named date (or a transfer when the statement asks for one), amount = the new balance; the rule wins over the AI's answer. Mailbox scan, Read-from-file and the forms all use it. - Lists show the chip "💳 Card statement · collected <date>"; the form explains it; paying the statement clears the card account 2050 against the bank (the charges were booked to 2050 when they happened), so the card account nets to zero after each statement.
- The 13-week cash forecast no longer guesses a statement outflow for charges that a filed statement already covers (one outflow, on the statement's collection date); charges after the statement date still get the guessed date.
- The document reader no longer mistakes "am 20.10.2026" for an amount of €20.10 (a number followed by a date part is never an amount) and reads "Neuer Saldo / New balance" as the total.
2026-10-11 — v1.50.3: Finance mailbox — the Gmail search never worked
- Gmail queries now reach Gmail. The scan asked Gmail with
X-GM-RAW newer_than:2y has:attachment (invoice OR rechnung …), but Python's imaplib sends arguments unquoted, so the query arrived as several search keys, Gmail answered BAD and the search quietly returned nothing — since the feature's first day only the plain SUBJECT fallback found anything (an e-mail whose subject did not contain an invoice word was never opened; v1.50.2's filename:pdf search failed the same way). Every X-GM-RAW query is now one quoted IMAP string. - The recent-mail-with-a-PDF check (SINCE + BODYSTRUCTURE, v1.50.2) now runs on every server, Gmail included — a second net under the Gmail search.
2026-10-11 — v1.50.2: Finance mailbox — every PDF is a candidate, re-scans spend their time on new mail
- An e-mail with a PDF attached is a candidate whatever its subject says. The scan only searched for invoice words (invoice, Rechnung, receipt …) in the subject or text, so an invoice forwarded with the subject "fyi" — or no subject at all — was never opened. Now Gmail is also asked for recent mail with a PDF (
filename:pdf, last 7 days for "Scan now", the cron's window otherwise); other IMAP servers are asked for recent mail and its BODYSTRUCTURE names the PDF parts — no bodies are downloaded for the check. The per-message check (invoice words or a PDF) still decides what is read. - A re-scan skips what is already filed — after the header, before the body and before the AI. "Scan now" is bounded (12 e-mails, 20 s), and before it spent that budget asking the AI about bills that were in Payables for weeks and then stopped before the new mail ("partial — run again" never progressed). The result now says "· n already filed".
- The AI's "not an invoice" is overruled by the document: when the attached PDF carries an invoice word, an invoice number and a total, the built-in reader files it.
- An e-mail that breaks the reader is counted, not lost: "· n e-mails could not be read (TypeError: …)" in the result and a warning with the traceback in the server log. The AI reader now also treats a read timeout or a dropped connection as an API error (falls back to the built-in reader) and turns
null / numbers / lists in the model's JSON into plain strings before anything calls .strip() on them. - Test:
tests/test_mailbox_payment.py sends PDFs with the subjects "fu", "tft", "kjzf" from a private address, one of them filed before, one that breaks the reader, with an AI that says "no" to everything — two are filed, one is skipped before the AI, one is counted as an error.
2026-10-11 — v1.50.1: payment recognition in the finance mailbox
- The nightly / "Scan now" mailbox scan (Settings → Finance mailbox) now applies the same payment rule as the forms: an attached XRechnung / ZUGFeRD e-invoice decides first (payment-means code, paid amount), then the text of the PDF and the e-mail (card, PayPal, cash → Paid on the charge date; direct debit → open, automatic; transfer → open), then the AI (its JSON now carries
payMethod, settled, paidOn). Filed bills carry payMethod, paidOn, autoDebit and payEvidence. - The attached document decides the direction: your company name in the letterhead → a receivable (party = the customer named after "Customer / Bill to"); your name as the addressee → a vendor bill. Before, a sentence like "our invoice to …" in the e-mail body could flip a receivable into a bill. Settings → Branding → Company name now counts as one of "our" names (next to the invoice settings and the brand).
- Due date and invoice number the extractor missed are filled from the document reader's rules.
- Samples:
tools/samples/make_payment_samples.py now writes 8 files — 4 payables (credit card, debit card, bank transfer, SEPA direct debit) and 3 receivables (open, paid via PayPal, overdue). tests/test_mailbox_payment.py sends all of them as e-mails from a private address and checks every one.
2026-10-11 — v1.50.0: Finance — payment recognition (card, direct debit, transfer)
A vendor invoice that was charged to a credit or debit card showed as "to pay in the next days" although the vendor already had its money. How an invoice is paid now decides whether it is still open — everywhere. No environment switches.
- The rule (
app/payment_recognition.py, 65b-bills-pay.js, the same on both sides): credit card, debit card / girocard, cash, PayPal / Stripe at purchase → the bill is Paid on the charge date, Accounts Payable owes nothing, no future outflow in the cash forecast (a credit-card charge appears once, as the card statement that settles it — 1st of the following month, settings.cardSettleDay to change). SEPA direct debit → open, but automatic: counted in the forecast on the due date, never "pay now", the "To pay in 30 days" and "Payables due in 7 days" tiles say "collected by direct debit". Bank transfer → the classic open item. Receivables follow the same rule the other way round (a customer who paid by card or PayPal at checkout owes nothing). - Read from file (
POST /api/plus/bill-read, button at the top of the bill and receivable forms): fills vendor / customer, number, dates, amount, currency and the payment method from an XRechnung or ZUGFeRD e-invoice (payment-means code BT-81, paid amount BT-113, amount due BT-115), from the text of a plain PDF (deterministic rules, English and German: "charged to your credit card", "bezahlt mit girocard", "wird per Lastschrift abgebucht", "amount due 0,00" …) or — for a photo or a scan without text — the workspace AI. The reader also tells a vendor invoice from your own invoice to a customer (company name in the letterhead) and offers to open it as a receivable, or the other way round. - E-invoice import reads the payment means too: a card-paid ZUGFeRD invoice is saved as Paid with the full amount; a partly prepaid one shows "x was paid in advance — y is still due". Gross, prepaid and due are shown separately.
- Approval rule: a card charge above the approval limit stays Paid (the money is gone) and the budget owner is asked to review it after the fact — a transfer above the limit still waits for approval before it may be paid (
app/money_guard.py). - Journal: a paid bill clears Accounts Payable — against the bank (1000) for cash, debit card and PayPal, against the new account 2050 "Credit card (to settle with the statement)" for a credit card; the bank entry follows with the statement.
- Samples:
tools/samples/make_payment_samples.py writes four test invoices (credit card ZUGFeRD PDF, debit card plain PDF, our own receivable as XRechnung XML + PDF, SEPA direct debit PDF). Tests: tests/test_payment_recognition.py, tests/frontend/bills-pay.test.js.
- The ⏱ working-time button in the top bar keeps its size but shows a crisp 20 px stopwatch (inline SVG in the button's colour) instead of the 13 px emoji glyph (
98r-wh-fix-time.js, .wh-clk-ico). No environment switches.
2026-10-11 — v1.49.3: ESRS Requirements → cross-cutting GDR layer (ESRS Set 2)
The four General Disclosure Requirements of the finalised ESRS Set 2 — GDR-P (policies), GDR-A (actions), GDR-M (metrics), GDR-T (targets); ESRS 1 para. 29, ESRS 2 paras. 38-52, Commission Delegated Regulation (EU) 2026/1563 — connected to the topical Disclosure Requirements. No environment switches.
- Master data (read-only).
app/esrs_gdr.json, built by tools/esrs_gdr_import.py from the GDR workbook: GDR_MASTER (exactly four active objects), GDR_AR_MASTER (14 ARs, kept once, never copied into DRs or DPs), TOPICAL_DR_GDR_MAPPING (59 rows; types DIRECT_REFERENCE, METRIC_FRAMEWORK, ENTITY_SPECIFIC_METRIC_FRAMEWORK, COMPLEMENTARY, DIRECT_OR_COMPLEMENTARY). The import refuses a workbook that fails the referential checks (missing Topical_DR_ID / GDR_ID, DR not in DR_MASTER, duplicate active mapping per DR + GDR + type, unknown type). Set 2 identifiers only — no legacy crosswalk. The S2-S4 "entity-specific metrics" rows are logical GDR-M mappings on the standard, flagged entitySpecific, never invented topical DRs. - Runtime — topical DR first (
app/esrs_gdr.py). The DR status still comes only from the material IROs (esrs_activation); a GDR mapping never activates a DR. Not active → "No material IRO connection", no GDR obligation. Active → the combined requirement is composed on each request: topical DR + its data points + the mapped GDR (objective, full requirement) + the GDR ARs + the topical ARs + the topical supplement. Nothing topical is replaced, hidden or deleted; GDR data points are never duplicated into the topical standards. - Absence logic (ESRS 2 para. 39 / para. 52). A material IRO without a policy / action / target keeps its DR active and shows the GDR's absence disclosure; GDR-M has no general absence statement (assess whether topical or entity-specific metrics are needed); GDR-T adds para. 52 effectiveness tracking when no measurable outcome-oriented target exists.
- Reuse register (
esrs.disclosures in the workspace document, validated by the server like the IRO → DR links). One policy, action, metric or target links to many DRs and IROs — registered once, scope visible on every DR it serves. Writing or approving the documents themselves, forms, evidence, workflow, transition rules, reporting elections and manual DR activation are not part of this. - API (same conventions as the ESRS routes: workspace = entity, reporting year in the path, computed per request):
GET /api/esrs/gdr/master; GET /api/esrs/<year>/dr-status/<drId>/gdr = getTopicalDRWithGDR → {topicalDR, drStatus, triggeredByIROs, mappedGDRs, gdrApplicationRequirements, topicalDatapoints, topicalApplicationRequirements, combinedRequirementView, standardFraming}; GET /api/esrs/<year>/gdr-overview; POST /api/esrs/<year>/disclosures/validate. GET /api/esrs/<year>/dr-status rows gain gdr (the mappings) and gdrApplies (only when active). - Screen (
71b-esrs-gdr.js, same lazy chunk). Every DR stays listed; active PATM DRs carry "GDR-P applies" etc., inactive ones "not applied". The expansion shows the cross-cutting framework: application rule, topical supplement, absence line with the covering register objects, objective, full requirement, GDR ARs, and the numbered combined requirement (e.g. E1-5: GDR-A paras. 44-46 → GDR-A AR 35-41 → E1-5 requirements → E1-5 ARs). S2-S4 show the entity-specific GDR-M frame under the standard. "Policies · actions · targets" opens the reuse register. - Tests:
tests/test_esrs_gdr.py (every unit and integration test of the implementation prompt: active E1-5 → GDR-A with E1-5 content kept; inactive E1-5 visible without a GDR-A obligation; E1-8 → GDR-M METRIC_FRAMEWORK; one policy serving several DRs without duplicates; absence of a target → GDR-T para. 52 without a status change; complete DR lists before and after; materiality change updates reportability; topical and GDR ARs without duplication; cross-standard reuse E1-E5 / S1-S4), tests/frontend/esrs-gdr.test.js.
2026-10-10 — v1.49.2: StartUp → Customers · Funding · Angels
- Three new tabs in the private StartUp section: Customers (the prospect list in three fit buckets — A trade & manufacturing with stock, B services & projects, C industries with a specialist system next to NovuHub — with fit score, management from the company's Impressum, best channel, a personalised German and English e-mail, a day-10 follow-up and a status per company), Funding (grants, loans, guarantees, banks, public funds, free advice, funding for customers' digitalisation projects and the networks that lead to customers — ordered by what to do first, each with the official contact and a ready e-mail) and Angels (angel networks, business angels and the early-stage funds that co-invest — with e-mail and a short LinkedIn note).
- The lists are not in the code. They are a JSON file the founder imports in the tab (
Import list (JSON)); the server keeps one copy for the founding team (startup:outreach) and the contact log separately, so a re-import keeps every status and note. GET /api/startup/outreach, POST /api/startup/outreach/import, POST /api/startup/outreach/item — owner-only like the rest of the section (404 for everyone else). Signature placeholders come from "Your situation". - No migration, no new package, no new .env switch.
2026-10-10 — v1.49.1: the connector files documents into SOPs & Docs
- New connector action
upload_file: the owner's Claude (MCP) can file a PDF, image or Word/Excel document from the owner's computer into SOPs & Docs — into a folder by name (created if it does not exist). The file goes through the same checks and storage as a user upload (type block-list, antivirus when configured, size limit), lands as a normal file tile the team can preview and download, and the upload is recorded in the audit log as "AI agent (connector)". Programs, scripts, web pages, audio and video are refused. POST /api/agent/file (Bearer token) with name, folder, content_base64; upload_file listed in /api/agent/capabilities. The MCP server (mcp/novuhub_mcp_server.py) has the new tool — restart Claude Desktop once after the deploy.- No migration, no new package, no new .env switch.
- New in Test → 24/7 Lab: nine report columns in the company table and the industry checklist — HR, Sales, Marketing, Finance, Inventory, Projects & Tasks, Customer Service, Learning, My Assistant — each "✓ 42/42", "✗ 37/40" or "—" (the industry row shows the worst company and the sum). A click shows every figure: report and tab, expected value, platform value, difference, who checked it, when, screenshot. On phones the columns fold into one "Reports" column.
- How: in every company run each team opens its own reports like a person (period set, filters cleared, the right view) and reads every key figure; the Lab works out the correct value itself from the data, independently of the platform's code (money to the cent or the printed euro, rates ±0.1 pp, counts exact). A wrong figure is a serious finding with a screenshot, a missing one a gap. The round PDF and e-mail carry a company × section table and every wrong or missing figure.
- Platform bugs the new checks found and fixed: My Assistant's weighted pipeline ignored a deal's own probability and currency and counted custom won/lost stages as open; Sales "Overdue receivables" counted the full amount of part-paid invoices; custom won/lost stages (e.g. "Gewonnen") were missing from Sales won, win rate, average deal and Marketing revenue/ROI; Portfolios counted archived tasks as overdue; My Assistant's headcount counted duplicates and people who had left; an FTE written "0,5" counted as 0; a receivable stored as "1.234,56" counted as €0 in My Assistant and the call list; My Assistant rounded euros differently from the screens. Customer Service shows the average first response.
- No migration, no new package, no new .env switch.
2026-10-10 — v1.48.6: Construction and Consulting audits — company-day dates, clear feedback, a Lab that survives restarts
- Dates follow the company day: invoices, credit notes, quotes, recurring invoices, the invoice-number year, the DATEV range and "created on" stamp, dunning letters, mailbox bills and e-invoices without a date use the workspace time zone (default Europe/Berlin) — an invoice issued at 00:30 is dated that day in Germany, not the UTC day before.
- My Assistant: 👍/👎 respond at once ("Thanks — we'll show fewer like this", optional reason) and re-rank the cards on the next visit; "Open →" in Routine always goes to the item and flashes it, or opens it in its own window.
- Shared computers: the browser's copy of the workspace now records whose it is; a copy of another person or workspace is removed before anything reads it.
- Faster first screen: date formatting warmed early, the browser copy is written after the first screen, the welcome-tour code loads only for people who have not seen it, the service worker keeps fonts and the event script.
- 24/7 Lab: a person's day cut off by a restart (deploy, reboot, memory limit) is replayed on the workspace as it was before that day, recorded as a Lab incident — the Nordland Consulting "bills 1 of 5 / 0 employees / bank 0 of 14" findings were such a replay; DATEV and headcount checks use the platform's day across midnight; the pipeline and won-value checks count the workspace's own deals.
- No migration, no new package, no new .env switch. Optional:
NOVUHUB_DEFAULT_TZ (default Europe/Berlin) for workspaces without a time zone.
2026-10-10 — v1.48.5: what the Fjord IT-Systeme audit found, fixed — instant windows, faster Tasks, honest Lab timing
- Nothing is silent any more: opening an invoice, a payment, Dunning or any tool whose code is still loading shows "Opening …" after 150 ms instead of nothing; every row in Issue invoice and Bills opens its record (mouse, Enter, Space).
- P&L layout: lines move up/down within their section reliably (positions renumbered, the two rows swap in place, the first/last arrow is disabled with the reason); changing a line's role says where it moved.
- Faster: Tasks list, Gantt, board and project cards draw what fits on screen first and the rest one frame later (List 1.5 s → 0.7 s, Gantt 1.4 s → 0.5 s at 6× CPU); HR › Reports 2.3 s → 0.15 s to first paint; P&L tabs, Save month and moves redraw only the P&L block; Finance is fetched in the background for finance people.
- 24/7 Lab measures what people really get: its own server now serves static files like nginx (gzip, immutable cache, no cookie variation), employees of a company share the browser cache like returning users, a screen counts until its first painted frame, and sign-in is reported as "first visit (cold)" and "usual (warm)".
- No migration, no new package, no new .env switch.
2026-10-09 — v1.48.4: what the Logistics and IT-services audits left, fixed — faster sign-in, exact P&L lines, ⏱ beside ⌘K
- Faster: start-up repair of the document about 3× faster, no whole-document save at sign-in when nothing changed, saves reuse the parsed base, the server keeps the assembled document per version (redaction still per person), "Re-analyse" returns the stored analysis when nothing changed (422 → 84 ms on the server) and shows "Analysing…" in place; first screen 12–22 % sooner, reload about 15 % faster; Dashboard shows the 8 projects that need attention most ("Show all").
- P&L imports (SKR03 / SKR04): the import review shows for every account "Why this line" (the chart's verdict, e.g. "SKR03 4950–4959 → Operating expenses"), flags a line that contradicts the chart and fixes it in one click (remembered for the next months); chart ranges checked against DATEV (4000–4099 cost of sales, 4820–4829 depreciation, 4870–4879 financial expenses, 8800 asset sales, 8960/8990 inventory change and own work, SKR04 7400/7500, profit appropriation and imputed costs excluded); a one-account BWA group is no longer taken for a total.
- Customer radar: every import says how many customers the file itself has ("customers in this file: 17") and how many the radar holds from earlier imports; the imports table shows customers per import.
- HR: a working day = the person's weekly hours ÷ their own working days (else the company's, else Mon–Fri) — hourly leave and the overtime target use it, the window says "1 day = 10 h (40 h ÷ 4 days)"; new "Working days" on the employee file. Contract figures (weekly hours, working days, annual leave, employment type, FTE, start date) can only be changed by HR, managers, admins and the team's lead — enforced on the server. Approve/Reject buttons name the request they decide.
- Clicks: "What does it know about me?" opens at once; suggestion questions are always asked; "Send reply" with an empty box says what to do; the sidebar rail no longer rebuilds under the pointer; ⏱ sits left of ⌘K.
- 24/7 Lab: HR approves the right request; the explorer no longer changes the wrong row when it sets a drop-down back (it had pinned 4955 to cost of sales); hourly-leave and Christmas probes follow the company's working days; the sign-in stopwatch waits for the first screen.
- No migration, no new package, no new .env switch.
2026-10-09 — v1.48.3: My Emails is private, leave by the employee (also by the hour), a ⏱ clock in the top bar
- My Emails privacy (serious): the 07:30 mail run wrote the mailbox's e-mails into the shared workspace document, so every colleague saw the owner's mail. Mail now lives per person (
personal_mail table, app/personal_mail.py, /api/mail/mine…): the company mailbox belongs to the person who connected it (before this release: the workspace owner), every person can connect their own mailbox, the shared document never carries mail (stripped on every read, save, 409 answer and backup), and old mail in the document moves once to its owner. A new connection needs its own password (an admin can no longer take over the owner's mailbox). - Leave: always requested by the signed-in person (no employee drop-down; enforced on the server for every role); "Full days / Half day / Hours" — hours on one date count hours ÷ the daily working hours against vacation (sick and personal hours are recorded, not deducted), one rule in browser, server and My Assistant; "🌴 Request leave" sits next to "+ Log hours". HR approves everyone's leave and hours except their own.
- ⏱ in the top bar: running work time; Clock in/out, Start/End break, Log hours, Request leave, Overtime account; ArbZG break reminders; the clock is each person's own (server-checked) and colleagues see only "working / on break", never times.
- Date of birth (Settings): saved only when complete and valid (no more half-typed years), dd.mm.yyyy; every date field ignores half-typed years.
- Phones: below 860 px the app was squeezed into a narrow column — fixed.
- 24/7 Lab: employees request their own leave (the Lab signs in as them); new check for the ⏱ clock and hourly leave.
- Database: new table
personal_mail and column workspace_config.mailbox_owner_id — created automatically on start. No new package, no new .env switch.
2026-10-09 — v1.48.2: what the Technical-wholesale company audits found, fixed — and the goals become reachable
- Why: the 24/7 Lab's two wholesale companies (150 and 127 people) reported 1,647 problems and neither reached its management goal ("overdue receivables under 5 % of revenue", "halve stock-outs without raising the stock value"). v1.48.1 already solved about half; this release solves the rest as strengths.
- Finance — collections: a ranked "Who to call this week" list in Receivables (
finCallList, app/collections_de.py, same rule in browser and server): amount and days overdue, dunning stage and Verzug, broken promises, payment behaviour, credit-limit use, customer value; contact and phone from the CRM (only when shared); next step per customer (call, Zahlungserinnerung/Mahnung, wait for a promise, clarify a dispute first, hand over after the Letzte Mahnung); log the call (reached, promise with date and amount, dispute, call back, not reached) — promises are checked against real payments, the promiser is reminded on the day and a broken promise is flagged; activity log without amounts. Overdue ratio (overdue ÷ 12-month revenue against a target, default 5 %), DSO, average days late and a 6-month trend. Stock value (Vorräte) in Finance from the Inventory valuation with slow-moving and obsolete shares (§ 253 (4) HGB). My Assistant gives the same figures. - Sales — Customer radar import: one ERP-export reader (
app/sales_erp.py): titles, page headers, group lines, subtotals and grand totals, credit notes and cancellations, net vs gross, customer number as key, name variants merged (legal forms, "Fa.", ä/ae, typos) with "Keep separate"; the preview lists every row left out and every merge and checks the file's own total; a new export of the same period replaces the old one instead of counting twice. The radar's call list ranks overdue customers by payment behaviour and links to the Finance call list. - Speed: Tasks, Calendar and Risk Radar open about twice as fast (one pass over tasks instead of one per person, id indexes, cached date and number formats); saves no longer write 1 MB to the browser storage inside the click; the page shell no longer reassembles the whole workspace; Settings redraws once instead of ten times; sign-in about 15 % faster.
- Forms: one required-field rule for every create window — nothing is saved empty, the missing field is marked, focused and explained; edit windows saved unchanged say "No changes" and write nothing. Fixed: saving an existing purchase order created a copy, connectors could not be switched off or newly connected, copied campaign links had quotes, My Assistant text settings and four automation-step settings were never stored.
- Usability: "+ Add employee ▾" (one employee, import a staff list, invite by e-mail) on Directory and Organization; P&L "Move up/down" disabled at the ends; account-to-line picker, Scan "Look up", Client what-if, Routine "Open →", Push API link, mind-map folder and task-row clicks all do what they say.
- HR: documents filed into an employee's folder (contracts, sick notes on leave requests) were never stored — fixed; vacation beyond the entitlement is warned before sending with the numbers (working days, Bundesland holidays, half days, carry-over only until 31 March, pending requests), the approver sees "⚠ N days over" and the server stamps the shortfall itself; HR can log hours and request leave for a person from every Time & Leave tab; rejecting in the Inbox asks for the reason in the page and tells the requester (leave approvals notify too).
- Also fixed: Marketing NPS / lead / newsletter links threw "link is not defined"; My Assistant settings show "Unsaved changes: …" while a draft is open.
- Privacy: signing out removes the workspace copy from the browser (shared computers); "tickets" is removed once from workspaces that still had the old default sharing (an admin's own choice is kept).
- Ready for launch: Finance → Archive moved out of Decision Intelligence into its own chunk; a test fails if any real section depends on Test, Decision Intelligence, Business Plan or StartUp, so those can be deleted cleanly at launch.
- 24/7 Lab: management goals judged per question by the sections that own the data plus My Assistant; edit and pre-filled windows are no longer "empty saves"; "+ New"/menus and aria-labels are understood; disabled buttons and redraws no longer count as slowness; a crashed Lab browser is restarted and not counted against the platform; HR business partners are team leads.
- No migration, no new package, no new .env switch.
- Why: the 24/7 Lab's first two "Machinery & plant engineering" companies (141 and 107 employees, v1.47.0) reported ~1,450 problems. The real platform problems are fixed here as strengths; the Lab's own false alarms are listed in the deploy note.
- Privacy: colleagues' leave reaches a browser only as "Away" (no type, note or sick note) unless the viewer is the person, their lead or approver, HR, a manager or an admin; saves from a filtered browser are put back on the server (
app/people_access.py). Bank, tax, social-security, birth date, private address and emergency fields only for the person, HR and admins; reviews confidential; salary bands only for HR-report users, leads and managers. A member can no longer file leave in a colleague's name, delete their own approved leave, approve their own hours, or delete whole sections (team, settings, projects …) with a save. - Everyone: every employee always has their own Time & Leave (request leave, balance, own hours, own documents — BUrlG) and the company Directory, whatever sections they were given; approvals stay with leads, HR and managers and are checked on the server (also for hours).
- Customer Service: the ticket queue only for the people given it (no longer shared by default); everyone else sees only their own requests in "My requests" (Help & feedback) and cannot change Support's answers or the triage fields.
- Finance: one bank-CSV reader for every German bank layout (BOM, cp1252, preambles, balance lines, Soll/Haben, S/H, every amount and date form, multi-line purposes, duplicates flagged); the preview lists every skipped line with the reason and proves opening + net = closing (CSV, CAMT.053, MT940). Six key figures at the top of Finance, Receivables and Payables (month revenue, open and overdue receivables, open payables, due in 7 days, cash with runway) plus "You entered today". "✓ matched" opens the match with Undo (audited). Bill and chat file previews no longer break the CSP.
- Risk Radar: mitigations as text or as an action plan (owner, due) — each action can become a tracked task; no more crash on imported data; the register CSV downloads. Every CSV export now uses one German-Excel writer (BOM,
;, decimal comma, dd.mm.yyyy, formula-safe). - My Assistant without AI: Ask answers management questions by rules in English and German — margin, overdue, cash, revenue, contracts and renewals, tickets, stock, pipeline, hours per project, leave, capacity, training … — with figures, top items and links, only from what the person may see (amounts only for Finance roles).
- Speed and usability: screens open in about 1 s instead of 7 s (the welcome tour no longer blocks the first clicks; polls download the document only when it changed; saves send only what changed; JSON is compressed; chunks preload in parallel). Directory search (any name order, umlauts, phone, skills, keyboard). "Mention a project" shows a chip strip of what the message links. Dismissing an Assistant card closes at once with a clear reason field.
- Also fixed: Settings → Data & Privacy "Export data" (GDPR export) opened
/api/members/undefined/export; an empty Ask now says what to type; My Assistant settings show unsaved changes and the first/last priority can't be moved further. - 24/7 Lab: file pickers work again in the Lab browser (v1.48.0 regression); Time & Leave and Directory are no longer reported as extra sections; own sick leave is not counted as a colleague's; the Assistant check reads the whole page; leave requests avoid the person's own booked days; "analytics" is no longer taken for an .ics export.
- No new environment switch, no migration. Deploy as usual.
2026-10-08 — v1.48.0: every "fixed" gap rebuilt to a strength, Inventory variants, bins, price lists and RFQs
- Why: a careful check of the 21 gaps marked "built" since the 29 September competitor assessment found 18 only partly done, 2 basic and 1 broken (Test → Functionality now shows the check at every suggestion,
app/testlab_functionality_checks.py). Each one was rebuilt end to end and then reviewed independently three times with exploit scripts against the server; the result is 6 ahead, 16 on par, 1 partial (carbon factors), 0 broken. - Projects & Tasks: the AI step in automations waits for its answer (with a rules fallback limited to the step's own categories), later steps route on the result, five modes (category, summary, draft, extract, translate), whitelisted fields, results visible on the ticket / task / deal, leave records never sent to the AI. Cross-section triggers fire from every path (also a manager's own leave, My Assistant's approvals, tickets from the API, e-invoice and expense bills — server-side events are queued, leased and confirmed exactly once); the over-budget check runs on the server. Forms: "show if" on the public page, routing on several conditions, tick box / several choices / file fields, due date and priority, notifications, one rule set in the app and on the public page. Project from a sentence respects the permission and what the person may see, learns durations, has an editable proposal, parallel tasks with dependencies and assignees by skill and load.
- Home: Suggest my day uses only my own work and busy times, my own hours and working days, reflows, re-plans when work changes, carries over and shows the plan in the Calendar; the morning brief keeps "since your last visit" all day (or 1/3/7/14 days) and ranks changes by what concerns me; the Inbox lists every approval the person can decide (time, leave, bills, expenses, sign-offs, documents to review or sign), "Tomorrow" is the next working day, done / keep / dismiss, the badge matches.
- Sales & CRM: a won deal starts its project by default (setting "When a deal is won") with an hours budget and an AI brief from the deal and quote; the customer accepts or declines a quote on a private link
/q/<token>; a quote becomes one invoice only after acceptance — enforced on the server for every role, together with Finance's right to change invoices. One deal score on the card, learned from the workspace's own won and lost deals, with "Why N %?". The 360° customer page is a real page with a shareable link and blocks that follow the viewer's access. Next best action: no fake "contact" from planning, no duplicates, e-mails sent to the customer's own contacts only, audited. Custom won / lost stages count everywhere. - Communication: chat catch-up works ("since my last visit", 24 h, 3 or 7 days); the AI wording needs an admin switch (off by default), the AI permission, membership and a consent naming the number of messages, nothing is stored. Task / deal / ticket cards from the composer, cards follow access, a message becomes a task through an editable dialog with a privacy note.
- Finance: dunning on one rule in browser, server and My Assistant — Zahlungserinnerung → 1. Mahnung → Letzte Mahnung with a minimum interval, Verzug per § 286 BGB, dated Basiszinssatz table with half-year split, B2B/B2C asked when unknown, interest and the € 40 fee booked without VAT, German letter on the letterhead (
app/dunning_de.py). Bill approval is four-eyes on the server for every save path and role (never the person who entered the bill, peak amount, currency and exchange rate locked, a looser limit needs a second finance admin; bank matching, DATEV and the VAT report respect it). Expense claims with 2026 per diems (per-day meal cuts), BMF foreign rates, three-month rule, mileage, restaurant food / drinks split, Bewirtung 70 / 30, VAT per kind into the bill, journal and DATEV, paid with the bill, receipts in private storage, HEIC and PDF (app/expense_de.py). - HR: retention table with legal basis (8 years Buchungsbelege per BEG IV, 6 years Lohnkonto, 10 years annual accounts …) counted from the document's period; "keep" only extends; early deletes refused on the server and for files; e-signature by the employee only, with the server's hash of the stored file, IP, device, a stamped copy and the audit chain; daily reminder from the worker (
app/retention_de.py). Leave requests take neither status nor approver from the browser. - Admin: the audit log records sign-ins incl. 2FA and SSO, failed sign-ins, sign-outs, HR views, API and agent calls, files and AI use (never the prompt) with the device; hourly chained anchors detect deleted rows; retention set by admins; filters and paging. Named custom roles (
app/rbac.py) enforced on the server; "Use AI features" checked on every AI call and greys out AI buttons; admins sign out anyone and revoke their API and agent tokens; replayed cookies are refused. New table audit_anchor and column audit_event.ua (migration c7e9a1b3d5f2, applied at start). - Sustainability: carbon footprint rebuilt (
app/carbon_footprint.py, same rules in the browser): correct scopes (district heating Scope 2, Erdgas, Heizöl, flights by distance band with RF), net amounts counted once, SKR03 / SKR04 accounts as the spend source, location- and market-based Scope 2 (AIB 2025 residual mix), activity data, months, data-quality score, automatic E1 evidence. VSME report built on the server for one reporting year with the previous year, net turnover, B2 matrix, energy in MWh, waste split, accident rate, training hours by gender and module C1–C9, German and English PDF (app/vsme_report.py). - Inventory: item variants (a family with options; each variant a full item; generator, size × colour matrix; from imports, the API and Shopify product exports), storage bins (home bins, bin-to-bin moves, stock and counts per bin in walk-path order, pick list by bin), supplier price lists (quantity breaks, validity, preferred supplier; orders and reorder drafts priced by the cheapest total offer with minimum order and pack counted; receipt price check; import kind "prices",
/api/v1/inventory/prices), requests for quotation (e-mail to several suppliers, answer matrix, comparison with the best split, award → draft orders and price lists). Transfers are value-neutral in every valuation method. - Privacy & security, also fixed on the way: members can no longer change the bookkeeper, approval limits, per-diem or base rates, retention rules, team leads or leave approvals by saving the document; expense claims are visible to the claimant, approvers, HR and the bookkeeper only; receipts and dunning letters are never public; recurring invoices are created only by finance; the event queue and budget marks are server-only.
- Environment switches (optional):
NOVUHUB_AI_PRICE_IN / NOVUHUB_AI_PRICE_OUT (AI cost per token in the audit log). Deploy as usual (git pull, bash build_frontend.sh, restart novuhub, novuhub-worker and novuhub-lab); the database migration runs on start.
- The virtual-staff table is gone; in its place the company audit. Every run is one invented firm of 100–150 employees in one B2B industry (twenty industries come built in: machinery, technical wholesale, logistics, IT services, construction, consulting, engineering, medical technology, agency, staffing, facility management, packaging, electronics, food, automotive supply, energy, printing, chemicals, training, textile). The firm has ten teams along our sections — management, project office, sales, marketing, warehouse & purchasing, HR, finance, knowledge & training, communication, customer service — never fewer than ten people each, every one with their own account and only the access their role would get (team leads get their section, administrators everything). The HR team imports the company's own staff list first (the old system's export: two title lines, German headers,
Nachname, Vorname, mixed dates, a duplicate row, a total row, a pay column that must not be read) — the people the audit then signs in as are the people from that file. - Every employee works the platform, no matter how long it takes. Each person does the everyday things (My Day, Inbox, directory, hours, leave, chat, a task, the calendar, a question to My Assistant), the team lead runs the section's day, the specialists run its hard cases, and the team shares out every screen and tab of its section and presses every control on it — every button, drop-down, tick box and export (each tick box, drop-down and switch is put back afterwards, and the company's settings are restored after every person, so nobody inherits a colleague's experiment; destructive ones are never pressed; the owner's Sustainability, Decision Intelligence, Business Plan and Test sections are never touched). Windows that save with nothing filled in, exports that hand out no file, buttons that do nothing, screens that stay empty, errors and slow screens are written down with a screenshot. The data is at the highest messiness throughout; the brief is stricter than ever: find every reason not to buy.
- A management goal per company. Each firm starts with one goal from its industry ("cut overdue receivables", "deliver on time", …) with the sections it needs and the questions management would ask. At the end the managing director checks on every section's screens whether those questions are answered, and asks My Assistant the same — the verdict (reachable · partly · not supported, and whether the Assistant helps) is part of the report.
- The new window in Test → 24/7 Lab: the checklist of industries (add, remove, how many companies each — the Lab follows a change at once), Start / Pause / Skip, one line per company (industry, size, people done, problems, serious, teams that sign off, goal, Assistant) filterable by industry and status, and for every company its profile, goal verdict, the team organisation (lead, people, access), per team the weaknesses, gaps, potential improvements, the team's reasons not to buy and every finding grouped (how many saw it, screenshot), every person's letter with their tasks. A running company shows its figures so far.
- The round PDF and the e-mail. Round PDF holds everything: the checklist, what fails in most industries (one line per finding, the industries that saw it), how each section does across the companies, the management goals, and a chapter per company with its teams and every grouped finding. The e-mail goes out once — when the whole checklist is done — with the figures per company and what was found in most industries; after that a new round starts only when the owner presses Start. The daily Lab report shows the round's progress.
- Also: the Lab's status says which company, person and task is running; the old daily staff day and its evening e-mail are off (
NOVUHUB_LAB_STAFF=1 brings them back); the members list links an existing directory entry to a new account by e-mail instead of adding a second person (the audit's accounts, but also a real staff-list import followed by invitations); the staff-list import skips total rows and the same person listed twice. - Environment switches (all optional):
NOVUHUB_LAB_AUDIT=0 switches the company audit off; NOVUHUB_LAB_AUDIT_SLICE is how many people work before the other Lab jobs get a turn (default 12); NOVUHUB_LAB_REPORT_TO the recipients of the round e-mail (default: the Test section's owners). Deploy as usual (git pull, bash build_frontend.sh, restart novuhub, novuhub-worker and novuhub-lab) — then press Start in Test → 24/7 Lab → Company audit.
2026-10-08 — v1.46.1: everything the 24/7 Lab found on 7 October, fixed — and My Emails after the morning mail run
- My Emails was empty after the 07:30 mail run. The run read the whole inbox from the end of the working day, but it opened every message to look for invoices, and when the mail server dropped the connection half-way the e-mails it had already read were never saved. Now the inbox is read first (headers only, nothing is marked read) and saved straight away; a dropped connection is retried once; only mail that looks like an invoice (subject, a PDF, or an invoice word with an amount) is opened for the invoice reader. My Emails shows when the mailbox was read and how many e-mails were found, and the tab pulls fresh data when it is opened.
- P&L and budget imports: a DATEV BWA with account detail (page break, group totals, running subtotals, the ratio row), a Summen- und Saldenliste (the month is Soll − Haben of the month columns; balance-sheet accounts, class totals and the year-to-date columns are left out), an English trial balance (title lines above the header, Debit − Credit, numbers stored as text, accounts 0440 stored as 440), and budgets by P&L line (costs negative or positive, Plan / Budget chosen over Vorjahr and Forecast, subtotal rows such as Rohertrag, EBIT, Jahresüberschuss never counted as lines). The SKR03 / SKR04 chart is now told apart by where the accounts sit; SKR04 4830 is other income, 7600–7649 income taxes, 7650–7699 operating taxes (Kfz-Steuer); SKR03 4320 Gewerbesteuer is a tax. The import window offers Debit and Credit columns.
- Bank imports: the bank's own CSV exports are read on the server — Sparkasse CSV-CAMT (pending rows left out, the real Betrag column), exports with info and balance lines, the PayPal activity download (Net, a foreign-currency sale booked through its conversion line, pending payments left out); a line without a booking day is never given today's date; CAMT.053 leaves out pending entries; a statement imported twice (or two overlapping files of one account) is not booked twice — the preview says how many bookings are already in the books.
- E-invoices: a document discount and a prepayment are no longer reported as problems; a supplier's credit note is saved as a credit that lowers what you owe (a negative bill), not as a bill to pay. The reader moved to
app/einvoice_incoming.py. - Receivables: a part payment can be recorded (Received so far); the open amount, aging, cash forecast and My Assistant use what is still open.
- HR: overlapping leave requests are refused; vacation beyond what is left this year is flagged; more than 10 hours a day and work on Sundays or public holidays are flagged (Arbeitszeitgesetz §§ 3, 9) and marked on the entry; the same e-mail cannot be added twice; leave after a person's last working day is cancelled (or shortened) when the end date is set; Directory → Import staff list reads the staff list of the old system (CSV / Excel, German or English headers; pay, tax and social-security columns are never read).
- CRM: the same e-mail cannot be entered twice as a contact; the search finds Müller for "mueller", Großmann for "grossmann", Bäckerei for "baeckerei"; deleting a customer with open deals asks first.
- Platform core: a collection that arrives as something other than a list (an object, a text) becomes an empty list before anything reads it, a list of records with stray values is repaired, and empty entries in the lists of Finance, Marketing and Inventory are dropped ("Cannot read properties of null (reading 'date')", "(…).timeLogs.forEach is not a function"). Error messages of the Data sources pages and reports are shown on the screen (they were moved into the help popover, so the screen looked empty); Marketing → Data sources no longer asks the server again on every redraw after an error.
- 24/7 Lab: the PDF counts the auditors (now eight) instead of saying six, long roles wrap in their cell, Marketing and Inventory have their proper names; the virtual staff read the messages the platform shows after pressing a button (they were swallowed); the Test Lab's expected leads per campaign follow the size of the company.
- No new environment switches. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub, novuhub-worker and novuhub-lab).
2026-10-07 — v1.46.0: Inventory — for trade and service companies with a small stock
- A new section with seven screens. Items — number, EAN, group, type (goods, consumable, spare part, service — never stocked), unit, cost and price, supplier, reorder point, min / max, lead time, pack size, shelf life; every row shows on hand, reserved (quotes of open deals), on order (open purchase orders), available, the reorder point, the run-out at today's demand, the value and a status chip; the item drawer shows levels per location, lots and every movement. Stock — quantities and value per item and location (warehouse, shop, van, consignment), transfers. Movements — the ledger: receipt, sale, transfer, customer return, return to supplier, adjustment, write-off with a reason, count correction, opening stock; one dialog books them all, a booking below zero is refused (unless allowed in the settings), lots carry an expiry date and the oldest is proposed first (FEFO). The rule everything rests on: movements are the truth, levels are a result — no figure is ever typed over. Purchasing — orders from the reorder list (one draft per supplier, quantities rounded to the pack), sent as PDF or e-mail (
POST /api/inventory/po/send), received partially with lot and expiry, turned into a supplier bill in Finance; late orders notify the owner; supplier performance (on-time rate, lead time learned from receipts, fill rate). Counts — count sheets by scope (all, cycle plan, ABC class, group), blind counting, printable sheet, post the differences; the cycle plan counts A monthly, B quarterly, C yearly (a permanent inventory the tax adviser accepts); accuracy score. Inventory reports — overview, levels, movements, valuation (moving average, FIFO or standard cost; a roll-forward that reconciles to the movements; month-end postings of the change in inventories to the journal), reorder, aging & ABC/XYZ, cash tied up with carrying cost, suppliers, shrinkage, stock-outs (lost sales while at zero), forecast (level + trend + month factors, run-out and order-by dates per item), expiry & lots, returns, profitability, a builder — with ✨ Commentary, Excel and PDF. Data sources — the opening-stock wizard; a CSV / Excel import of items, stock levels as of a date (the difference is booked), movements, purchase orders, count results and sales with quantities (JTL-Wawi, Lexware, sevDesk, lexoffice, Shopify, WooCommerce, Shopware, Xentral, weclapp, Billbee or a plain spreadsheet; German or English headers; preview; templates; dedupe by number / EAN / reference; undo — POST /api/inventory/import); live connectors; the push API; the scan page (the phone's camera reads the EAN, then receipt / sale / transfer / count in three taps); settings (valuation, currency, default location, count tolerance, ABC thresholds, forecast window, carrying cost, alert owner, sales from invoices, negative stock). - Connectors: Shopify (now also products, variants, cost, inventory levels per location and orders as stock sales), WooCommerce (REST v3, consumer key / secret), Shopware 6 (Admin API integration), Xentral (now also products, stock, sales orders, purchase orders), weclapp (articles, warehouse stock with batches and expiry, sales and purchase orders), Billbee (products, stock, orders) — items, levels, orders and purchase orders every night, on the same encrypted-key base as the Marketing connectors; NovuHub only reads. JTL-Wawi and Lexware run on the company's computer and come by file — the page says so.
- Push and read API (workspace token):
POST /api/v1/inventory/items | levels | movements (items matched by number, then EAN, then name; a level sets the quantity by booking the difference; a movement with the same extId or the same ref + item + date + quantity is never booked twice; up to 2 000 records a call), GET /api/v1/inventory/items | movements | pos. - Connected everywhere: invoices issued in NovuHub are sales (credit notes returns) and quotes of open deals reserve stock; Finance → Balance sheet carries the stock at cost, a received order becomes a supplier bill, the change in inventories can be posted month by month; the Dashboard has tiles for stock value, items to reorder, run-outs, dead stock, late orders and days of inventory; promised deliveries and counts are in the Calendar and the ICS feed; Customer Service tickets about returns become return bookings; dead stock becomes a clearance campaign in Marketing; write-offs count as waste for ESRS E5 (70-sustainability.js untouched); the low-stock list posts to the warehouse channel; a late order becomes a task. My Assistant has an Inventory tab: items below the reorder point (with a draft purchase order per supplier to approve), late orders (a task to chase), dead stock, shrinkage above the tolerance, counts due (a count sheet to open), expiring lots, negative stock.
- Test section: the Calculations check has an Inventory area with 17 known answers and four planted problems (a sale before the opening delivery, a late order, a write-off without a reason, an invoice line naming a service); Connections shows Inventory as the tenth section, linked both ways to every other one and to the AI; Functionality assesses six areas against Shopify, JTL-Wawi, Xentral, weclapp, Billbee, Lexware, Netstock; the 24/7 Lab has an eighth person, a warehouse & purchasing lead, with seven new hard checks (German quantities, the negative-stock guard, a German level file, the push API retried, the reorder proposal, the balance-sheet line, the valuation switch).
- Also: the sign-in scene no longer shows Inventory as "coming soon"; user guide chapter, API guide and in-app help texts (English and German) added; German phrases for every new screen.
- No new environment switches. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub, novuhub-worker and novuhub-lab).
2026-10-07 — v1.45.0: the open ideas of the Sales & Marketing wave, built
- Custom report builder (Sales → Sales reports → Builder): pick a measure (revenue, units, margin, discount, invoice lines, average price, won deals, won value, open pipeline, deals created, lost deals, win rate), one or two dimensions (customer, product, group, region, rep, channel, month, quarter, year; for deals also stage, source, campaign, lost reason, kind), a chart (bars, line, table) and the row count; the period / rep / region / channel filters apply. Save a report (everyone with Sales reports can open it), post its top rows to a chat channel, export it to Excel / PDF like any tab.
- Commentary (✨ on every Sales reports and Marketing reports tab): My Assistant writes three sentences from the figures on the tab — what changed, why, what to do — the person edits them, copies the text or sends it to a channel (the sales or marketing channel is pre-selected).
- Newsletters sent from NovuHub: a campaign of type Newsletter / e-mail gets a Mailing tab: text from a doc (headings, lists, links kept; the campaign's landing page becomes the button with its UTM tags), subject, sender, the recipient list of a connected Brevo or Mailchimp account; a test goes to yourself, then the real send (confirmed). The open / click / unsubscribe figures are read back by the worker and credited to the campaign (
POST /api/marketing/campaigns/<id>/newsletter, GET /api/connectors/<id>/lists; managers only; logged in the activity log). - Scoring learned from won deals (Marketing → Leads → Scoring → Propose from won deals): from the decided leads of the last 12 months (customers vs disqualified or stale) the lift of every criterion is computed and today's points are spread in proportion (at most 40 on one criterion; threshold and recency bonus unchanged); needs 20 decided leads and 5 customers; the manager accepts.
- Data-driven attribution (Marketing reports → Attribution): a sixth model next to first / last / linear / position / time-decay — channel weights from the converting vs non-converting paths of the last 24 months (removal effect: the share of the wins that would be lost without the paths through a channel), shown with its working; falls back to linear until there are 20 decided deals with touchpoints (5 won). Selectable under Leads → Scoring → Attribution model.
- Executive funnel (Marketing reports → Executive funnel): one page from spend → visitors → leads → MQL → SQL → pipeline → won revenue → retention (buyers in the period who had bought before), with the month chart, the channel table and four sentences.
- Brand & social (Marketing reports → Brand & social): followers (growth since the period started), engagements and engagement rate, impressions / reach, posts, mentions, share of voice (our vs competitor mentions) and sentiment per network and by month. A new import kind Brand & social (one row per day and network; German or English headers; template) and the Meta (page id) / LinkedIn (organisation id) connectors feed it.
- Four more connectors: Microsoft Ads (Reporting API v13: daily campaign spend, clicks, impressions, conversions; OAuth with the Microsoft identity platform), CleverReach (mailing figures, optionally the receivers of a group as leads; OAuth client id / secret), Xentral (ERP invoices → sales history; personal access token), DATEV (DATEVconnect online sign-in: the clients the login may see and the outgoing invoices filed in Belege online, amounts only when the document data carries them — DATEV has no public read API for bookings or revenue, and the connector says so).
- Also: the campaign editor shows only the open tab (the Details form stayed visible under Costs / Results / Links since v1.43.0); the Functionality tab shows the six ideas as built and the connectors directory counts as code; user guide and API guide updated.
- New environment switches (optional):
NOVUHUB_MICROSOFT_CLIENT_ID / NOVUHUB_MICROSOFT_CLIENT_SECRET + NOVUHUB_MICROSOFT_ADS_DEVELOPER_TOKEN (Microsoft Ads), NOVUHUB_DATEV_CLIENT_ID / NOVUHUB_DATEV_CLIENT_SECRET (+ NOVUHUB_DATEV_SANDBOX=1) (DATEV). The Meta sign-in now also asks for page insights and the LinkedIn one for organisation follower statistics (only used when a page / organisation id is set; re-sign-in to grant). Deploy as usual (git pull, bash build_frontend.sh, restart novuhub and novuhub-worker).
2026-10-07 — v1.44.0: a new scene on the sign-in and sign-up pages
- The "electric sphere": the picture beside every account page (sign in, create account, forgot / reset password, two-step sign-in) is now a slowly turning sphere of light — the AI Assistant's network, with the assistant in its core and electric veins running across it. All ten areas sit on a ring around it — Projects & Tasks, Sales & CRM, Marketing, Finance, Inventory (marked "coming soon", its routes dashed), HR, Sustainability, Learning & SOPs, Customer Service, Communication — wired to the assistant and to each other along sixteen links that really exist in the app (a lead becomes a deal, a won deal an invoice, an order reserves stock, low stock becomes a purchasing task, an energy bill becomes climate data …).
- Data makes the connections: a route is faint until data travels along it; each packet draws its line as it goes, carries a label ("Lead", "Invoice", "Stock value" …) and makes the route brighter every time it passes; unused routes fade again. Data moves slowly (6–8 seconds per route). The caption names one link at a time — area → the data → area — and hovering an area lists everything it is linked to.
- Built for every screen: a smaller sphere and short names on phones, German labels on the German pages, a fixed still picture with "reduce motion", and no drawing while the page is hidden or scrolled away. No new environment switches; deploy as usual (
git pull, restart novuhub).
2026-10-07 — v1.43.0: Sales reports, a Marketing section, and the data integration behind them
- Sales → Sales reports: one page with eleven tabs for any period, filtered by rep, region and channel — revenue by month (against last year and the Finance budget), by customer, product, region, rep and channel, new vs existing customers; pipeline and weighted forecast, stage conversion, win rate, deal size, cycle time, stuck and slipped deals; customers (ABC and concentration, payment behaviour, health from tickets and NPS, lifetime value, lost and shrinking customers); products (margin, discounts and price leakage, price waterfall, cross-sell pairs); team (quota attainment, win rate, cycle, activities, quote-to-order); forecast (seasonality, the normal next month, forecast accuracy by quarter); subscriptions (MRR, ARR, churn, NRR) and partner revenue; activities (first response, waiting deals, follow-up tasks); a region map from the customers' postcodes; lost deals by reason, competitor and stage. Revenue comes from the invoices issued here and an imported sales history (
GET /api/sales/history); every tab exports to Excel and PDF. - A deal now carries its source, campaign, sales channel, currency, region, partner, kind (one-off / subscription) and — when lost — the reason and the competitor (asked when a deal is dragged to Lost); every stage change is logged, so conversion, cycle time and "the forecast as of a date" are real. A contact carries its source, campaign, lead status, kind, region and marketing consent.
- Marketing (new section): Campaigns — ads, newsletters, content, trade fairs, events, webinars, partner and referral programmes with budget, dates, owner, goals, costs (typed, imported, synced, or a Finance bill in the category Marketing, counted once), a UTM link builder and a lead-form link per campaign, results (leads, MQL/SQL, deals, won, revenue, CPL, ROI), a project and a chat channel per campaign, over-budget notes to the owner, dates in the Calendar. Leads — the lead life-cycle lead → MQL → SQL → customer with a configurable score and MQL threshold, one-click accept (SQL + deal) or disqualify with a reason, touchpoints from the lead form, imports, connectors and the API. Marketing reports — spend vs budget, leads and cost per lead, funnel visitor → lead → MQL → SQL → won, CAC, ROI/ROAS, pipeline and revenue from marketing, LTV:CAC, time to customer, attribution (first, last, linear, position, time-decay), website, e-mail, fairs & events, content & SEO, NPS, the campaign calendar and the budget plan against actual and the Finance budget line. Data sources — the four doors in order of effort: built in (lead form with campaign links, typed costs, a public NPS survey), a CSV / Excel import for costs, leads, website, newsletter, Search Console, content and NPS (German or English headers, a preview, templates, undo), live connectors, and the push API.
- Live connectors: HubSpot, Pipedrive, Brevo, Mailchimp, lexoffice, sevDesk, Shopify, Matomo, Google Analytics 4, Google Search Console, Google Ads, Meta Ads, LinkedIn Ads. Connect once with a key or a sign-in; the worker reads new figures every night (
Connector table, keys encrypted at rest, OAuth tokens refreshed); invoices from lexoffice / sevDesk / Shopify land in the Customer radar's sales history for the Sales reports. NovuHub only reads. Push API: POST /api/v1/marketing/leads | costs | touches with a workspace token (Zapier, Make, n8n, the website). - Connected everywhere: Dashboard KPI tiles (revenue, win rate, leads waiting, campaigns, spend, cost per lead), Customer 360° shows how the customer found us and their touchpoints, the Calendar and the ICS feed show campaigns, Finance bills count as spend and a cost can become a bill, campaigns become projects, NPS detractors become tickets, My Assistant reports leads waiting for sales and campaigns over budget and can accept a lead or pause a campaign; the Test section's Connections tab shows Marketing as the ninth section linked to every other one both ways; Functionality assesses the new areas against HubSpot, Pipedrive, Salesforce, GA4, Brevo, Zapier; the Calculations check has a Marketing area with known answers; the 24/7 Lab has a seventh person, a marketing manager with six new hard checks.
- New environment switches (optional):
NOVUHUB_GOOGLE_CLIENT_ID / NOVUHUB_GOOGLE_CLIENT_SECRET (+ NOVUHUB_GOOGLE_ADS_DEVELOPER_TOKEN for Google Ads), NOVUHUB_META_APP_ID / NOVUHUB_META_APP_SECRET, NOVUHUB_LINKEDIN_CLIENT_ID / NOVUHUB_LINKEDIN_CLIENT_SECRET — only for the sign-in connectors; API-key connectors need nothing. The connector table is created on boot. Deploy as usual (git pull, bash build_frontend.sh, restart novuhub, novuhub-worker and novuhub-lab).
2026-10-07 — v1.42.0: the virtual staff work with the new layout — and check something new every day
- Fixed after the new sidebar and page bar (v1.39–v1.41): the six virtual employees could not find most sections (the rail opens a small panel for sections with several pages) nor the “+ New …” buttons (they moved from the page header into the bar; extra actions sit under “⋯”, extra tabs under “More”). They now use the rail and the page bar the way a person does. Their HR day no longer reports a lost leave request when the request was never made, picks the person in the pay-information letter, and stops cleanly when an employee is missing.
- Something new every day: besides their routine, each person now runs new checks from a pool of hard cases in their own area (52 in all) — never-run checks first, one earlier failure checked again (fixed or not), then the longest ago. Four a day per person (
NOVUHUB_LAB_STAFF_PROBES). - Finance: the bank's own exports (Sparkasse CSV-CAMT, CAMT.053 with a pending entry, MT940 / a CSV with balance lines, PayPal), the same statement imported twice, the tax adviser's BWA, a Summen- und Saldenliste, an English trial balance, budgets written three ways, incoming e-invoices (prepayment and discount, credit note, duplicate, dollars, a broken file, an order confirmation), part payments, exports against the books, budget against actual, the forecast with little history, who can read salaries, phone and tablet.
- HR: overlapping leave, more leave than the entitlement, Christmas with public holidays, 11.5-hour days and Sunday work (Arbeitszeitgesetz), part-time FTE, a starter who has not started, the same person twice, loading a staff list, leave after the last working day, the HR export.
- Sales: the same customer twice, search without umlauts, a lost deal without a reason, a deal in dollars, deleting a customer with open deals, a quote with 7 % and 19 % and a discount, the customer radar fed with a real ERP export, the lead form under attack and sent twice, loading a customer list, the CSV in Excel, phone. The files come from five invented companies, a different one most days, with the right answers worked out independently (
app/lab/realfiles/). - The letters lead with “Reasons not to buy that I found today”, say what was fixed since the last look and what is still not fixed; a missing feature is reported as a problem, not as a wrong figure. The Test section shows the reasons on each letter and a table of every person's hard cases: how many were tried and which still fail.
- Platform fixes found on the way: a project with an invalid priority broke its page; the Activity list printed an activity's type into a tooltip without escaping it; the customer radar's settings said “NovuMetrics” instead of NovuHub.
- Test Lab truths brought in line with the platform's own rules: close-date reminders count only the deals you own (since v1.36.0), and the Organization chart (v1.36.3) shows people who have not left — and its new team cards are read correctly.
- No new environment switches besides the optional
NOVUHUB_LAB_STAFF_PROBES (default 4). Deploy as usual (git pull, bash build_frontend.sh, restart novuhub and novuhub-lab).
2026-10-06 — v1.41.2: the section's own tabs are always the top box
- When a section switches with buttons (Sales & CRM: Pipeline · Contacts · Reports · Customer radar; Business Plan: Plan · Evidence · Roadmap) and the open page has tabs of its own (Customer radar: Call list, Customers, …; a plan's side panel), the page's tabs were lifted into the top box, above the section's switch. Now the section's switch is always in the top box and a page's own tabs stay below it.
- The same rule now puts Portfolios (Cards · Roadmap · Capacity · Forecast), Calendar (Month · Week · Day), Customer Service (Tickets · Knowledge base) and ESRS Requirements in the top box. Language toggles (DE | EN) are never treated as tabs. Checked on every section and every tab.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-06 — v1.41.1: Messages — the message box stays on screen
- After sending, the message box could slide out of view when a conversation was long (the message list did not shrink). The list now scrolls inside the chat and the box always stays at the bottom; the chat fills the window below its own top.
- Small polish: the channel's round icon in the chat header and the search icon in "Filter conversations".
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-06 — v1.41.0: a modern chat; Learning tidied; Messages layout fixed
- Messages fixed: since v1.39.0 the chat was squeezed into a narrow column (the page bar landed inside the chat's grid). The bar now always sits above the chat.
- The message box empties after sending (Enter or Send) — before, the text stayed and had to be deleted by hand.
- A modern chat: clean line-icon buttons (call, video, meeting link, channel settings, search, mentions, new message, new channel); a composer that grows while you type, with icon buttons for attach, emoji, @ mention and link a project, and a round Send button.
- Full emoji picker — more than 400 emojis in sets (smileys, people, hearts, work, food, nature, travel, symbols, flags) with your recent ones first; emojis go in where the cursor is.
- Reactions: hovering a message shows three quick reactions plus Add reaction (any emoji), then reply, turn into a task, pin, edit, delete, report as icons.
- Learning: "+ New learning path" moved into the top bar next to "+ New Course"; the course list and the certificates box start at the same height.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
- The sidebar is now a slim rail: one icon with a short name per section (Home, Projects, Sales, HR, Finance, …). The open section is highlighted with a thin accent line.
- A section with several pages (Home, Projects, HR, Finance, Sustainability, Learning, Admin) opens a small panel beside the rail with its pages and their counts; a section with one page (Sales, Chat, Service, Business Plan, …) opens straight away. Point at another section while a panel is open to switch; Esc or a click outside closes it. Arrow keys move through the panel.
- The NovuHub logo and name sit at the top of the rail.
- Settings has its own button at the bottom; your picture opens your name and role, Settings, the language switch and Sign out.
- Pages get about 140 px more width. Works in light and dark mode, in German (short German names), right-to-left, and on phones (the ☰ button opens the rail).
- Nothing was removed: the same sections, access rules, counts and notification dots.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-06 — v1.39.0: one neat bar on every page (design C2 · one box)
- Every section and tab now has the same layout. Under the page title sits one box: the section's tabs on the left (with simple line icons), its tools on the right as small icons — hover one to see its name. When there are too many tabs for one line, the last ones go under More ▾ (the open tab always stays visible).
- "+ New …" buttons moved down from the page header to the right end of the bar directly above the table or list they add to (New Task, New Project, Add Employee, Add Team, New Deal, New Invoice, Add Receivable, …) — the same place on every page. When a bar has more than two extra buttons, the rest go under ⋯; Add / New buttons are never hidden.
- KPI tiles keep one height (Projects & Tasks): number and name on one line; the detail ("across projects", "next 14 days") shows on hover.
- Nothing was removed: every button is the same button, only moved. Works in light and dark mode and on phones. The N shortcut still opens the page's "+ New".
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-06 — v1.38.0: the "From other sections" box is hidden
- The folded 🔗 From other sections box on top of each section (Finance, Sales & CRM, Projects, HR, Learning & Docs, Communication, Customer Service, Sustainability) is no longer shown to anyone. The connections between the sections stay: the link cards remain in the code (Test → Connections maps them), and My Assistant's playbooks read every section's data on the server, independent of this box. The 📚 Sources chip and the section tools on top of a screen are unchanged.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
- Business Plan → 🗺 Roadmap (next to Plan and Evidence): an execution roadmap kept beside the plan. Tabs: Overview (verified / overdue / due in 14 days / in progress / blocked, the next tasks, the planning rules, the business-plan baseline, financial assumptions and the milestone gates with days to go), Tasks (the whole task register — search, filter by owner, priority, status, overdue or phase; open a task for its actions, acceptance evidence, what it depends on and what needs it), Phases (a timeline of the phases and each phase's task packages), Operating rules (sales, marketing, funding/hiring/assurance, governance and adoption) and Comments.
- Comments everywhere: 💬 on every section, gate and task — replies, @mentions (notified), resolve / reopen, remove; the Comments tab lists all open or resolved threads and jumps to their place. They use the same store as the plan's comments.
- Execution status per task: Proposed → Ready → In progress → Review → Verified complete, or Blocked / Deferred — set by owners, admins and managers, with who and when. Verified is a person's sign-off, never automatic.
- Import / export: a manager imports a roadmap file (JSON) under Roadmap → ⬆ Import; re-importing a newer version keeps everyone's statuses and comments. ⬇ JSON exports it. The roadmap is stored on the server apart from the shared workspace document (owners, admins and managers only, like the plans) — its content is never part of the code.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-06 — v1.36.4: Organization — the manager stays on top; nest teams from the employee editor
- A manager who also leads a team stays on the company card. In v1.36.3 a manager who led a team (the owner leading Finance) appeared only inside that team; now every manager who is not a member of a team sits on top, and also shows as the lead of the team they lead.
- Edit Employee → "Their team reports to" (for a Team Leader): puts that person's team under another team — the same setting as Team → Reports into, now where people look for it. A team can never be put under its own sub-team.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-06 — v1.36.3: Organization rebuilt for real org structures; colleagues no longer see each other's workload
- Organization, rebuilt. The chart is now a real tree: every line runs from a card to the cards below it, however many teams there are (the old chart drew short stubs that did not meet). Teams can report into other teams — Team editor → Reports into — so a Management team with departments under it, departments with sub-teams, to any depth, all draw correctly; a team can never be put under its own sub-team. The company card shows the managers who are not in a team; a manager who sits in a team (e.g. Management) shows there. Each team card shows its lead, its members (long teams fold after six) and a switch for the teams below it. On top: people, teams, team leads, teams without a lead and people not in a team at a glance, a search that fades everyone else, Teams only, zoom and Fit (a wide chart fits the screen by itself). On a phone the tree becomes an indented outline. Managers set a missing lead straight from the card.
- Reporting line follows the tree. Someone in a team without a lead reports to the lead of the team above; a department head reports to the lead above them; at the top, the manager (leave requests are sent there). A department head looks after the people of their sub-teams too — the same HR view and editing rights as for their own team, on the server as in the browser. Personal details stay HR-only.
- Directory → a person's page: the projects, open tasks, hours logged, billable value and estimation record are shown to team leaders, managers, admins and the person themselves — no longer to every colleague. The Directory shows which team a sub-team belongs to.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-06 — v1.36.2: members see only their team's projects; "My personal details" for HR
- Projects without a team are no longer shown to everyone. In v1.36.0 a project with no team was still company-wide, and every live project had no team — so a Finance member saw them all. Now a project with no team belongs to its owners' team; if the owners have no team either, only its own people (plus team leaders, managers and admins) see it. To share a project with the whole company, pick the new Team → Everyone in the company in the project editor.
- Settings → My personal details (new): each person enters what HR needs — date and place of birth, nationality, marital status, private address and contact, emergency contact, tax ID, tax class, child allowances, church tax, social-insurance number, health insurer, and the bank account for salary. Format hints for the tax ID, social-insurance number and IBAN (check digits) never block a save. It is stored on the person's own employee record, so HR sees it on the member page (🪪 Personal details) and in the HR editor.
- HR only, on the server: the tax ID, insurer, social-insurance number and the personal details now reach only the person, people marked HR (Directory → edit person → HR) and the workspace owner/admins — no longer managers by level or team leaders. Other browsers never receive them, the API never returns them, and nobody else's save can change them.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-06 — v1.36.1: CI is green again
- v1.36.0 left an unused
import os in app/routes/finance.py, so the CI lint step (ruff) failed. Removed. No behaviour change, no environment changes.
2026-10-06 — v1.36.0: who sees which projects, "Needs attention" for the people involved, My Emails every morning at 07:30
- Projects & Tasks (all tabs): team leaders, managers and admins see every project and task. A team member sees their own team's projects, projects they own or are assigned to, and — in another team's project — only the tasks they are assigned to, own or watch. A project without a team counts as its owner's team; a project nobody's team owns is company-wide. Applies to the project list, All tasks, My tasks, mind maps, portfolios and roadmap, calendar (and its calendar feed export), workload, dashboard numbers, risk radar lists and the ⌘K search. This is the screen rule; the server still sends the workspace's tasks to every member (a server-side boundary is a separate, later step).
- Needs attention (🔔): lists only what you are part of — tasks you are assigned to, own or watch, overdue tasks in projects you own, workflows you own and deals you own.
- Settings → Data: "Export one project" lists only the projects you may see, and "Export to CSV → Tasks" exports only your visible tasks (leaders, managers and admins: all teams). Reset and JSON import/export are for administrators only. Members no longer see "Run engine now" (the engine runs by itself); their Send me a test email button now really sends one to their own address (3 an hour) — before, a member got a misleading "check SMTP settings".
- Home → My Emails was empty since 21 Sep: the mailbox job crashed on start (a circular import between
finance.py, finance_mailbox.py and invoice_extract.py), so no mail was read. Fixed, and a failed attempt is now shown in Settings → Finance mailbox. The job no longer runs every 10 minutes: it runs once a day at 07:30 (Europe/Berlin) and puts every email since the end of the last working day (Settings → Working hours, default 17:00; weekends skipped) into My Emails in one pass — headers only, no AI. New invoices are filed in the same run; Scan now still works at any time. - Deploy: the timer changed — copy it and reload systemd:
sudo cp deploy/novuhub-mailscan.timer /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart novuhub-mailscan.timer. Optional: NOVUHUB_DEFAULT_TZ (default Europe/Berlin) sets the time zone of the working-hours window.
2026-10-05 — v1.35.3: Board → "By section" works on every project
- A project's Board → By section did nothing when the project had no sections yet: the button was disabled, but looked like a normal button, so it seemed broken. It is always clickable now; a project without sections shows all its tasks in one column and a + New section column — type a name, press Enter, and drag tasks into it. Section names are trimmed and not added twice. Disabled toggle buttons now also look disabled everywhere.
- No environment changes. Deploy as usual (
git pull, bash build_frontend.sh, restart novuhub).
2026-10-05 — v1.35.2: Run QA's sync checks work again, the staff day's XRechnung task, team leaders' access requests
- Run QA → Saving & sync: all 26 checks failed (QA report of 5 Oct, QA-001…QA-014, QA-018…QA-029). Not a sync bug in the product: the Test Lab sandbox replaces the browser storage with a plain in-memory object, and since v1.29.2 the sync engine (
wh-shell.js) listens on Storage.prototype — so the two sandbox copies never saw a change and never saved (0 saves on the stand-in server; "undefined is not valid JSON" came from an empty copy). The engine now hooks the sandbox store itself. All 26 checks pass again (7 lists are still skipped: the sample data has fewer than two Risks, Clients, Invoices, Help articles, Automations, Saved views or Goals). New test in tests/frontend/sync-storage-hook.test.js. - Run QA → "the page stopped responding" (QA-015 Open Finance, QA-016 + New Task, QA-017 Tomorrow ×5) and "0 screens · 0 buttons · 0 windows". Reproduced on the live site: in a background tab the browser slows timers down to about once a minute, the robot crawled (2 buttons in 159 s), the 90-second watchdog fired and blamed whatever button came last; after 7 restarts the run ended without counts. The robot and the sync checks now sleep on a worker timer that keeps its pace in the background (
static/testlab/timer-worker.js); the watchdog no longer counts time the tab is hidden and says so; a run that ends early still reports what was seen. - 24/7 Lab staff day: "Issue the invoice and send it as an XRechnung" blocked (e-mail of 5 Oct, 2nd run of the day). GoBD (v1.35.0) rightly keeps issued invoices on the server whatever a browser saves, so the morning's issued invoice survived the manager's "empty workspace" reset; the new invoice got the same number and could not be issued. The Lab now removes issued invoices, their ledger rows and the migration marker in its own database before the day starts (it refuses to do this on any other database). The persona's report now also quotes what the platform said when a button is missing.
- Team leaders' requests for extra sections were thrown away.
settings.accessRequests was admin-only in the access guard, so a lead who added a person with extra sections saw "sent to an admin to approve", but the request never arrived (seen in the Lab log as repeated reverts). A lead's (or manager's) own new pending request for a person in the roster is kept now; only owners/admins decide, change or delete requests. - No environment changes. Deploy as usual (
git pull, pip install -r requirements.txt, bash build_frontend.sh, restart novuhub novuhub-worker novuhub-lab).
2026-10-31 — v1.35.1: the nightly backup no longer reports "failed" after a good backup
ops/backup.sh: the clean-up of old backups used ls on a pattern that matches nothing on a new server (no backups from before the rename); under set -o pipefail that ended the run with exit code 2 after the encrypted backup had been written, so systemd showed the backup as failed. The clean-up now ignores a pattern without matches.ops/restore_verify.sh: without an argument it now looks in BACKUP_DIR (e.g. /var/backups/novuhub, where the nightly timer writes), not only in ./backups.- No environment changes. Deploy:
git pull, then sudo systemctl start novuhub-backup once to confirm (no app restart needed).
2026-10-31 — v1.35.0: security hardening + German finance compliance (e-invoice, GoBD, DATEV)
Security (the findings of the 5 Oct assessment)
- No cross-company actions: an admin can only erase or export people who belong to (or asked to join) their own workspace; an account that also belongs to another company is never deleted from here (
routes/workspace.py, routes/compliance.py). - Section access is a data boundary (
app/section_data.py): members receive only the collections of the sections they may open — finance (books, bank, P&L), receivables, bills, invoices, contacts, deals, salary bands, ESRS — and whatever they never received is put back when they save. Without Finance they still get the harmless formatting data (currency, VAT rate, exchange rates, chart of accounts, closed months), and the journal postings of their own invoices are accepted. - Colleagues' level and access are protected (
access_guard 2d): only managers change anyone; team leads only their own team (never to manager, never beyond their own sections); a new person gets exactly what the invite/approval granted. - Client IP the safe way (
app/net.py): the right-most address not of our proxies — a forged X-Forwarded-For no longer bypasses the login throttle or forges audit addresses. Account lockout after 20 failures from any address (LOGIN_ACCOUNT_MAX_ATTEMPTS), 2FA attempt limit (5 per 15 min). - Uploads cannot run (
app/upload_safety.py): programs/scripts/web pages refused; stored type from the file's bytes; only pictures, PDF, audio, video and text inline; CSP: sandbox + nosniff on every file; optional ClamAV (NOVUHUB_CLAMAV). - Backups: the in-app download contains the whole (sharded) document; server snapshots are encrypted;
ops/backup.sh encrypts dumps with AES-256 (BACKUP_PASSPHRASE); deploy/novuhub-backup.timer runs it nightly; restore scripts decrypt. - Secrets at rest encrypted (AI key, mailbox password, 2FA seeds —
app/secretbox.py, existing values encrypted at start-up). Tamper-evident audit log (hash chain, GET /api/plus/audit/verify), the change detail names what changed, and audit retention is admin-only. - Impressum template on the current law (§ 5 DDG, § 18 MStV, DSA; the EU ODR link removed).
German finance compliance
- E-invoices built and checked on the server (
app/einvoice/): XRechnung 3.0 (UBL and CII) and ZUGFeRD/Factur-X EN 16931 from the stored invoice and new company master data (address, VAT ID/tax number, register, contact, IBAN/BIC, § 19 small business) plus per-invoice customer data (address, VAT ID, Leitweg-ID, order no., VAT treatment S/Z/E/AE/K/G/O, delivery date or period). Every file is validated with the official XML Schemas + CEN EN 16931 Schematron 1.3.16 + KoSIT XRechnung Schematron 2.5.0 before it is released; 422 with the rule IDs otherwise. ZUGFeRD is PDF/A-3B with embedded fonts and all § 14 UStG details. External proof: KoSIT validator 15/15 ACCEPTABLE, veraPDF PDF/A-3b compliant (docs/evidence/, tools/einvoice/verify_external.sh). Incoming e-invoices can be checked the same way. - GoBD (
app/gobd.py): invoices are drafts until issued ("🔒 Issue invoice", or automatically with the first e-invoice); then content is locked for everyone, deletes are undone, corrections only by a credit note ("↩ Cancel with a credit note"); unique numbers enforced; hash-chained invoice ledger (invoice_ledger, GET /api/gobd/verify); closed months final, only owner/admin reopen; invoices dated before the upgrade are locked once on the first save. Pre-filled Verfahrensdokumentation (PDF, owner/admin). - DATEV EXTF 700 Buchungsstapel on the server (
app/datev.py): full 31-field header, 125 columns (format version 13), DDMM, SKR 03/04 revenue accounts per VAT rate/category, input-tax keys for bills, stable debtor/creditor numbers, optional manual journal via account mapping, Windows-1252/CRLF; checked against the format before download. - Fixed on the way: the document repair (
sanitizeDoc) wiped every invoice's seller block (company) and would have cleared the issue flag; VAT is rounded per rate then summed (EN 16931 BR-CO-17). - Lab: the manager enters the company master data; Lena issues and sends an XRechnung checked with the official rules and tries to edit it afterwards; Katrin's DATEV file must pass the format check and contain Sales' invoice; a member's browser must not receive the books.
- New dependencies:
saxonche, lxml, factur-x (pypdf), cryptography. Migration b2d4f6a8c0e2. Tests: tests/test_security_v135.py (10), tests/test_einvoice_gobd_datev.py (17), tests/frontend/einvoice-gobd.test.js (4). Deploy notes: docs/deploy-notes/DEPLOY-v1.35.0.md.
2026-10-30 — v1.34.4: the staff day starts at once, runs again after every release, and the page refreshes itself
- "Run the staff day now" no longer waits. The Lab used to finish its current run first — after a restart that is the Connections run, which takes many minutes on the live server, so the button showed "Starting soon" for a long time. Long runs now stop within seconds when the staff day is asked for (or the Lab is paused, or a new release is deployed); the rest of the Connections run follows about 10 minutes later.
- After every release the staff work again (once per release, the same day and data), so the letters and figures always describe the version that is live. The earlier outcome is kept: the Test page shows a Before → after table (would not sign off, tasks done, problems, figures matched per release) and each letter shows "was: … on 1.34.0"; the e-mail subject says "after the fix (was 6 of 6)" and lists whose verdict changed.
- The page follows by itself: while the staff wait or work, the panel shows who is at work ("At work: Lena Hartmann (1 of 6 people)") and checks every 15 seconds; when the day is written, all Lab figures reload. When a new version goes live, the Lab rebuilds the day's figures at once and the open page reloads them.
- Tests:
tests/test_lab_staff_rerun.py (4). Deploy: add novuhub-lab to the restart so the Lab stops its current run and starts on the new release straight away.
2026-10-30 — v1.34.3: the virtual staff's findings fixed; DR detail without "Linked IROs"
- Sustainability → DRs & DPs: the "Linked IROs" block under a DR is gone — the material IROs already show as "Triggered by" chips under the DR. Managers keep a small "+ Link an IRO by hand" button.
- Amount fields take amounts the way people write them:
1.250,00, 8.716,42 €, € 3.880,40, 1.250,00-, 1,250.00. Number fields become text fields with the decimal keypad when drawn and turn the text into a plain number when left (before any Save runs); ↑/↓ still step. Before, the browser silently emptied such a field (deal values, amounts, salaries). - Names are tidied when saved: extra spaces removed, a name typed ALL IN CAPITALS or all small written the usual way (von/van/de/zu stay small) — employees and contacts; supplier and customer names get their spaces tidied. A new contact at a company that already exists, written differently (G.m.b.H./GmbH, ü/ue), shows a notice.
- Imports: the delimiter is chosen by how many lines share the same number of columns (a preamble line or a tab file no longer confuses it); a single "Saldo"/balance column is read as the period amount. 0 failures in 2,400 generated files at every messiness level (before: up to 291 failures in 600).
- FTE: the exact contracted hours are summed and rounded once (no per-person rounding drift).
- Finance → Receivables/Payables: the invoice number opens the record.
- Sync: a change arriving from a colleague no longer redraws the page while you are clicking or typing (it waits until you pause); a save aborted because the page is reloading is no longer reported as "save failed: HTTP 0".
- 24/7 Lab staff day: clicks retry when the screen is redrawn, tabs are verified, the manager waits for the access picker, cards and grouped lists are found like a person would; FTE checked with half-up rounding.
- Tests:
tests/frontend/amounts-names.test.js (3), one in tests/frontend/esrs-requirements.test.js. No new environment switches, no migration.
2026-10-29 — v1.34.2: profile photos — the server reads, crops and saves them (HEIC included)
- The bug: a new member's photo never appeared — not for her, not for anyone. The browser decoded the picture on a canvas before uploading; a photo the browser cannot decode (an iPhone/Mac HEIC picture in Chrome or Edge, a CMYK JPEG) never fired the image's load event, there was no error handler, so nothing was uploaded and nothing was said. Confirmed on the live server: no file ever arrived. And even when it worked, the URL reached colleagues only through the browser's whole-document save.
- The fix: the original file goes to the server (
POST /api/files/photo, app/routes/profile_photo.py), which reads any picture with Pillow (HEIC/HEIF through pillow-heif, now in requirements.txt), turns it upright from its EXIF data, crops it square, stores a 320 px JPEG and — for one's own photo — writes the URL into the person's own roster entry on the server (compare-and-swap, matched by the signed-in account, case-insensitive). Everyone sees it on their next sync, whatever the browser did. DELETE /api/files/photo removes it. - Nothing fails silently any more: signed out, too large, unreadable or a HEIC file on a server without pillow-heif each give a clear message; the in-browser resize remains only as the fallback when the server cannot be reached, now with error handlers. The Directory editor (a manager changing a colleague's photo) uses the same server conversion.
- Tests:
tests/test_profile_photo.py (4). Deploy: the usual command — pip install -r requirements.txt brings pillow-heif (prebuilt wheels for Python 3.9–3.13).
2026-10-29 — v1.34.1: AI Assistant Library — honest drafts, no blocking mode, ⚑ Report this message
- "Do it for me" no longer blocks a scenario. For an action that always needs a yes (an e-mail, a message, a note to someone else, a record), the mode is switched to Ask me first with a note — the scenario still tests and saves. The editor follows the switch.
- Drafts no longer guess. A sentence that needs what people wrote (insults, tone, bad language …) is answered plainly: My Assistant never reads chats or e-mails. The AI draft now sees each field's meaning, adds "Only if" conditions only when asked, and says unsupported instead of picking a field that merely has the right type. The Test warns when the explanation asks about message content.
- ⚑ Report this message on every chat message of someone else: the reporter picks a reason (insult, harassment, discrimination, threat, other) and may add a note. Only that one message goes to managers, HR and the author's team lead — never to the author — through the new source Chat messages a colleague reported and the built-in scenario A colleague reported a chat message (alerts at once, with steps). Reports are kept on the server (
app/assistant_reports.py, AppState), not in the shared workspace document; nobody's chats are scanned. Route POST /api/assistant/library/report-message. - Tests: 2 new in
tests/test_assistant_library.py, 1 in tests/frontend/assistant-library.test.js; tests/test_lab_staff.py no longer depends on the real date. No new environment switches, no migration.
2026-10-29 — v1.34.0: AI Assistant Library — scenarios, "Ready for your yes", quiet mode, the record
- New tab Home → My Assistant → AI Assistant Library. A scenario is a readable rule in four nodes — When (a data source and conditions) → Only if (the day, the person) → Then I prepare (an e-mail draft, a message, a note, a task, a reminder, a how-to, a calendar block, or a record in the section) → How & who (ask me first / do it quietly / off; importance; repeat; the section and the roles). 158 built-in scenarios ship with it, at least 15 per section (Finance 24, Sales 18, Projects 19, People & HR 17, Communication & Calendar 16, Customer Service 15, Sustainability 15, Knowledge & Learning 16, Management & Risk 18), among them the German-market ones (dunning levels with § 288 BGB interest and fee, UStVA/Lohnsteuer on the 10th, e-invoicing, Resturlaub, Jahresabschluss and disclosure deadlines, the managing director's liquidity duties).
- The matrix. Sections × roles with the count per cell (bold = runs for the signed-in person); a cell filters the list, an empty cell starts a scenario for exactly that role and section. A scenario runs for a person only when they can open the section and the data it watches, and have one of its roles.
- Everyone adds, edits and removes. Any employee writes scenarios for themselves (switches any scenario off for themselves, sets their own mode); managers and team leads publish for roles and edit the built-ins for the company (reset to default at any time). "Describe it in one sentence" drafts the nodes (AI when a key is set, otherwise the nearest built-in as the starting point).
- The Test button — the Assistant confirms it can do it. Before anything is saved the server validates the rule, checks the person's section and data access, their roles, that the action is allowed for them and who it would go to, evaluates the scenario on their own data right now and shows the drafts it would prepare — check by check, with a verdict. Save needs a passed Test; every change invalidates it. With an AI key an optional double-check compares the explanation with the nodes.
- Proposals are cards. Each match becomes a card of My Assistant (chip Scenario) with the one prepared action, so snooze/dismiss/approve, follow-ups, briefs, alerts and the Results ledger all apply; the approval goes through the usual re-checked
/api/assistant/act. New action kinds there: a reminder (comes back under Follow-ups due), a note to myself, and a calendar block (a task with a time, optionally with a colleague). - Ready for your yes. One list of everything the scenarios prepared, grouped by scenario — Approve (opens the usual dialog), Not now (7 days), Never… (with the reason). The For you tab opens with a banner when something is ready; the tab badge counts it.
- It learns the person. Three "not now" in a row pause a scenario for that person (resume with one click); five approvals without an edit make the Assistant offer to do it quietly. Quiet mode is only possible for the safe kinds (a task or calendar block for oneself, a reminder, a note to oneself): the background worker carries them out, records them like an approval, tells the person ("Done for you: …") and never does one twice. An e-mail or a message always waits for a yes.
- What I did for you. The record per person: approvals, quiet actions, e-mails sent from drafts, reminders, briefs and alerts, day by day, with what it led to (collected, invoiced, avoided — measured, not assumed).
- Code:
app/assistant_library.py (model, validator, storage, learning), app/assistant_scn_sources.py (23 data sources on the person's scoped copy), app/assistant_scenarios.py (evaluation, cards, the Test, quiet mode), app/assistant_scenarios.json (the built-ins), app/routes/assistant_library.py (/api/assistant/library/…), app/static/src/79a-assistant-library.js, 79b-assistant-library-editor.js; new table assistant_scenario (migration a1c3e5f7b9d1). Tests: tests/test_assistant_library.py (12), tests/frontend/assistant-library.test.js (4). Docs: docs/assistant-library.md, docs/deploy-notes/DEPLOY-v1.34.0-assistant-library.md. No new environment switches.
2026-10-29 — v1.33.2: ESRS Requirements — conditional data points can be switched off
- The 57 data points whose own condition code says they are not mandatory on their own (E1-1.13 only if no transition plan, E1-2.17(a–d) only with scenario analysis, E1-7.27/28 high-impact sectors / energy producers, E1-9.33–35 removals / carbon credits / neutrality claims, E2-4.16 microplastics, E2-5 by role, E3 water-stress sites, E4 offsets / sensitive areas, E5-1.9, S1-6 non-employees, S1-7.24, S1-13.37, S2/S3/S4 sub-topic items …) now have a Deactivate button while their DR is active — with a reason, kept on the audit trail; Follow the DR resets. Mandatory data points have no switch; an inactive DR still makes every child data point Not active. The API (
/dp-status) carries conditional, the override and the reason. - E1-6.24(b) ("where target scope diverges from the GHG inventory") is treated as conditional by its own wording; the screen says the condition was added by the platform.
2026-10-29 — v1.33.1: ESRS Requirements — the owner's three adjustments
- Under a DR the linked IROs are one short line each (id, name, link type, on/off; the rule text behind “why”) instead of a table that repeated the rationale.
- The full regulatory text of every DR and every data point is shown next to its id (DR detail, the data-point table and the Data points list) — no more “more” link.
- Application requirements with a switch. ARs that only explain a DR or data point are marked guidance and carry no status. The 38 ARs that are themselves subject to materiality or a condition (carbon credits, EU ETS, water-stress sites, biodiversity-sensitive areas, critical raw materials, non-employees, whistle-blower directive, late payment to SMEs …) show the condition and follow their DR, and managers / team leaders can Activate / Deactivate them with a reason (kept on the audit trail; Follow the DR resets). API:
/api/esrs/<year>/ar-status; the DR detail carries them too.
2026-10-29 — v1.33.0: ESRS Requirements — every DR and data point, activated by the DMA
- New section 🌱 Sustainability → ESRS Requirements: the 64 Disclosure Requirements, 209 data points and 146 application requirements of the finalised revised ESRS (Set 2, Delegated Regulation (EU) 2026/1563) for E1–E5, S1–S4 and G1 — always all of them, each with Active / Not active, the reason, and for active DRs the material IROs that trigger it. Tabs per standard, a DR | data-point switch, search, status filter, reporting year.
- How a DR becomes active. From the DMA in the IRO Register, nothing else: a material IRO of a standard activates the DRs that are mandatory whenever that standard is reportable; sub-topic-bound DRs (pollution of air/water/soil, resource inflows/outflows, S1 wages/health & safety/training…, G1 corruption/lobbying/payment practices) need a material IRO in that sub-topic; E4-1 is never derived. Each derived link is a mapping row with its rationale (the audit trail). Managers and team leaders can link an IRO by hand (Primary / Supporting, strength, rationale) or switch a link off with a reason; statuses follow at once. Data points inherit the parent DR's status; their activation-condition codes are shown for later sprints.
- Opening a DR shows the requirement text and source paragraphs, the activation rule, the IRO → DR mapping table (including inactive and non-material links), its data points with status and condition, and its application requirements. The data-point list links back to the parent DR.
- API for the same logic (
app/esrs_activation.py, app/routes/esrs.py): /api/esrs/master, /api/esrs/<year>/dr-status[/<drId>], /dp-status, /diagnostics, mappings/validate. Statuses are computed, never stored; everything is scoped by workspace and reporting year. Design and source of truth: docs/esrs-activation.md. - The Sustainability module itself (
70-sustainability.js) is untouched; the new screen reads its IRO Register and materiality rule. Tests: tests/test_esrs_activation.py, tests/frontend/esrs-requirements.test.js.
2026-10-29 — v1.32.1: what the virtual staff found on their first day — fixed
- Creating a contact as a member no longer throws (
logActivity … .filter is not a function): the contact was saved but the dialog stayed open with no confirmation. logActivity now accepts a single id where a list is expected, and saveContact passes a list. - Dunning covers Receivables. The Dunning window (Finance side bar) now lists every overdue customer invoice — the ones issued from the invoice builder and the ones recorded under Finance → Receivables. Reminders sent and the consumer flag are written back to the receivable.
- Issued invoices on the Receivables screen. Under the AR table a block Issued invoices lists what was written in the invoice builder (open, part-paid, paid, overdue highlighted) with Open / Record payment — what customers owe us is in one place.
- Import: a two-column file no longer loses its first rows. A file with just “account; amount” whose first rows carried € signs or padding had those rows counted as header rows and silently dropped. A row with an amount in it is never a header. Test:
tests/test_finance_import_flow.py::test_a_two_column_file_with_euro_signs_keeps_every_account. - Smaller: quotes and invoices start in the workspace currency (not $); the P&L / budget import proposes the last closed month, never a month in the future; the sidebar says Incoming invoices (supplier bills) next to Issue invoice; the DATEV export is also on Finance → Journal; deleting invoices, bills and receivables is for team leaders and managers (a member only when the administrator ticks Delete records for members under Settings → Access → roles); the full backup (export / import JSON) under Settings → Data is for administrators.
- The virtual staff's day was tuned from the first runs: one number convention per file (a real export never mixes 1.250,00 with 1,250.00), a duplicated statement line is a remark rather than a wrong figure, name checks ignore case. Frontend tests:
tests/frontend/staff-day-fixes.test.js.
2026-10-29 — v1.32.0: 24/7 Lab — virtual staff: six auditors work a day and write to the owner
- Who they are. Six virtual employees with their own accounts in the Lab's own copy of the platform (never the real workspace): two in sales (account executive, sales assistant), one HR manager, three in finance (controller, accountant, analyst). The Lab's manager account sets the workspace up each morning the way a manager would — through Settings → Access and the Directory — so every person has only the access their role would get. Their brief: find every reason not to buy the platform.
- What they do. A full day's work through the real screens and forms, not the API: the controller imports last month's accounts and a budget, reads the P&L, forecast, consolidation and balance sheet, exports Excel/PDF and the DATEV file; the accountant enters invoices and bills, imports a bank statement, runs dunning, records a payment, checks VAT, the journal and an incoming e-invoice; the analyst checks the overview, cash flow and burn rate against independent sums, tries the exports, asks My Assistant and tests what an analyst can delete or export; HR creates a team, adds employees with their contract details, enters a salary, requests and approves leave, logs hours, reads every HR report, files a document and offboards a leaver; the account executive builds contacts and a pipeline, moves a deal to won, writes a quote and turns it into an invoice, plans a follow-up and messages the manager; the sales assistant submits a lead through the public lead form as a visitor, works follow-ups, the customer radar, a ticket and its answer, the CRM reports and the Inbox. Every figure is checked against a known answer; every error, dead end, restriction and slow screen is written down with a screenshot; after each day a browser refresh checks that nothing was lost.
- Sample data at four messiness levels (
app/lab/staff_data.py): clean, normal (31.12.2026, 1.250,00, semicolons), messy (mixed formats, € signs, BOM, blank lines, Müller/Mueller, German headers) and chaos (duplicates, 1.250,00-, a preamble before the header, empty cells, one company written two ways). The level rotates by day so a week sees all four; the clean answer is always kept next to the messy text. - The letters. In the evening each person writes to the owner in their own words — what matched, what would stop them, what got in their way, where their role was restricted, what simply worked, and a recommendation (yes / yes with notes / not yet / no). The letters go out by e-mail in one message, worst first (
NOVUHUB_LAB_REPORT_TO, default the Test-section owner; needs the server's SMTP settings), and appear in Test → 24/7 Lab → Virtual staff with every problem's screenshot and the task-by-task log; the day PDF carries them too. Their results and findings also feed the Lab's day report, trends and fixed-detection (job staff, finding kind staff). - When. Once a day from
NOVUHUB_LAB_STAFF_HOUR (default 9, NOVUHUB_LAB_TZ), or on demand with the ▶ Run the staff day now button or venv/bin/python lab_worker.py --staff. NOVUHUB_LAB_STAFF=0 switches the daily run off. The whole day takes about five minutes. - Code:
app/lab/staff.py (framework and the manager's morning), staff_finance.py, staff_hr.py, staff_sales.py (the workdays), staff_report.py (letters and e-mail); routes /api/testlab/lab/staff, /staff/shot, control staff_now; UI app/static/src/94g-lab-staff.js. Tests: tests/test_lab_staff.py.
2026-10-29 — v1.31.1: the "?" next to a page title now always explains the section
- Problem: the small "?" after the page title was drawn on every page (its CSS beat the
hidden flag) but had content only where a view carried a guidance banner — so on Settings, CRM, Projects and most other sections it opened an empty white popover. - Fix: the popover now always has something to say, in every section, sub-section and tab: the view's own banners (as before), the section's paragraph from the user guide (
/docs/help.json, built from docs/guide/user-guide.md) with a link to the full chapter, and a short text for every open tab — first-level and nested (Settings → Access, Finance → Profit & Loss → Import, CRM → Customer radar → Call list, HR → Working hours → Teams, Business Plan → Evidence → Gates, SSOT, Test Lab, StartUp, Sustainability …), in English or German with the interface language (app/help_texts.py, 180 tabs). The text is recomputed when the icon is clicked, so a tab switched without a page re-render still gets its own guidance. A truly empty icon is hidden. - User guide: paragraphs for Messages, Settings and the Business Plan added so every main section has one. Test:
tests/test_docs_support.py::test_help_json_gives_every_main_section_a_guide_paragraph.
2026-10-29 — v1.31.0: Business Plan — the evidence builder
- Plan | Evidence. The Business Plan section has a second workspace next to the document: the evidence builder. It carries the review framework the founders were scored against — 8 pillars, 46 criteria with the founder questions, structured fields, required evidence, builder rules, acceptance tests, dependencies and the gates each one feeds — and the 20 hard gates (PG1–PG10 first-customer pilot, SG1–SG10 commercialisation and scale), the credibility corrections C1–C8, the ten-step workflow (diagnose, ask, collect, validate, resolve, generate, challenge, rescore, approve, export) and the Definition of Done.
- Registers. Evidence items, claims, assumptions, calculation models, experiments, action tasks (PR0–PR3), decisions and risks — each with stable IDs and one editor; the state lives inside the plan (
plan.ev), so saving, history, JSON export and import carry it. - Scores and gates are computed on the server (
app/bizplan_evidence.py, POST /api/bizplan/evidence/report), never in the page, and never from narrative: validated points = weight × min(Logic, Evidence) ÷ 5; Evidence is capped by the linked items (missing / assumption / claimed 1 · uploaded 2 · verified up to 5); a contradicted item caps until resolved; "Verified" needs a verifier; unverified items older than 180 days turn stale; Logic ≥ 3 needs a rationale; raising Logic above the previous version needs an answered question; "done" needs every acceptance test, Logic and Evidence ≥ 3 and every dependency done. A gate is PASS only with a verified evidence item and an owner — otherwise Not Verified, which blocks release like FAIL. Gates override the score. - Checks (completeness, consistency, freshness, authority, dependencies, overdue tasks, P0 blockers, formula errors), the corrections C1–C8 and the builder log; founder approvals (owners) for claims, model, gates and the export version; freeze as new baseline keeps the previous L/E/points per criterion.
- Output package: "Write the reports into the plan" generates — deterministically from the registers — the claim–evidence index, evidence register, pillar score, rescore of the 46 criteria, gate report, unresolved-action register and change log in the appendix (replacing earlier versions by their table name); JSON and CSV (zip) export (
GET /api/bizplan/evidence/export); a test dataset that walks the missing, contradicted, stale and verified paths (GET /api/bizplan/evidence/testdata). - Import accepts plan files of version 2 (with evidence state) and offers to replace a plan with the same id (new version) instead of always creating a copy. Tables with five or more columns render denser; in print, wide tables keep every month column and the first column no longer sticks.
- Front end:
97o–97r-bp-ev-*.js in the bizplan chunk. Tests: tests/test_bizplan_evidence.py. No new environment variable, no migration.
2026-10-28 — v1.30.0: new sign-in and sign-up pages
- Sign in and Create account are now one page design. Both (and forgot / reset password, two-factor and "account in use") use one layout: the living network on the left, the form on its own light panel on the right. The panel is set apart by colour, rounded corners and depth — no border line. A Sign in | Create account switch at the top of the panel moves between the two. The spinning light around the old card is gone.
- The network shows how NovuHub really connects. Eight areas (Projects & Tasks, Sales & CRM, Finance, Sustainability, HR, Learning & SOPs, Customer Service, Communication), each with its tools, all wired to the AI Assistant — and now to each other, along the links that exist in the app: a won deal becomes an invoice, a finished course a certificate, energy bills climate data, a ticket a task, leave shapes the workload, and more. A caption tells one of these stories at a time while its whole path lights up (tool → area → area → tool); hovering an area shows everything it is linked to. Tools inside an area are linked to each other too. Nodes stay where they are; only light travels. With "reduce motion" it is one still picture.
- Form details: icons in the fields, show / hide password, a password-strength bar when creating an account or a new password, "the two passwords don't match" before sending, a busy button while signing in, messages shown inside the panel (and in German when German is chosen), footer with Terms, Privacy and Imprint. Phones and tablets: the network on top, the panel slides over it. All new text is in German too.
- The language switch on the other public pages (invitation, waiting for approval, new workspace) is readable on their light background again. The old
_auth_hero.html (it still showed the former name) is removed. - Test:
tests/test_auth_pages.py.
2026-10-27 — v1.29.3: fixes from the 24/7 Lab report of 1 October
- The Lab now tests the version that is deployed. It kept running the version it was started with (the report still said 1.26.0–1.27.1) because the deploy command restarts only the platform and the worker. The Lab now notices a new VERSION within a minute, finishes its job and restarts itself on the new release (systemd restarts it). Restart it once by hand after this deploy so it picks up this change.
- Customer radar "Status of …" (8 findings, 1,013 failed checks): not a platform error — the Test Lab read the last column of the customer table, and a column (the health score) now comes after "Status". The check reads the Status column by its heading; all 218 calculation checks pass for 1 October.
- Broken data never stops a screen (
sanitizeDoc, run on every load): text where a list of records belongs (e.g. timeLogs) becomes an empty list — the cause of "(…timeLogs || []).forEach is not a function"; empty entries are removed from every list of records, one level deep too — the likely cause of "Cannot read properties of null (reading 'date')"; a field that is text in the other records of a list is text in all of them — no more "[object Object]" on Activity, Workflows, Learning or Customer Service. Lists of numbers keep their gaps. - Period buttons show which one is picked (Finance, VAT, cash flow, HR reports). On the first day of a quarter "This month" and "This quarter" are the same range, so a click seemed to do nothing.
- Archive breadcrumb: the folder you are in is no longer a link that does nothing.
- Connections: Sustainability's S1 card shows training certificates (completed in 12 months, and the count); the connections scan no longer counts "create a task from a chat message" as Communication reading the task list.
- Not changed, explained in the report answer: the escaped-HTML finding in Settings and the script errors could not be reproduced on the current version; "My Assistant does not read Communication" is a feature still to build; the partly visible connections depend on the dates of the Lab's test records.
- Tests:
tests/frontend/sanitize-doc.test.js, tests/test_lab_fixes_1001.py.
2026-10-26 — v1.29.2: the real cause — in Firefox (and other browsers that follow the standard) no change was ever saved
- Cause found: the sync engine noticed a change by replacing
localStorage.setItem. Chrome allows that, but by the web standard (Firefox follows it) the assignment stores an item called "setItem" instead — so in those browsers the app never sent anything to the server, and nothing reported an error. That is why a teammate's SOP folders and profile photo showed for them and vanished on refresh, while everything worked in the owner's Chrome. - Fix: the hook now sits on
Storage.prototype (only for localStorage), which works in every browser; items left behind by the old hook are removed. - Test:
tests/frontend/sync-storage-hook.test.js runs the sync engine in jsdom, which follows the standard like Firefox: it fails with the old hook and passes with the new one.
2026-10-25 — v1.29.1: a teammate's changes always reach everyone (SOP folders, documents, profile photos)
- Problem: a teammate created a SOP folder with documents and uploaded a profile photo; after a refresh everything was gone, and nobody else ever saw it. Their browser had not been able to send the changes to the server, the app only showed a short message, and a refresh then replaced their copy with the server's.
- Fix 1 — saving no longer depends on the browser's storage. If the browser's storage is full or blocked, the document is kept in memory and still sent to the server (before, that silently stopped every save of that person).
- Fix 2 — a refresh keeps unsent changes. Until the server confirms a change, the browser keeps it together with the copy it was based on; after a refresh it is merged with the server's latest version and sent (only for the same person in the same workspace).
- Fix 3 — a refused save is visible. The person sees a red "Not saved" bar with the reason (signed out, refused, too large, no connection) until it works; the app keeps retrying. Every refused save is written to the server log and listed for owners and admins in Settings → Server backups → Saving problems (who, when, why). The browser's error report now gets through the same-origin check, so this report arrives even when the save itself was refused.
- Profile photo: says "Photo saved", or clearly says when it could not be saved.
- Server backups: snapshots are throttled again (one every 90 seconds at most, the last 20 kept); a hand-named backup file had switched the throttle off, so only about the last hour of saves was kept.
- Tests:
tests/test_sync_problems.py, a new check in tests/test_csrf_origin.py, and a two-person check in tests/e2e_smoke.py (an invited teammate creates a SOP folder and a profile photo; after a refresh both are there for them and for the owner).
- Every line of every table can be opened. Click a line's name (ⓘ on hover shows a short explanation): a window explains what the line is for in a German business plan, with a tip, and lists what it usually contains in your case (e.g. "Betriebsausstattung" → laptops, screens, desks, phones … with orientation ranges). Tick what fits, enter the amounts — per column (once, per year, or per month in the liquidity plans, with ⇉ to copy one month to all) — and the total goes into the table; subtotals, totals and the other tables follow. The picked sub-items show under the line (▸) and are part of the block's history (↶ undo). Text tables (company data, founders, contacts, competitors, milestones, SWOT, risks) get choices to pick that fill the columns. Calculated lines explain how they are calculated.
- Built-in + AI: the catalogue (
app/bizplan_items.py, 76 entries, German and English, as of 2026) covers every line of the template; tables imported under other names are recognised by their titles. For a line it does not know — or for more ideas — "AI suggestion" proposes an explanation and sub-items for your case (workspace AI key, the usual limits); kept suggestions are stored with the plan. - Comments on any block (💬 in the toolbar) and on any table line (💬 next to its name): replies, resolve / reopen, remove your own (owners and admins: any), @mentions notify the person (the notification carries no plan content). Comments live on the server apart from the plan (
/api/bizplan/comments), so they never collide with someone saving the plan. Open comments are counted on the block and in the new Comments tab of the right panel. - Highlights: select words in a text or note → pick yellow, green, red or blue, optionally with a note (shown on hover and in the Comments tab), or comment on the selected words. Highlights are stored per language with the block; they print in the PDF only when "Show highlights in the PDF" is ticked.
- Fix: plans exported as JSON before the rename (v1.28.0) import again.
- New:
app/bizplan_items.py, app/routes/bizplan_notes.py, app/static/src/97m-bp-notes.js, 97n-bp-items.js; tests tests/test_bizplan_notes.py, tests/frontend/bizplan-items.test.js. No migration, no new environment variables.
- Every name people see is now NovuHub — the app, sign-in, landing page, e-mails, PDFs, exports, help, docs and API. The sidebar name is fixed ("NovuHub", one size bigger, no longer editable); the workspace's own name stays in Settings.
- Technical names follow: settings
NOVUHUB_*, files novuhub-app.* / novuhub-chunk-*, systemd and nginx files, SDK, MCP connector, metrics, backups, browser keys. - Nothing breaks: settings under the former prefix are still read (
app/envcompat.py), saved browser keys are carried over, webhooks carry both header names, shared links keep their signatures, older backups are still found. The server folder and services can stay as they are; deploy/migrate-server-to-novuhub.sh moves them when you want (with --undo). - Kept on purpose: the domain workhubtest.site, the repository name, docker database names and volumes, the SSOT package's own identifiers, historical PDF reports.
tests/test_rename_novuhub.py guards the rest. Notes: docs/deploy-notes/DEPLOY-v1.28.0-novuhub.md. - Earlier entries below were updated to the new name.
2026-10-22 — v1.27.2: tests no longer depend on the calendar day (CI was red on 1 October)
- Why CI failed: six My Assistant tests built their invented company around the real date but expected findings that only appear later in a month (e.g. "missing invoice" is not raised before the usual invoice day + grace). On the 1st of a month they failed — nothing in the app was broken.
- Fix: the tests now pin "today" (
ASSISTANT_TODAY, honoured only when TESTING) and build their data on the same pinned day (tests/assistant_fixture.py: API_TODAY). The whole suite was checked with the clock moved to 1 Oct 2026, 16 Nov 2026, 31 Dec 2026, 15 Jan 2027 and 1 Jun 2027 — all pass. - Small app fix found on the way: the background job forgot "already announced" alerts by the server clock instead of the cycle's own clock (
assistant_store.put_state(..., now)); in production both are the same, so nothing changes for users. - Changed:
app/routes/assistant.py (_today, Results window), app/assistant_store.py, app/assistant_jobs.py; tests test_assistant.py, test_assistant_loop.py, test_assistant_jobs.py, test_assistant_links.py, test_plus.py, assistant_fixture.py.
2026-10-21 — v1.27.1: Single Source of Truth — big files are sent in pieces
- Fix: "Import failed" on the package check. The web server in front of NovuHub (nginx) refuses request bodies over 1 MB unless
client_max_body_size is raised; the package ZIP is 1.6 MB. The Import tab and "Add new material" now send files over 750 KB in pieces (POST /api/v1/knowledge/upload/chunk, then the import / upload names the upload_id); the pieces are joined on the server, checked against the same limits and deleted after use (left-overs after 24 hours). Works whatever the web-server limit is. - Clearer messages: a refused size or a server error now says so (with the HTTP status) instead of only "Import failed".
- Tests:
tests/test_ssot.py (pieces, unsafe upload ids, members refused). No migration, no new environment variables.
2026-10-20 — v1.27.0: Single Source of Truth — one governed answer, with its sources (advisory)
- New area "Single Source of Truth" under Learning, for owners, admins and managers (everyone else gets 404). Built from the NovuMetrics SSOT package v1.0.0: the package ZIP is not in git — the owner or an admin uploads it in the section (⬆ Import). A dry run first checks every file against
CHECKSUMS.sha256, the record IDs, hashes, Drive links, relations and chunk IDs; then 32 records, 1,790 citable passages (the 1,755 legacy citations kept, plus 35 OCR passages), 32 relations, the claims, the source-of-record matrix, the resolver policy, the ontology and the package's tests are registered. - Nothing is approved by the import. Every classification arrives as a proposal in the Review queue; only the workspace owner decides — approve, amend, reject, retire, override, revert — always with a reason. Decisions are append-only (the database refuses edits and deletes) and a revert is a new decision.
- The Authority Resolution Engine answers a question through the package's 11 ordered gates (permission → query context → lifecycle → scope → time → approval → lineage → evidence → conflict → citation → action) with exact citations, scope, truth type, as-of time, confidence, conflicts and approval — or it abstains (Needs a human decision) and opens a review. Live NovuHub data wins for "what is happening now" (projects, finance, sales, HR, sustainability, the deployed platform), read with your own permissions; plans, decks and trackers are listed as "not proof". Superseded material only in history mode; communications never leave their audience; a candidate never authorises an action; writes and external messages need an approved verdict and NovuHub's own approval (the engine never executes anything).
- Record ACLs are applied before anything is shown: a restricted record leaves no existence, title, snippet, count or conflict for someone who may not see it.
- AI as proposals only: ✨ AI proposal on a source (summary, truth type, scope, tags, claims with citations, relations, possible conflicts) — validated against the source's own passages and filed in the Review queue; ✨ Explain simply drafts a plain-language explanation of a verdict from its cited passages only.
- Advisory reach: a 📚 Sources chip on the main sections shows where their truth comes from, and My Assistant shows an SSOT card (answer, sources, warnings, "Why?") under its own answer. Nothing is changed automatically anywhere.
- Tabs: Ask, Registry (with lineage, decisions, hashes and the text of each source), Review queue, Lineage, Conflicts, Sources (connectors, the source-of-record matrix, upload of new material — registered, hashed and extracted, then waiting for a classification; Google Drive answers "not connected"), Policy, Audit (questions stored as a hash, decisions, verdicts), Tests (the package's 30 acceptance tests and 15 gold questions run on the server — all pass with the v1.0.0 package), Import.
- Removable: own code (
app/ssot/, 97h–97l), own tables (ssot_*, migration f7b9d1e3a5c6 — runs by itself on start), own API (/api/v1/knowledge), every hook in existing files marked SSOT-HOOK. Switch off with NOVUHUB_SSOT=0; remove completely with docs/ssot/REMOVE.md. - Functionality (Test section) assesses the area against Guru, Notion and Glean. Tests:
tests/test_ssot.py (a synthetic package — the real one is never in the repository); the migration head in two tests. New environment variable (optional): NOVUHUB_SSOT (default on).
2026-10-13 — v1.26.0: Business Plan — a new section for the business plan, to German criteria
- New section "Business Plan" in the sidebar, for owners, admins and managers only (members and the tax adviser do not see it; the server answers 404). The plans are kept outside the shared workspace data (their own store,
/api/bizplan/plans), with conflict detection when two people save at once; images go to file storage and are only served to managers, never as a public link. - The plan is a document of blocks — headings, text, note boxes, tables, charts, images, KPI tiles and page breaks — with a cover page, contents and chapter numbers. Every block has ✎ edit, ↻ refresh, ↶ undo (its own history, any earlier version can be restored), ✕ remove (restorable) and ⋯ more: move, duplicate, insert, turn a table into a chart (linked, or replacing it) and a chart into a table, a text into a note box, add an explanatory note.
- Tables calculate: formulas like
=B2+B3, =SUM(B2:B9), =B4*19%, across tables (=capital!B14), subtotal and total rows, rows and columns can be added, moved and typed (text, €, number, %), and cells pasted from Excel. Charts (bars, stacked, horizontal, line, area, pie, donut) read a table live or keep their own data; type, labels, series and colours can be changed. Texts can show live figures ({{capital!B14}}) that follow the tables. - German criteria: a template after the Handelskammer Hamburg forms (capital requirements, capital budget, profit forecast, liquidity plan), the jobcenter checklist (summary with key figures, team, market, 36 months by month, private living costs) and the chamber's statement checklist (6 months financed, wider economic benefit). The platform checks the plan at every change against a rule book with sources: 15 % own funds, financing covers the requirement, operating funds, 3-year profit forecast with taxes (Hamburg trade tax 470 %, corporate tax path to 2032), 36 months of liquidity that never turns negative (§ 15a InsO), UG reserve (§ 5a GmbHG), transparency register, social-insurance status of managing shareholders, KfW StartGeld and IFB InnoFounder facts, the outdated DtA programmes, placeholders, formula errors, charts without title or source, images without alt text. A score shows the required chapters and warnings.
- AI feedback on every change (with the workspace AI key): the AI reviews exactly the change against the rules of that chapter and may only cite rules from the rule book — the platform adds the source. It can also improve, shorten, update to the current figures or translate a text (as a proposal to accept). Current guidance from the web per chapter (chambers, KfW, IFB, laws), searched without any company data and cached for the month.
- German and English: each block has both languages; a change in one marks the other as "changed" with one-click translation. Print / PDF (wide tables on landscape pages where the browser supports it), JSON export and import.
- Functionality (Test section) assesses the new section against Gründerplattform, LivePlan and Upmetrics; two new suggestions (plan vs actual from Finance, pitch deck from the plan).
- New:
app/routes/bizplan.py, app/bizplan_rules.py, app/static/src/97c-bp-model.js, 97d-bp-checks.js, 97e-bp-view.js, 97f-bp-edit.js, 97g-bp-template.js (lazy chunk "bizplan"). Changed: 10-shell.js, 05-helpers.js, 15-dashboard.js (sidebar, access, router), app/routes/files.py (business-plan images), app/routes/plus.py (audit log records plan saves), app/platform_map.py, app/testlab_functionality.py, tools/build/bundle.py, novuhub-app.css. Tests: tests/test_bizplan.py. No migration, no new environment variables.
2026-10-12 — v1.25.0: 50 of the 51 Functionality suggestions built, the Lab's click findings fixed
- Each section has a tool bar at the top of its main screen (next to "From other sections") with the new tools of that section. Functionality now shows 50 of 51 suggestions built; the one left is the live bank connection (PSD2), which needs a licensed provider and is not built on purpose.
- Sales & CRM: a deal score (warm / cool, 0–100) with the next best step on every deal card and in the deal window; won deal → project with its tasks, quote → invoice, Customer 360° (money, tickets, projects, deals, contacts; an AI summary on request), a health column in the customer radar, e-mail → contact and deal, AI e-mail drafts and replies, and a capacity check before a deal is promised.
- Projects & Tasks: a project from one sentence, find tasks in plain words (and save it as a view), a status update posted to the project's chat, a risk forecast from how late similar work was, SOPs linked to tasks, chat message → task (☑ in Communication; the task shows as a card in the chat), an AI brainstorm in mind maps, a catch-up of any chat (decisions, questions to you, open questions — the rules run without AI).
- Finance: payment reminders in three steps with interest (§ 288 BGB: base rate + 9 points for businesses, + 5 for consumers; €40 flat fee; the base rate is a setting), bill approval with a limit (a bill over the limit cannot be marked paid until approved; over the project budget → automation), expense claims with receipts and German per diem (2026: €14 / €28, meals deducted), incoming e-invoices (XRechnung UBL / CII and ZUGFeRD PDF; files with DOCTYPE / ENTITY are refused), invoices built from approved, not yet invoiced hours, a forecast from pipeline, project billing and HR cost, and a tax adviser role (reads everything a manager reads, changes nothing; can ask questions that become tasks).
- HR: clock in / out with the legal breaks (ArbZG: 30 min after 6 h, 45 min after 9 h), overtime balance (CSV), salary bands and pay-transparency letters, document retention periods, e-signatures (drawn signature, SHA-256 of the file, certificate PDF), review evidence from the year's work, onboarding and offboarding projects, a skill gap → course, key results that count won deals or paid revenue, goals linked to projects.
- Learning & Docs: SOP review dates (a review task when due), where an SOP is used, contradictions between documents, a course from a document, knowledge articles from repeated tickets.
- Customer Service: ticket type, customer, project and invoice links, make a task, related articles (insert into the reply).
- Sustainability (the ESRS module itself is unchanged): IRO evidence from the other sections, Scope 3 from bills and travel with editable factors (German electricity mix 344 g CO₂/kWh, UBA 2025), the VSME basic report filled from the platform (PDF).
- Home: a status card on the Dashboard (whole company, a project or a team; three lines from tasks, projects, invoices, tickets and deals — never chat; an AI version on request), KPI tiles with a small trend line and adjustable size, Suggest my day (time blocks in the free hours around meetings, leave, holidays and lunch), a morning brief on My Day, an Inbox with filters (approvals, mentions, due work, snoozed).
- Automations: new triggers (ticket created, bug ticket, leave approved, bill over budget, form submitted) and actions (create project, ticket → task, reassign work during leave, notify the owner, ask the AI), with ready-made recipes.
- Platform: forms with conditions and routing and a public link (
/f/<token>, rate-limited), an audit log (sign-ins, changes, HR views, downloads, exports, settings; CSV; kept 365 days by default, a setting), sign out all sessions, calendar busy times from Google / Outlook (an ICS link, https only, times only — never titles) shown in Workload, Suggest my day and booking pages, and a useAI permission (off → the AI tools are refused). - My Assistant has four more actions after your approval: the next payment-reminder step, a new close date for a deal whose date has passed, cover for work while someone is on leave, and a draft IRO for ESG topics with evidence (complaints, energy bills, team size).
- Lab click findings fixed: "Add" buttons with an empty field now say what is missing instead of doing nothing; Calendar → Today and the invoice period "All time" / "Today" now visibly act; the fields the Lab found without a label have one. More tidy-up of messy data on load (lists of ids, text fields, dates).
- New:
app/routes/plus.py (/api/plus/…, /f/<token>), app/static/src/96d-plus-core.js and the lazy chunk "plus" (96e–96k), migration e6a8c0d2f4b5 (audit events — runs by itself on start), role adviser. Tests: tests/test_plus.py; updated tests/test_assistant_links.py, tests/test_testlab_qa.py, the migration head in two tests. No new environment variables.
2026-10-11 — v1.24.0: every section connected, messy data no longer breaks screens, My Assistant reads Communication
- "From other sections" on every business section. The main screen of Projects & Tasks, Sales & CRM, HR (reports), Finance, Learning & Docs, Communication and Customer Service starts with a folded panel: small cards with the other sections' records that matter there, and one-click actions that carry the work across. The folded line already counts the records each card uses, so a change in one section shows in the others at a glance. Examples: Sales — what each customer still owes, won deals without an invoice (Create invoice), customers waiting for support, deal owners away, a chat per key deal (Start deal chat), playbooks (Write a playbook from the won deals); Finance — won deals not invoiced, approved hours this month per project (Invoice), leave earned but not taken (value, totals only), billing tickets, energy and travel spend next to the E1 climate topics, a month-end checklist and Post the month-end status to #finance; Projects — a ticket or an ESRS topic becomes a task, lessons-learned documents for finished projects, invoiced vs budget, projects talked about in chat; HR — deals per person (leavers flagged), support load, overdue training, people in no team chat, ESRS S1; Learning & Docs — training per team, repeated customer questions (Write article), policies for material ESG topics (Draft policy); Communication — who is away, deals closing this week, new documents (Share here), urgent tickets, ESRS status (Post an update); Customer Service — what is at stake with the customer, their open invoices, tickets handed to the team (fixed → tell the customer), matching documents, support people away, Escalate in chat, ESRS S4 complaints.
- A card is shown only to people who may open the section its data comes from. Leave shows as "away" with dates only (never the type or note); messages are counted, not quoted. Sustainability is only read — its module is unchanged and nothing writes into the ESRS data from these panels.
- Connections: 28 of 28 pairs connected (was 7), 21 both ways — the 7 one-way pairs are the ones with Sustainability, which the others read. At run time every link is visible: the 24/7 Lab's check (a marked record is added, the reading section's screen must change) passes for all 47 links, including the three that were invisible before (leave and tasks in Finance, teams in Learning).
- My Assistant across the sections: reads dates and amounts written the German way (31.12.2026, 1.234,56), counts one customer written two ways as one, and reads Communication — chats waiting for your reply (a direct message or an @mention you have not answered for 2+ days), from activity only: who, where and since when, never the text. New actions after your approval: record a payment reminder on the overdue invoices, log a follow-up on a quiet deal, approve pending leave you are the approver of, send a first reply to waiting tickets, give an overdue course a new due date, give ESG topics without an owner their owner, and post a short reply in a waiting chat. The AI Assistant now reads and acts in all 8 sections. The Lab's Assistant cases: 100 % in every section and every kind of data (was: German dates and name variants missed, Communication not read).
- Messy data no longer breaks the platform (all 24/7 Lab crashes of 2026-09-30): the data is tidied when it is loaded — empty entries in lists removed, duplicate ids renumbered, lists that are not lists emptied, German amounts and dates converted, impossible dates ("0000-00-00") cleared, time stamps in date fields turned into the day, titles and names always text, an unknown task priority becomes Medium. Fixes: normalize, navCount, pendingTimeApprovals, myAttention, calendar ("undefined"), task board and timeline, messages list, knowledge base, portfolios (and their finish forecast with an impossible hour estimate), the chat call link, the activity list ("[object Object]"), "Invalid Date" on projects. Home → Open tasks counted empty entries as tasks (11 vs 12) — fixed. If loading ever fails, the platform still starts.
- 24/7 Lab: a screen that freezes the browser is now reported ("The page stopped responding", after 120 s without a sign of life) instead of stopping the Lab.
- New:
app/static/src/96c-links.js. Changed: 00-data.js (tidy-up on load), 15-dashboard.js (panel on each section's main screen), app/assistant_facts.py, app/assistant_scope.py, app/routes/assistant.py, app/testlab_connections.py, app/lab/engine.py, 79-assistant.js, 50-messages.js, 90-portfolios-workload.js, novuhub-app.css. Tests: tests/test_assistant_links.py; updated expectations in tests/test_lab.py, tests/test_testlab_qa.py, tests/test_assistant.py. No new settings, no migration.
2026-10-10 — v1.23.0: StartUp and Test are normal sections — the owner decides who gets them
- StartUp and Test are ordinary sections in the sidebar now, and access is given in the app: Settings → Access → Owner-only sections lists everyone in the workspace with a StartUp and a Test tick box. Only the workspace owner sees and changes it; administrators and managers do not get these two sections automatically and cannot give them to anyone (themselves included) — the server puts any such change back (
app/access_guard.owner_granted) and checks every request of both sections (app/section_access.py). The owner always has both. A person you tick sees the full section, every tab, after their next page load; untick to take it away. - Shared, not copied: the StartUp roadmap (start date, ticked steps) and the contact log with every organisation are one shared copy for everyone with access; the Test results (Calculations, Run QA, 24/7 Lab) likewise. What the owner already had is carried over automatically.
- Private stays private: each person's own StartUp situation — residence, unemployment benefit, university, e-mail signature (the profile that ranks the programmes) — stays theirs alone. Decision Intelligence is unchanged: the workspace owner only.
STARTUP_OWNER_EMAILS / TESTLAB_OWNER_EMAILS are no longer needed (they still work as a server-side extra list). Stored on the person's team entry as ownerGrants. Tests: tests/test_team_access.py; the Test and StartUp tests now use a workspace member who was not given the section. No migration.
2026-10-09 — v1.22.2: the 24/7 Lab's browser starts under the service user
- The hidden browser now gets its own home folder in the Lab folder (
instance/lab/browser-home): Chrome writes its profile, cache and crash reports under HOME, and the service user's home (/var/www) is not writable — Chrome stopped at start. It also starts without the sandbox, GPU and crash reporter, as on a server. sudo -u www-data venv/bin/python lab_worker.py --check-browser starts the browser exactly as the Lab does and says plainly whether it works, and why not.- The 24/7 Lab tab shows the reason next to "No browser" (not only on hover), with that check command.
2026-10-08 — v1.22.1: the 24/7 Lab works on Ubuntu 26.04
- Playwright cannot download its own Chromium for Ubuntu 26.04 yet ("does not support chromium on ubuntu26.04-x64"). The Lab can now use a browser installed on the server:
NOVUHUB_LAB_CHROME=/usr/bin/google-chrome in .env, or — without the setting — Google Chrome / Chromium found in the usual places when Playwright's own is missing. Steps in docs/deploy-notes/DEPLOY-v1.22.0-24-7-lab.md. Test: tests/test_lab.py.
- Test section → 🧠 24/7 Lab. A separate background service for the test server (
lab_worker.py, deploy/novuhub-lab.service) runs its own copy of the platform on its own database — the real data is never read or changed — and checks it day and night with a hidden browser: - Data for every section at four messiness levels (clean, normal, messy, chaos). A small generative model learns from the platform's own sample data and the Test Lab scenarios what each field holds (a date and how far from today, an amount and its range, a choice and how often each option occurs, a reference to another list) and samples new records from it. Messy data adds what real data brings: empty and missing fields, German dates and amounts, dates far in the past or future, negative and huge numbers, emoji / right-to-left / very long text, HTML in text fields, references to deleted records; chaos adds duplicate ids, empty entries in lists and values of the wrong type. Every change is recorded, so each failure can be traced to the data that caused it.
- What it checks: the calculations with known answers (Finance, HR, Sales — every seed, size and messiness); every screen and tab with the generated data (errors, "NaN"/"undefined", empty screens, HTML shown as HTML, response times; clicking one of each kind of button in part of the runs); the platform's own figures against independent calculations (the key metrics on Home, the counts on My Day — an amount written 1.234,56 counts as 1234.56); every connection the code has, at run time (a marked record is added, the reading section's screens must change); and the AI Assistant's accuracy on planted cases with known answers — in every section, positives it must flag and look-alikes it must not, each in six kinds of data. Free: only the rule-based analysis runs, no AI calls.
- What it learns: a Thompson-sampling bandit chooses the next data setting (settings that find new problems are chosen more often, every setting keeps a chance, old evidence fades); a logistic model per section learns which data raises the failure odds ("fails with German dates in invoices, odds ×8"); the same fault seen on many screens is grouped into one root cause with the data that triggers it and how to fix it; failure rates before and after a new version (newly broken / fixed, two-proportion test); response times against their usual range; a finding counts as fixed when the same check ran again 8 times on the same kind of data without it.
- The tab: running / paused, the current job, today's checks and pass rate, open / new / fixed findings, strengths and weaknesses per section (health 0–100 with a 30-day line), "Fix these first" root causes (+ Create a task), AI Assistant accuracy per section and per kind of data with how to improve it, new and fixed findings with how to reproduce them, what changed with the new version, connections at run time, development potential, all findings with filters, and where the Lab looks next. ⬇ Day report (PDF) — for any day. A day report is rebuilt every 10 minutes and closed at midnight.
- Proven with three planted defects in a copy of the code: a wrong key figure (Pipeline value counting won deals), a broken connection (deals no longer on the Projects calendar) and a wrong Assistant rule (overdue invoices only after 30 extra days). All three were reported in the first runs ("Home → Pipeline value shows a wrong figure: 698,000 worked out, 917,500 shown"; "Sales & CRM → Projects & Tasks: a new record in deals changes nothing"; "My Assistant misses overdue invoices with data as the platform stores it"); after the fix, as a new version, they were marked fixed.
- First findings on today's code: My Assistant misses overdue invoices, quiet deals, projects at risk and overdue trainings when dates are written the German way, and splits one customer written two ways; it does not read Communication; empty entries in a list (chaos data) stop most screens (normalize, navCount, pendingTimeApprovals); three code links show nothing on screen (tasks and leave in Finance, teams in Learning).
- Run QA, Connections and Functionality follow new sections by themselves. One map of the platform is read from the code each time (the sidebar in
10-shell.js, the screen router, the i18n names — app/platform_map.py): a section added to the sidebar is offered in Run QA (new screens start selected; its lists get the two-people sync check), becomes a new node in Connections with its pairs to every other section and the AI Assistant (its files and data lists are found from its screens), and appears in Functionality as "Not assessed yet" until its competitors are researched. Tested with a planted ninth section. - Functionality: Home (dashboard; My Day & Inbox) and Activity & Admin are now assessed too (researched 2026-09-29): 38 sub-sections — 5 ahead, 19 on par, 11 behind, 3 missing, 51 suggestions.
- Connections PDF and tab: the counts follow the number of sections (no fixed "8").
- New:
app/lab/ (engine, synth, assistant_eval, oracles, learn, report, store), app/platform_map.py, app/routes/testlab_lab.py, app/static/src/94f-lab.js, lab_worker.py, requirements-lab.txt (Playwright, Lab only), deploy/novuhub-lab.service, docs/deploy-notes/DEPLOY-v1.22.0-24-7-lab.md; qa-runner.js can load given data and reports screen timings and unescaped HTML. Tests: tests/test_lab.py, new tests in tests/test_testlab_qa.py. New environment variables (Lab only): NOVUHUB_LAB, NOVUHUB_LAB_DIR, NOVUHUB_LAB_PACE, NOVUHUB_LAB_SCREENS, NOVUHUB_LAB_TZ. No migration.
2026-10-06 — v1.21.0: QA report as PDF; Connections and Functionality tabs in the Test section
- Run QA → ⬇ Report (PDF) (the Excel stays next to it). Every finding has an ID (QA-001 …), its severity and check, the section and screen, what went wrong, where it is and "Go to: screen › tab › click “button”", and the technical detail — send the PDF and name the IDs to have them fixed. Then the saving & sync checks, the windows inventory (fields, buttons, what happened empty / filled / on Escape) and the AI suggestions if they were asked for.
- Test section → 🕸 Connections. Which section really uses which section's data — measured from the platform's own code each time (every link names the file, function and line: "reads invoices", "writes tasks"), never from workspace data. The web: the AI Assistant in the middle and the 8 sections around it (Projects & Tasks, Sales & CRM, HR, Finance, Sustainability, Learning & Docs, Communication, Customer Service); green = both ways, blue = one way, grey = only the people list, red dotted = not connected. Click a section to see only its links, click a pair for the evidence and an idea for the missing flow. Per section a score out of 8 (7 sections + the AI Assistant), a matrix of all 28 pairs, and the AI Assistant per section: does it read the data, can it act there. ⬇ Connections (PDF). Home, Risk Radar and Settings read many sections but are views, so they are listed separately and not counted.
- Today's measurement (v1.21.0): 7 of 28 pairs connected (3 both ways), 4 only through the people list, 17 not connected; the AI Assistant reads 7 of 8 sections (not Communication) and acts in 1 (Projects & Tasks); no section has all 8 yet — Projects & Tasks 5/8, HR 4/8, Finance 3/8, Learning & Docs 3/8, Sales 2/8, Customer Service 2/8, Sustainability 1/8, Communication 1/8. Every missing pair has a concrete idea (e.g. Finance ↔ Customer Service: billing questions and refunds in tickets linked to the invoice, open invoices visible to the support agent).
- Test section → 📊 Functionality. All 35 sub-sections held against their strongest competitors (Asana, monday.com, ClickUp, HubSpot, Pipedrive, Personio, BambooHR, DATEV, sevDesk, Sweep, Normative, Zendesk, Freshdesk, Notion, Slack, Teams …; researched 2026-09-29 from their own pages, links at every item). Per sub-section: what we have (checked in the code each time — an item that disappears is marked), what competitors offer, a verdict (ahead / on par / behind / missing) and suggestions with how — the logic and the data flow, which sections it connects, effort and priority. "Do these first" ranks them (priority, then most sections connected, then least effort). A suggestion turns to "built" by itself once its code exists. ⬇ Functionality (PDF).
- Today's result: 5 ahead, 18 on par, 9 behind, 3 missing (Expenses & receipts, Carbon accounting Scope 1–3, VSME / customer questionnaires), 44 suggestions. First five: a 360° customer page, the AI Assistant acting in every section, an AI step inside automations, cross-section triggers, onboarding as a real project.
- New:
app/testlab_connections.py, app/testlab_functionality.py, app/testlab_reports.py (the three PDFs), app/static/src/94e-insights.js (testlab chunk); routes /api/testlab/qa/report.pdf, /api/testlab/connections(.pdf), /api/testlab/functionality(.pdf) — owner-only like the rest of the Test section (404 for everyone else). Tests: the PDFs, access, and a scanner test that plants a new link in a copy of the code and expects it to be found. No migration, no new environment variables.
- Test section → 🤖 Run QA (private, next to the Test Lab calculations). Choose the sections and screens to check (all 11 sections, 30 screens — a project's workspace and a person's profile included), which checks to run, and Quick or Full depth, then press ▶ Run QA. The robot runs the real platform on sample data in the Test Lab sandbox: nothing is saved to the workspace and nothing leaves it (downloads, links, e-mails, file pickers and pop-up windows are caught).
- Buttons & tabs: every screen and every tab (and the tabs a tab opens) is opened and one of each kind of button, card, link, checkbox and menu is clicked — "kind" = the handler the build gives it, so 100 task cards are one kind. After every click the data is put back. Reported: an error when opening a screen or clicking, a button that does nothing (it first tries another of its kind, and the field next to it filled in — an "Add" that silently ignores an empty name is its own finding), "undefined" / "NaN" / "[object Object]" / "Invalid Date" on screen, an empty screen, a page that reloads itself or stops responding (the robot restarts after that button).
- Windows (pop-ups): every window that opens is listed with its fields and buttons, and checked: no title, fields without a name, the same field twice, no Close / Cancel, too tall to use; Save with the fields empty (does it save a record without a name?), Save when filled in (does anything happen? an error?), Escape. 🤖 Which fields are needed? (AI) — optional, with the workspace AI key: a reviewer reads only the field names and button labels (never data) and says per window what to keep, what is probably not needed and what is missing.
- Saving & sync: two copies of the platform (person A and person B) run the real sync engine against a stand-in server that follows the real server's rules, and work at the same moment: add, change and delete in the same list (tasks, projects, mind maps, portfolios, workflows, contacts, deals, leave, teams, invoices, documents, courses …), two messages in the same chat and ticket, two settings, a profile photo while the teammate keeps saving, two projects made in the mind map minutes apart, a task made in the Calendar (must show in My tasks and All tasks), a save still on its way, a dropped connection. Nothing may be lost — on the server or for either person.
- Screens (phone width: page scrolls sideways; text that is hard to read, light and dark mode) and Accessibility (buttons and fields a screen reader can name, the same id twice).
- Results: high / medium / low / ideas, filters by section and check, "How to get there" for every finding, 👁 Show me (opens that spot and marks the button), + Create a task (or tasks for all high findings, into a project you pick), ⬇ Report (Excel) with Findings, Windows, Saving & sync and the AI review; earlier runs are kept.
- Proven with three planted defects (a button that throws an error, a window that saves a project without a name, and the old two-way sync merge that lost photos and mind-map projects): all three were found every time; without them the same run finds none of them. A full run of all 30 screens with every check takes about 14 minutes (quick): 142 screen/tab states, 422 kinds of buttons, 135 windows, 22 of 22 sync checks passed — 0 high and 0 medium findings; the low ones left are small things to polish (e.g. “Add” in Invoices silently ignoring an empty name).
- Fixed from the first full run: white initials on light colours in the CRM contact list and on the portfolio roadmap bars (contrast 2.8:1, now ≥ 4.6:1); the same element id twice on Settings → Job description; switches and arrows without a name for screen readers (named after their row), filters and fields without a label (named after their title, placeholder or first choice).
- New:
app/routes/testlab_qa.py, app/static/src/94d-qa.js, app/static/testlab/qa-runner.js, app/static/testlab/qa-server.js; the sandbox page has two new modes (qa=run, qa=a|b); window.__whSyncState() (read-only) in wh-shell.js. Tests: tests/test_testlab_qa.py, tests/frontend/qa-server.test.js. No migration, no new environment variables (the AI review uses the existing workspace AI key).
2026-10-04 — v1.19.0: Saves are never lost; Projects & Tasks rebuilt project-first
- Lost changes fixed (mind-map projects vanishing, team photos disappearing after a few minutes). When two people (or two tabs) worked at the same time, the app merged their copies in a way that could throw one side away: a teammate's new profile photo was reverted by someone else's unrelated edit to the team list, and a project/mind map made in one tab was deleted by a pending change in another. The merge is now three-way — it compares both copies with the last version they shared, so only what each person actually changed is applied: additions and deletions on both sides are kept, an edit beats a delete, and only a real clash (the same field changed by both) is decided in favour of the person saving. A change made while a save was on its way is no longer marked as saved; failed saves are retried (2 s up to 60 s) and after three failures the app says so; closing the tab with unsaved changes asks first. Checked with two tabs, two users, a slow network and an offline spell.
- A member's save no longer writes a false "tampering" warning to the server log on every save (the owner-only Decision Intelligence part is put back quietly, as the full save already did).
- Projects & Tasks, project-first. The section opens on Projects: a health overview (active projects, on track, at risk, off track, overdue tasks, milestones in the next 14 days) and every project as a card — status, progress against time used, owner, due date, next milestone, team, and its health with the reason (e.g. "1 task overdue · behind plan"). Filter by active / needs attention / done, by team, sort by health, due date, progress or name. Tabs: Projects · My tasks · All tasks · Mind maps.
- Each project has its own workspace: Overview (health and reasons, milestones, sections, team, what needs attention, what is coming up), Board (by status or by section, drag between columns), List (grouped by section, everything editable in place), Timeline (the Gantt, grouped by section, with dependencies and milestones), Mind map (the project's map, inside the project; created with one click) and Time & money (the former project page).
- Quick add everywhere: type "Send offer @anna friday !high #sales" and press Enter — people (@name), dates (today, tomorrow, weekday, "in 3 days", "next week", 15.11., 2026-11-15), priority (!high / !low), tags (#tag) and milestones (!ms) are understood. Every board column and every section has its own box.
- Inline editing: in every list the title, project, section, people, priority, status and due date change right in the row; the round tick completes a task (a blocked one asks first). All tasks → List also has bulk move to project.
- Sections and milestones: a project can be split into sections (add, rename, reorder, delete — the tasks stay); milestones show on the Overview, the board, the list and the timeline, with what they are waiting on.
- One place for all work: the Calendar, the mind map, templates, intake forms and the task form all create the same tasks. A new task goes into the project you are looking at; from the Calendar or elsewhere it starts without a project instead of silently going into the first one — and tasks without a project are always visible (My tasks, All tasks → No project, and a card on the Projects home). Deleting a project now really leaves its tasks without a project (before they moved into the first project).
- Tests: three-way merge unit tests, the quick-add parser, project health, the new default project, a member delta save; the Projects home and workspace boot in the built bundle. New visual baselines: projects home, project overview, project list. Test Lab: all 258 checks pass. No migration, no new environment variables.
2026-10-03 — v1.18.0: New sign-in page — the platform as a neural network
- The sign-in page shows NovuMetrics as a living network (designed from the owner's sketch): the five sections — Projects & Tasks, Sales, Finance, HR, Sustainability — each with its own sub-sections (e.g. Pipeline, Contacts, Customer radar, Quotas; Invoices, Bank, P&L, Forecast), all wired to a larger centre node, the AI Assistant. The nodes stay where they are; only small bright points of light move: data enters a sub-section, runs along its path to its section and on into the AI Assistant, which ripples as it arrives. Connections brighten with their traffic. NovuMetrics colours, one colour per section; labels in English or German with the page language.
- Layout: network on the left, the sign-in card on the right (as in the sketch); on phones the network is a banner above the card. The network always fits its area. With "reduce motion" it is a still picture. One
<canvas>, no library, no network calls; the form itself is unchanged. - The other sign-in pages (create account, forgot password, two-step code) keep the current background.
- New:
app/templates/_auth_neural.html; styles .auth-nn / .nn-* in app/static/css/style.css. Visual baselines updated for the calendar, directory and HR reports (the v1.16/v1.17 changes).
2026-10-03 — v1.17.0: Weekends and public holidays in the calendars
- Both calendars now show weekends and public holidays: Projects & Tasks → Calendar (month, week and day) and HR → Time & Leave → My calendar. Weekends (every day that is not a working day in Settings) are hatched grey; a public holiday is tinted red with its name on the day (English, or German when the workspace language is German). A legend says which state's holidays are shown. The Gantt chart marks holidays too. Any year works (Easter is calculated), so the calendars are correct for this year, next year and beyond.
- Settings → Working hours → Public holidays: the federal state (default Hamburg), Bayern with Mariä Himmelfahrt for Catholic municipalities, national holidays only, or none; the next three holidays are shown. Included: the 9 nationwide holidays and each state's own (Heilige Drei Könige, Frauentag in Berlin since 2019 and Mecklenburg-Vorpommern since 2023, Fronleichnam, Mariä Himmelfahrt in Saarland, Weltkindertag in Thuringia since 2019, Reformationstag — nationwide in 2017, in the northern states since 2018 —, Allerheiligen, Buß- und Bettag in Saxony, Berlin's one-off 8 May 2020 and 2025). Holidays of single towns (Fronleichnam in parts of Saxony and Thuringia, the Augsburger Friedensfest) are not included.
- A public holiday is a day off in the numbers: leave requests (Thursday before Easter to the Tuesday after = 2 vacation days, not 4), vacation left and carry-over, sick days, the working-hours target, the absence report and sick rate, the leave-liability pro rata, and the month summary in My calendar (which now counts working days only). The after-hours e-mail triage treats a holiday like a weekend.
- One setting: My Assistant's German deadline rules (§ 108 AO) now use the same state; its own Federal state choice was replaced by a pointer to the new setting.
- Test Lab: each seed picks a federal state, so all 17 choices are tested; a new planted problem (vacation over Easter) and 4 new checks (holidays highlighted over 24 months in both calendars, weekend days this month in both). A messy run of everything: 258–259 checks, 45 planted problems; all pass for every state and with the browser clock on 24 December, 1 January and Easter Monday.
- Tests: new
tests/frontend/holidays.test.js (5, against the official 2026/2027 lists) and a check that the calendar code, the assistant and the Test Lab give the same holidays for every state, 2015–2040. No migration, no new environment variables.
2026-10-02 — v1.16.0: Test Lab Phases 3 + 4 (HR, Sales & CRM) and the fixes they found
- Test Lab: choose the area — Finance, HR, Sales & CRM, or Everything. The same seed, size and messiness always give the same data; adding HR or Sales never changes the Finance data or answers. With everything, a messy run has about 255 checks and 44 planted problems.
- HR (46 checks, 14 planted problems): a team of 19–50 people in three teams plus leadership (full-time, part-time, intern, contractor, joiners, leavers, an override FTE), a year and a half of leave requests and weekly time entries. Checked: HR Overview tiles, headcount and FTE by department, FTE detail, trend and turnover, working hours (worked, target, utilisation, pending), people out today, the statutory reports (absence and sick rate, untaken leave, headcount bridge, turnover rate, §267(5) HGB average, gender pay gap), Time & Leave → My balance, a profile's leave panel, Directory and Organization counts.
- Sales & CRM (42–45 checks, 8 planted problems): contacts, deals and quotas in the standard pipeline, and a two-year sales history with one customer for each radar situation (steady, drifting, lost, seasonal, stopped a product, shrinking, price leak, new). Checked: pipeline tiles, every board column, win rate, each rep's open pipeline / won / quota attainment, duplicate contacts, close-date reminders, a contact's open deals, and the radar's revenue, customers, value at risk, leakage, call list and each customer's status. In the sandbox the radar is answered by
/api/testlab/sales/radar: the platform's real analysis over the planted history (nothing is stored). - Evidence: the Excel file now also has People, Leave, Time, Deals, Stages and Sales history sheets; 71 answers are live formulas (44 before). The PDF names the area.
- Fixes the Test Lab found (on the first messy run of the old code 41 checks failed and 2 could not be measured; all pass now, also with the browser clock set to 1 and 3 January, 1 March, 30 June, 2 November and 31 December):
- Leave is counted in working days (Settings → working days; Mon–Fri by default) — a Friday-to-Monday vacation is 2 days, not 4; a day covered by two requests counts once; a request over New Year counts in each year separately. Vacation days left, Sick days taken (now "this year", before all-time), the profile's leave panel, the absence report and the leave list all use this.
- Carry-over = last year's unused days, at most the policy's carry-over (before: always the maximum 5). It is shown in My balance, the profile and the leave-liability table, which now includes it.
- Working hours used Mon–Fri and ÷ 5 regardless of the company's working days; now the set working days.
- As of today: a start date, end date or time entry after today no longer counts — Joined / Left this year, the trend's current month, turnover, the headcount bridge and the reports (the current month ends today), Logged 30d.
- Weekly hours typed with a decimal comma ("32,5") gave the part-time default FTE 0.5; now 0.81.
- An end date before the start date (a typing error) made an active person disappear; it is ignored and marked End before start.
- The same person entered twice (same e-mail) counted twice in headcount, FTE, joiners and the reports; counted once now and marked Possible duplicate.
- A team lead with no team on their own record was Unassigned in HR and missing from the Directory count; they belong to the team they lead (Directory = Organization).
- Out today lists each person once.
- Lost deals: every pipeline now has a Lost stage (added to existing pipelines); lost deals are not open, not in the forecast, the dashboard or the nav badge, and get no close-date reminder; the win rate uses them.
- A deal amount typed as text ("12.500,00") was glued together as text in the board totals and a contact's open deals; amounts are numbers now.
- A deal in a stage that no longer exists vanished from the board while still counted elsewhere; it moves to the first stage.
- Quota attainment counted deals won in earlier years; now this year (the won date, recorded from now on, else the close date).
- Duplicate contacts: the same name without an e-mail was not found; contacts are matched by e-mail or name.
- Won and lost deals no longer show a red overdue close date.
- Customer radar: a seasonal-quiet customer was counted in revenue at risk; the label Customers who ordered this year now says in the last 12 months (what it counts).
- Bank import: a closing line such as Kontostand am 03.01.2027 was read as a booking dated 1 March; text that only mentions a date is no longer a date, and lines without an amount are skipped.
- The public lead form now uses the workspace's own first pipeline stage.
- New:
app/testlab_hr.py, app/testlab_sales.py. Config TESTLAB_ANY_DAY (default off; automated tests with a fixed browser clock only). No migration, no new environment variables. Tests: tests/test_testlab.py now 12, new tests/frontend/hr-crm-testlab-fixes.test.js (7).
2026-10-01 — v1.15.0: Test Lab (Phase 1 + 2: Finance) and the Finance fixes it found
- New private section Test → Test Lab (only for the e-mail in
TESTLAB_OWNER_EMAILS, default the platform owner; everyone else gets 404). It generates a complete set of made-up Finance data — invoices with payments, receivables, bills, bank lines, 13–14 months of accounts, a budget and bank/account files to import — with a seed, a size (small / normal / large) and a messiness level (clean / normal / messy). The correct answer for each figure (126–164 checks) is worked out on the server in Python from the planted facts; the real platform runs the same data in a sandbox and the figures it shows (tiles, tables, totals — or its own calculation where a tab has no single figure) are compared. Results: pass / fail / not measured per check, with the rule behind each answer; 22 planted problems with what the platform should do and whether it did; a history of runs. - The sandbox never touches the workspace: an iframe that loads the real app bundle without the sync shell, with browser storage replaced by an in-memory store before any app code runs, and a network filter that only lets static files and the three stateless "read this file" endpoints through (everything else is answered locally and listed). If the storage cannot be isolated, it does not start. The browser never receives the expected answers.
- Evidence: Evidence (Excel with formulas) — the planted data on its own sheets and, for 44 of the figures, the correct answer as a live formula (SUMIFS …) next to what the platform showed, the difference and Pass/Fail; Report (PDF) — summary, planted problems, every check.
- Finance fixes the Test Lab found (44 of 161 checks failed on the first messy run; all pass now):
- An invoice saved without stored totals (older data) showed as paid; its total is now worked out from its lines everywhere (status, balance, journal).
- A negative discount raised the invoice total and the journal; it is ignored now.
- Two payments without an id overwrote each other in the journal; each is posted.
- Dates typed or imported the German way (15.03.2026) were not understood (overdue, aging, VAT period, burn rate); they are converted to ISO dates when the data loads.
- A/R aging listed one customer twice when the name differed in capitals or spaces; one row now.
- A customer's outstanding balance counted an invoice twice when it was also pushed to Receivables.
- Consolidation showed EUR and € as two currencies; one row now.
- Cash flow: with no start date and a manual investing/financing line, closing cash ≠ opening + net; closing is now always opening + net.
- P&L: an account whose line was deleted vanished from the statement; it is listed under Not placed in your P&L.
- Forecast: a missing month in the history is now pointed out (the methods treat months as consecutive).
- Marks for data problems: Duplicate number on invoices sharing a number; Possible duplicate on bills and on bank lines imported twice.
- Bank CSV import now reads German exports (info lines before the header, Buchungstag / Verwendungszweck / Soll / Haben, 1.234,56 and 1.200, minus at the end, typographic minus) and skips closing-balance lines.
- Account-file (P&L) import: 12.000 in a German file is twelve thousand; 150,00- and −35,00 are negative; heading rows without an amount are not accounts; a Balance column is read; English Total … / Gross profit / Net profit rows are set aside; revenue written as credit (negative) is turned positive for every file type; depreciation, interest, tax and cost of sales inside a cost group keep their own P&L line (EBITDA stays right).
- The Sales demo button of the unreleased v1.14.1 draft was removed on request.
- New:
app/testlab_finance.py, app/testlab_files.py, app/testlab_evidence.py, app/routes/testlab.py, app/templates/testlab_sandbox.html, app/static/testlab/{shim,boot,runner}.js, app/static/src/94c-testlab.js (lazy chunk testlab). Environment: TESTLAB_OWNER_EMAILS (optional; falls back to STARTUP_OWNER_EMAILS, then ANALYSIS_OWNER_EMAILS). No migration. Tests: tests/test_testlab.py (7), tests/frontend/finance-testlab-fixes.test.js (6).
2026-09-30 — v1.14.1: Customer radar — fewer false alarms
- Fewer false alarms (found by testing on made-up sales data with known answers). Shrinking now also needs the last 90 days to be below the customer's own normal range (mean − 1.5 standard deviations of the seven 90-day windows before) and the last 6 months to be 15% below the same 6 months last year; before, normal ups and downs flagged about one steady customer in four. Stopped a product now needs a group bought often enough that 2.5+ orders were expected in the last 6 months, and it is checked before shrinking because it names the reason. Seasonal now looks at the pause around the same date last year. On the made-up data: drifting, lost, stopped product, price leaks and seasonal are all found; shrinking is found for 6 of 10 cases; about 1 steady customer in 90 is flagged as shrinking.
- The generator of that made-up sales data (
app/sales_demo.py) is used by the tests only; there is no demo button. - Fix: a My Assistant follow-up test compared the server's UTC date with the person's local date and failed close to midnight; it now uses the person's day, as the app does.
- No migration, no new settings. Tests:
tests/test_sales_radar.py now 9.
2026-09-30 — v1.14.0: Sales & CRM → Customer radar (drift and margin radar)
- New view Sales & CRM → 📡 Customer radar. It learns each customer's own buying rhythm from the sales history and points at what already happened: drifting (a customer with 4+ orders in 24 months whose usual gap between orders is clearly overdue — more than 1.5× the median gap and at least 2 weeks late), lost (more than 3× the gap and over 180 days), shrinking (last 90 days at least 30% below the same 90 days last year), stopped a product (still ordering, but no longer buys a product group it bought at least twice in the 6–24 months before) and price leaks (lines more than 5% below the list price or, without one, below the product's median price across customers; lines below cost). A quiet spell that also happened in the same weeks last year is shown as seasonal and kept off the call list. "At risk" = what the customer bought in the 12 months before their last order. Nothing is a forecast.
- Call list — most valuable first, each with the reason in words, the CRM contact, a draft e-mail (English or German, from templates; nothing is sent), and Contacted (with what was learned), + Task (a task for the person, due in 2 days), Snooze (7–60 days) and Dismiss (with a reason; not raised again for the same situation). A signal closes by itself when the customer orders again.
- Customers (search, last order, usual gap, orders, revenue 12 months and trend, margin, status) with a Customer 360 window: key figures, 24-month revenue chart, signals, CRM contact, unpaid invoices, deals, radar history, products and the latest lines. Products: ABC classes, trend, quantity, customers, margin. Margin leaks: customer × product with typical discount, below-cost amount and the gap.
- Did it help? A share of the flags (default 20%, 0–30% in the settings) is held back on purpose as a comparison group. The tab compares both groups — ordered again within 60 / 90 days and revenue per customer in the 90 days after the signal — and says "too early" until each group has at least 5 signals that are 60+ days old. A held-back customer can be released to the call list; released ones leave the comparison.
- Data: import sales history as CSV or Excel (up to 12 MB / 200,000 rows per file, 300,000 lines per workspace). German and English headers are recognised (Rechnungsdatum, Kunde, Artikel, Warengruppe, Menge, Umsatz, EK, Listenpreis, Verkäufer …), ";" or "," files, 1.234,56 or 1,234.56 numbers, 31.12.2025 / 2025-12-31 / Excel dates; the person confirms the column mapping before anything is stored. Imports can be deleted. Invoices written in NovuMetrics are used as net sales lines unless switched off.
- Who sees what: everyone with Sales & CRM access sees the radar; importing, deleting imports and the settings are for managers; margin figures and price leaks only for managers and people with Finance access.
- New:
app/sales_radar.py (analysis), app/sales_import.py (file reading), app/routes/sales_radar.py (/api/sales/radar, /customer, /import, /import/<id>/delete, /settings, /action), app/static/src/89-sales-radar.js (lazy chunk salesradar), styles .sr-*. Migration d5f7b9c1e3a4 (tables sales_import, sales_line, sales_contact; runs on boot). Settings per workspace in AppState salesradar:ws<id>. No new environment variables. Tests: tests/test_sales_radar.py (8).
2026-09-29 — v1.13.0: HR Reports → Working hours; the Team page is removed
- New tab HR Reports → Working hours with three levels. Company: hours worked, target hours, utilisation, balance, pending approval and hours per person per week, a monthly chart of worked vs. target, and the teams at a glance. Teams: people, FTE, target, worked, pending, absence days, utilisation, balance and hours per person per week for Leadership, every org team and Unassigned, with a company total; click a team to see its people. Employees: contract hours per week, work days, absence days, target, worked, pending, utilisation, balance and the last logged day, filtered by team; click a name to open the person.
- How it is counted: worked = approved time logs dated in the period (tasks, projects and general time); pending = logged but not approved yet; target = contracted weekly hours ÷ 5 for every working day (Mon–Fri) the person was employed, minus approved leave (public holidays are not deducted). It uses the HR period picker, and Excel / PDF export follows the level shown; the full HR export includes it.
- The Team page under HR is removed. It showed the same people as the Directory, which keeps editing, inviting and the team grouping. Old links, bookmarks and the keyboard shortcut 6 now open the Directory; the Team switch is gone from Settings → Modules; links in the statutory reports point to FTE detail and the Directory.
- No migration, no new settings.
2026-09-28 — v1.12.0: StartUp — a private founder section for one person
- New section StartUp (sidebar, rocket icon) with six tabs: Overview, Roadmap, Contacts, Emails, Documents and Your situation. It holds 30 Hamburg and federal contacts for founding NovuMetrics — advice centres, grants, loans, investor networks, programmes and events — with what they offer, the money or cost, their conditions, what to send, the official e-mail and phone (checked 27–28 Sep 2026) and the source.
- Ranked by their conditions and yours. "Your situation" (residence status, unemployment benefit and its end date, university status, full-time, founded or not, planned start) is compared with each organisation's rules: every contact shows ✓ / ✗ / ? reasons and a status (Start here, Ready, Check first, After founding, Later, Not eligible). Warnings on the Overview cover the residence question, the 150-day rule of the Gründungszuschuss and EXIST eligibility.
- Roadmap that starts on a button. 26 steps in four phases over about 120 days; nothing is dated until Start my roadmap is pressed, then every step gets a due date from that day, with ticks, overdue / this-week states and a reset. Fixed dates (events, deadlines) are listed beside it.
- E-mails in English and German for each contact, written from the founder's answers (name, single founder, founded or not), with copy buttons and Mark as sent; a status per contact (sent, replied, meeting, applied, approved …) is kept.
- Private to one person. Shown only when the signed-in person's own e-mail is in
STARTUP_OWNER_EMAILS (defaults to ANALYSIS_OWNER_EMAILS, i.e. the platform owner) — per person, not per workspace, so even admins of the same workspace never see it; every /api/startup call answers 404 to anyone else. The founder's answers are stored per person on the server (AppState startup:u<id>), never in the shared workspace data, and never in the front-end code. - New:
app/routes/startup.py, app/static/src/94b-startup.js (lazy chunk startup), styles in novuhub-app.css. Environment: STARTUP_OWNER_EMAILS (optional). No migration. Tests: tests/test_startup.py (4).
2026-09-25 — v1.11.0: one sign-in per account at a time
- First sign-in wins. While an account is signed in and in use on one device, signing in with the same e-mail and password anywhere else is refused: the new browser sees This account is already signed in and is not let in; the session in use keeps working. Applies to every account, including sign-ins with two-factor (checked before the code is asked) and SSO.
- Why a refused sign-in matters: a correct password from a second device means someone else may know it, so the account owner gets the e-mail A sign-in to your NovuHub account was refused (at most once per 15 minutes) with the advice to change the password.
- How the account becomes free again: signing out on the other device; the other device being unused for 30 minutes (closed browser, sleep —
NOVUHUB_SESSION_IDLE_MIN); the button E-mail me a link to end the other session on the refusal page (a one-time link to the account's own mailbox, valid 30 minutes — proves the mailbox, not just the password, so someone who only has the password cannot throw the owner out); or a password reset by e-mail. - An open NovuHub tab counts as in use (it talks to the server every 20–25 seconds). A tab whose session was ended goes to the sign-in page with Your session has ended. Settings → Security shows One sign-in at a time — on.
- Sessions that were open before the upgrade keep working: the first one to make a request holds the account. API tokens (connectors, the public API) are not sessions and are not affected.
- New:
app/single_session.py, template login_busy.html, routes /login/busy and /session/end/<token>, migration c4e6a8b0d2f1 (user.active_sid, user.active_seen_at; also self-healed at boot). Environment: NOVUHUB_SINGLE_SESSION (default on), NOVUHUB_SESSION_IDLE_MIN (default 30). Tests: tests/test_single_session.py (12); tests that deliberately use several browsers per account switch the rule off for themselves. Full suite 458 passed.
2026-09-25 — v1.10.2: change your password in Settings
- Settings → Security → Password → Change password: enter the current password and the new one twice. The new password follows the sign-up rules (at least 12 characters, not a common password, no keyboard sequence, not the name or e-mail address); the form checks length and match before sending and shows the server's reason otherwise.
- The current password is required, and wrong guesses count against the same throttle as sign-in (8 per 15 minutes per address), so an open session cannot be used to take the account over or to guess the password.
- After the change every other device is signed out (the session is bound to the password), this browser stays signed in, and the account owner gets an e-mail "Your NovuHub password was changed" with what to do if it wasn't them.
- New endpoint
POST /api/account/password {current, password, password2}; GET /api/account/security reports passwordMinLength. German UI strings added. No migration, no new environment variables. Tests: tests/test_change_password.py (5).
2026-09-25 — v1.10.1: fixes from the v16 assessment
- Weekly review PDF works on real data: a workspace whose cash runs short made
/api/assistant/review.pdf and review.html fail (500) — the shortfall week is a date, the builder read it as a record. Pinned by a test; a 216-run robustness sweep (tools/assessment/v16/assistant_fuzz.py) covers empty workspaces, missing P&L, text in number fields, broken dates and deep shortfalls. - AI models current: account categorisation and invoice reading still called
claude-3-5-haiku-latest, retired by the provider on 19 Feb 2026 (every call failed and fell back silently). Now claude-haiku-4-5-20251001 (env NOVUHUB_AI_FAST_MODEL); the Assistant uses claude-sonnet-5 (env NOVUHUB_ASSISTANT_MODEL) — the previous id retires no sooner than 29 Sep 2026. - P&L template placement: accounts a company template does not quote go to its catch-all line (flagged for review) instead of a line guessed from words — fee accounts classed as commissions had landed on Personnel and overstated personnel cost in the HR reports (v1.5.0 regression). Commissions are their own word family; a guess never lands on the personnel line.
- Cash forecast: P&L months that ended more than 120 days ago no longer fill the 13-week forecast (a January P&L had projected January's loss onto every week); the forecast says so and lowers its confidence.
- My Assistant tabs follow access, not today's findings: Sales, Service, Anomalies … stay visible with "All clear — checked: …" when empty. Money in charts and cards is locale-formatted (1,5M in German).
- Privacy policy and DPA describe the AI features — what each sends to the AI provider, when the provider is a sub-processor (installation-wide key) and when the customer's own contract applies (own key). The privacy version is now 2026-09-25, so members accept it again on next sign-in.
- Assessment v16 (
tools/assessment/v16/, report docs/assessments/WorkHub-SaaS-Readiness-Assessment-v16.pdf).
2026-09-25 — v1.10.0: My Assistant knows German rules, writes the weekly review, runs each role's routine
- German rules (on automatically for a German workspace — German language, an address in Germany, or euro invoices at 19 % / 7 % — or by hand; Settings → My Assistant → Germany): a tax & payroll calendar — Umsatzsteuer-Voranmeldung on the 10th (monthly / quarterly / annual from last year's VAT, or as set; Dauerfristverlängerung and the Sondervorauszahlung on 10 February), Lohnsteuer-Anmeldung on the 10th, the social security report (before 0:00 of the fifth-last bank working day) and contributions (third-last) — moved to the next working day by weekends and the federal state's holidays (§ 108 AO), with a card two weeks ahead (owner, task, ELSTER / DATEV steps, § 240 / § 152 AO) and an alert when it is due within two days. VAT is estimated from invoices (exact tax) and receivables / bills (the VAT rate), Soll or Ist, and enters the 13-week cash forecast unless a tax bill is already in Payables.
- Late payment law on overdue cards: in default since (the safe date, 30 days after due date and receipt, § 286 Abs. 3 BGB — and the date if the payment date was agreed), default interest (base rate + 9 points B2B, + 5 consumers; Basiszinssatz 1.52 % since 1 July 2026, overridable), the €40 flat fee per invoice in default (§ 288 Abs. 5). Interest is only claimed from the safe date. Businesses vs consumers from the legal form and the address. German reminders — Zahlungserinnerung, Mahnung, Letzte Mahnung (with § 288 and "at the latest when this reminder arrives" where default has not started) — first for German clients, with a Deutsch / English switch that keeps your edits; the court dunning procedure is pointed to once a client is long in default.
- Skonto card when an early-payment discount runs out within 5 days, with its yearly worth; the payment plan shows it too. Month-end close checks on days 1–12 (bank matched, bill documents, invoice numbers without gaps or doubles, P&L imported and mapped, then DATEV). E-invoicing card (from 2027 above €800,000 turnover, 2028 for all).
- Weekly management review (PDF) for managers: key figures, 13-week cash chart, what needs attention with owner and status, results of the week and 90 days, items waiting over a week, follow-ups, deadlines, workload, the plan — branded, from the same checked figures (
GET /api/assistant/review.pdf, printable review.html as fallback; managers only). The Monday brief links it (/?go=assistant&t=review). - Routine tab — role playbooks for My work, Finance, Sales & clients, People & HR, Team lead, Management and Service desk: daily / weekly / monthly items with live status from the analysis (3 to do with the first to open, or Nothing to do), ticks per period for reviews, roles suggested from job, sections and level (changeable), built-in items switchable and the company's own items (Settings → My Assistant → Role playbooks).
GET|POST /api/assistant/playbook. - The AI sees the next German deadlines in its context. New:
app/assistant_de.py, app/assistant_review.py, app/assistant_playbooks.py. No migration, no new environment variables (ticks live in the existing per-person Assistant state). Tests: tests/test_assistant_de.py (27). Browser run 21/21, zero console errors.
2026-09-25 — v1.9.0: My Assistant closes the loop — lessons, follow-ups, results
- Why dismiss? Dismissing a card asks why, and every answer says what it will do: already handled, not relevant to me (three times for a kind → that kind is hidden for the person, never a critical card), it is someone else's (pick the colleague — the card, its draft and its task go to them for everyone, with "set by … on …"), not billed every month any more, we agreed later payment (overdue only after the agreed days, for 3 months; the cash forecast expects the money then), expected — a one-off or the season (moves up to that size not raised for 6 months), not a duplicate, the figures are wrong (reported to managers), something else (with a note). Lessons are specific (a client, a line, a pair of bills), only accepted from a card in the person's own analysis, audited, listed in Settings → My Assistant with author and date, and reversible by the author or a manager. Suppressed checks appear under Checked and normal with the lesson that suppressed them. 👍 now lifts a kind as 👎 lowers it.
- Follow-ups: approving an action — or pressing I sent it under a client e-mail draft — schedules a check (a week after a payment reminder, 3 days after a missing-invoice question, the day after a task's due date). Still unchanged then → Follow-ups due at the top of For you, a badge on the card, a line on the Home card, one notification and a Follow up section in the brief e-mail, each with the next step; snooze, mark resolved or stop. Changed → it closes itself.
- Results tab: the worker keeps a workspace ledger of every finding — first raised, first acted on, resolved how and when — with money measured in the data: collected (what clients actually paid of the invoices that were overdue), invoiced (the invoice actually issued for a missing month, up to 15 days late), double payments avoided (the duplicate removed before both were paid), paid twice, not billed. Median time to act and to resolve (acted vs not), what is being worked on; re-opened when a finding comes back; shown within each person's access. A hero chip shows what was secured in 90 days.
- Cash forecast: payments and bill payments recorded after the bank balance's date now carry the balance forward (a receivable marked paid today no longer disappears from the forecast before the bank balance is updated); bills keep a paid on date too.
- New:
app/assistant_learning.py, app/assistant_outcomes.py, migration b3d5f7a9c1e2 (dismiss reason and note, follow-up columns on actions, tables assistant_outcome and assistant_learning). Endpoints: POST /api/assistant/followup, GET /api/assistant/results, GET /api/assistant/learnings, POST /api/assistant/learnings/delete; POST /feedback takes reason/note/owner/days; POST /act takes sent:<e-mail>. Tests: tests/test_assistant_loop.py (17) + 1 cash test. Browser run 16/16, zero console errors.
2026-09-25 — v1.8.0: My Assistant analyses sharper — cash, payment behaviour, seasons, look-ups
- Cash, next 13 weeks (Finance access): a week-by-week forecast from this Monday — open invoices on the day each client usually pays, overdue ones about a week after a reminder (over 90 days: not counted on), the next invoices to clients billed every month, open bills on their due dates, the next bills of suppliers who bill every month, recurring monthly costs at each month end. Two lines: expected, and issued invoices only (prudent). When most revenue or costs sit outside NovuHub, the gap is estimated from the P&L — in both directions — and said so. Every assumption is listed, with a confidence level (bank balance age, recurring costs set or estimated, payment history, coverage). New KPI Cash low point, 13 weeks; the pipeline's weighted upside is shown beside the forecast, never inside it.
- What if…: a client pays later / does not pay / is lost, suppliers paid later (tax, payroll and rent stay on time unless named), a hire, a one-off, revenue still to be billed ±% — applied to the same items, with the exact effect on the lowest point and the end balance; ready-made questions from the data. Nothing in the data changes (
POST /api/assistant/cash, Finance access only, validated and bounded input). - Cash runs short card when the expected balance goes below zero (critical within 4 weeks), or a medium warning when only invoices not issued yet keep it positive; alerts include it.
- How clients pay: per client, days after the due date (median and three-in-four), on-time share and the trend of the last three invoices, learned from recorded payments, the new Paid on date of receivables (kept when marking paid, editable in the dialog, from the bank line when matched) — and a … is paying later early-warning card with the collector and the relationship owner. Overdue reminders say whether the delay is normal for that client.
- Judged against the season: P&L variances and the revenue trend compare a month with what that line would normally do — last year's pattern with a year of history, else the previous month and the line's own usual swings (robust statistics,
app/assistant_stats.py). Moves the season or the noise explain are listed under Checked and normal instead of raised. - Ask looks things up: the AI may call read-only look-ups — records, a client overview, a P&L line by month, the cash forecast, a what-if, workload — on the person's own scoped data (section access re-checked); what it looked up is shown under the answer and its figures join the invented-number check. At most 4 rounds.
- Charts: round axis ticks, x labels that fit the width, dashed what-if line; the cash chart is drawn at its shown width.
- New:
app/assistant_cash.py, app/assistant_stats.py, app/assistant_tools.py. Tests: tests/test_assistant_cash.py (18). Browser run: cash panel, what-if presets and custom changes, payment table, checked-and-normal, Ask with look-ups (stub model), Paid on, member scope, dark mode, phone — 22/22, zero console errors. No migration, no new settings.
2026-09-25 — v1.7.0: My Assistant works ahead — briefs, alerts, Home card
- Prepared in the background. The worker analyses each person's data ahead of time (again when the data it reads changes, when the day changes, or after 6 hours), so Home → My Assistant opens at once — the page says Prepared in the background … ago. Changes it never reads (notifications, chat, today's focus, dashboard layouts) no longer make it start over: stored analyses carry a fingerprint of exactly the data analysed.
- Daily / weekly brief at each person's hour in their time zone, on working days: what is new or got worse since they were last told, with the next step and the owner; the weekly brief repeats what is still open; nothing new → nothing sent. Default: daily for managers, HR and team leads, weekly for everyone else, 07:00. A brief that could not go out in the morning goes out later that day — never at night.
- Instant alerts for what should not wait (likely duplicate bill, cash runway, tight payment week, client over a month late, project about to miss its date, customers waiting): to the owner (managers when there is none or it is critical), within working hours, at most three a day. An item is announced again only when it got worse (higher severity, or +25 % and at least 100 more at stake).
- One e-mail per person per run — alerts due with a brief go on top of it (Needs you now); in the app one 🔔 per alert plus one for the brief, each opening the item. Nothing is sent to someone on approved leave; delivery is recorded before sending, so two worker processes cannot send it twice.
- Home card: My Assistant on the Dashboard (first widget; hide or move it under Customize) and in My Day — the top three items with owner and money at stake, refreshed when the data changes; a click opens the item in My Assistant.
- Links: e-mails link to the item (
/?go=assistant&f=<item>); signed out, the link goes through sign-in (also 2FA and SSO) and then opens it. Only known views and item ids are accepted. - Settings → My Assistant → Your briefs & alerts (everyone): daily / weekly / off, hour, alerts, e-mail, the next brief, what was sent recently, Use company defaults. Managers: company defaults for both groups and a Background worker panel (running / stopped, last cycle, what it did, errors — for their own workspace only).
- What changed now compares with your previous visit (a reload keeps the marks; dismissed items are not "resolved"), with a New badge on new cards. An AI write-up is kept, marked as older than the data, when the findings change, instead of disappearing.
- Also: queued notification e-mails are sent as escaped text with clickable links (no HTML from members' text reaches a mailbox).
- New:
app/assistant_store.py, app/assistant_jobs.py, migration 9e4b2c6d1f58 (on top of v1.6.0's), app/static/src/84a-assistant-peek.js. Env: NOVUHUB_DEFAULT_TZ (optional). Tests: tests/test_assistant_jobs.py (21). Browser run: Home cards, notification and e-mail links, settings, member scope, phone — 22/22, zero console errors.
2026-09-25 — v1.6.0: My Assistant
- Home → My Assistant — the "brain" of the platform. It reads every part of NovuHub a person has access to (finance, people and training, projects and tasks, sales, customer service, risks, sustainability) and says what matters now, why, who should act and how. Everyone can Re-analyse now at any time.
- Findings with the work done: missing monthly invoices (e.g. "No September invoice for Hafen Logistik AG" → the client's owner from the job descriptions, a note to them drafted, a follow-up task ready), overdue receivables with a staged reminder e-mail (first / second / final) and the relationship owner, a payment plan for payables sorted by what to pay first depending on cash (tax, payroll and rent first when tight; early-payment discounts when comfortable), P&L variances with the accounts behind them and how to investigate, revenue trend, margin, cash runway, client concentration, possible duplicate supplier bills and unusual amounts, projects at risk, unassigned and stalled work, workload over capacity with concrete re-assignments, approvals waiting, overdue (mandatory) trainings reported to HR with reminders, team absence clusters, quiet deals and a thin pipeline, customer tickets waiting, high risks without owner or plan, open points in the materiality assessment (read-only), key results behind, skill gaps and training coverage, and missing job descriptions.
- How to do it: every card has step-by-step guidance with links into NovuHub; e-mail drafts can be edited, copied, opened in the mail program or — to colleagues — sent through NovuHub.
- Actions only with approval: create a task, remind people, move a task, start a project, send an internal message — each through an approval dialog, re-checked on the server against the person's own analysis, recorded in the activity log as approved by them, notified to those involved, never carried out twice, and undoable while untouched.
- Views: briefing and "what changed", key figures, For you (priorities, My work — in this order, data reliability), area tabs with charts (single-axis, legends, table view, validated colours in light and dark), Anomalies, Plan (short / medium / long term), Company (what the data says — industry, size, run-rate, margins, growth, revenue mix — and what managers told it), Ask.
- Per-person scope, server-side: only the sections a person can open; confidential HR fields removed; other people's leave only as "away"; chats and mailboxes never used; people insights (workload, training, absence of others) for managers, HR and — for their team — leads, and switchable off. What does it know about me? lists what is used and who sees it.
- AI (optional): with an Anthropic key the Assistant writes the briefing, priorities, horizon plan, coaching and strategy, answers questions and rewrites e-mails. Platform content is passed as data, never instructions; the AI cannot act; every figure it writes is checked against the calculations and sentences with unmatched figures are removed (and the page says so); daily limit per person; unchanged data re-uses the last write-up. Optional public market context uses a web search with industry and region only.
- Settings → My Assistant (managers): company description, industry (inferred suggestion), business model, offer, customers, markets, goals per horizon, priorities, risk appetite, constraints, instructions, thresholds, people insights, AI options. Settings → Job description (everyone): title, department, summary, duties, clients, channels, P&L lines/accounts, KPIs, escalation; managers and HR see completeness for all and can remind or edit. The server lets people change only their own job description (managers and HR anyone's).
- New:
app/assistant_scope.py, assistant_facts.py, assistant_owners.py, assistant_ai.py, app/routes/assistant.py, migration 7c1e9a2b3d44, app/static/src/79-assistant.js (lazy chunk), 97a-assistant-settings.js, guide page My Assistant. Tests: tests/test_assistant.py (18). Browser runs: four roles (owner, accountant, analyst, HR) 29/29, AI path with a stub model 11/11, zero console errors.
- Mind Map: the two buttons that created a map are one. The white "+ New map" button beside the section title is gone; the purple + New mind map button now sits there, on the right of "All mind maps" (and of each folder, where it creates the map in that folder). "New folder" and "From a project" stay together at the top right.
2026-09-24 — v1.5.0: P&L mapping codes and grouped placement
- Mapping-code column. An accounts file may say which statement line each account belongs to ("Target Mapping Code", "Line code", "P&L code", "BWA-Zeile" … or a column of codes such as REV_01 / OPEX_SND recognised by its values). It is offered as a new column role, Mapping code (P&L line); the code's prefix gives the role (REV → revenue, COR/COGS → cost of sales, OPEX/SND/MKT/RND/GA → operating expenses, DA → D&A, FIN_INC / FIN_EXP → financial income / expense, TAX → tax), so a file with its own mapping is never classified by guessed chart-of-accounts ranges again.
- Grouped placement. Accounts that share a code — or, without codes, a category — go onto the template as one group: the line whose own code is the code (a template with a code column), else the line chosen for that code last time, else the line whose label, description and functional group best match the code's words, its category and what its account names share, within the code's role and one line per code while the template has enough. The review opens with a Mapping codes → your P&L lines table (change a code's line and all its accounts follow; remembered per workspace), a verdict ("15 mapping codes → 15 of 15 template lines"), and a preview of the statement's totals before saving. A code's line now wins over the line a single account had in an earlier (wrong) import; an account moved by hand keeps its line.
- Account categories read better: "Sales & Distribution", "Marketing & Advertising", "Research & Development", "General & Administrative" are operating expenses named after themselves (not revenue, not "Other"); "Direct Labor", "Freight & Logistics", "Cost of Revenue" are cost of sales; "Financial Expense" is an expense by its own words; "Indirect costs" is no longer cost of sales.
- Templates: the functional-group column decides a line's role over caption words ("Sales & Distribution Expenses" is an expense); the caption still splits the financial result into income and expense; "Total Cost of Revenue" and "Total Depreciation & Amortization" are real COGS / D&A totals instead of running totals, and a summary row totals the lines of its own group; a line-code column (e.g. "Line Item Code") is read as the line's ID and joined to the accounts' codes; an amount column in a template is ignored instead of being read as account numbers; REV_TOTAL-style codes mark totals, while TAX_TOTAL beside "Income Tax Expense" stays the tax line. Applying a template removes the old lines no month uses.
- Currency: the amount header's currency ("Balance (USD)") is shown in the review with a one-click switch of the Finance currency label.
- Tests:
tests/test_finance_mapping_codes.py (6), tests/frontend/pnl-mapping-groups.test.js (7). Real-browser run with the support-case files in four orders (template → file, re-import over stale mappings, file → template, codes in the template): every one of the 25 lines equals the expected results, 16/16, zero console errors.
2026-09-24 — v1.4.1: three small fixes
- Workload → Submit time for approval: the "Who are you?" picker is gone — hours are always submitted as the signed-in person, shown as "Submitting as <name>" (previously anyone could file hours under a colleague's name).
- Activity: managers and admins can tick rows (or Select all) and Delete selected, or Clear old activity… older than a week / 30 days / 90 days / a year / everything, after a confirmation; the export stays available first. The stale "sending activates with the online version" banner now says what is true.
- Time & Leave → My calendar: the name shown twice is shown once (the summary line stays).
- Test:
tests/frontend/small-fixes-v141.test.js (3); browser run 9/9, zero console errors.
2026-09-24 — v1.4.0: employee document folders
- Time & Leave → Documents. Every employee has a folder: what HR or a manager filed for them (contract, resignation letter, termination, payslip, certificate, warning or reference letter, ID / permit …) and what they filed themselves. The employee opens and downloads their documents there and is notified (in the app and by e-mail) when HR files one. HR and managers pick any employee — including people who have left — and upload; team leads can view their team's folders but not file into them; an employee can remove only what they uploaded.
- Leave requests: the optional document (sick note …) is now filed in the employee's folder (category Sick note, linked to the request) instead of being stored inline in the shared document; the approver opens it from the request as before.
- HR sees what managers see: the HR mark now also opens the confidential employee details (tax and insurance numbers, pay …) — server-side in
app/hr_privacy.py as well as in the UI. - Private by construction: folder files are
hrdoc:<employee> uploads that /api/files serves only to the employee, their team lead, HR, managers and admins (404 for anyone else, never a public bucket URL); only HR and managers may upload into someone else's folder. The folder list itself — file names included — and a leave request's attachment are withheld from everyone else's copy of the workspace and restored on save, so a colleague can neither see nor alter them. Documents filed before this release are moved into the protected store automatically the first time HR, a manager or the employee opens the folder (POST /api/files/<id>/protect). - Tests:
tests/test_employee_documents.py (7), tests/frontend/employee-docs.test.js (3). Real-browser run with five accounts (owner, HR, team lead, employee, colleague): 17/17, zero console errors.
2026-09-24 — v1.3.0: video courses
- Video lessons. A lesson can carry an uploaded video file (MP4, MOV, WebM, Ogg) or a YouTube / Vimeo link, plus WebVTT subtitles and a transcript. Only HR and managers upload video files (workspace role manager/admin/owner, org level Manager, or the new HR mark in the member editor — which only a manager can give; the server enforces both:
app/routes/video_upload.py, access_guard). Uploads go in resumable 8 MB chunks written straight to disk (never held in memory), are checked to really be a video by their container signature, stored locally or in S3 via multipart, and streamed back with byte ranges so learners can scrub and resume. Size cap NOVUHUB_MAX_VIDEO_MB (default 2048); an upload is refused up front when it would leave less than NOVUHUB_MIN_FREE_DISK_MB (default 2048) free. - Watch tracking. What a learner actually played is recorded as merged time ranges (skipping ahead is not watching), per learner per course in
DB.learnProgress (own record each, merged by id). A course's Required watching per video (%) (default 90 for new courses) unlocks the next lesson and the exam; learners resume where they stopped; the Track view shows watched % and checkpoint results per person. Works for uploaded files and — through the players' postMessage interfaces, with no third-party script and no CSP change — for YouTube and Vimeo. - Questions tied to the video. Checkpoint questions at set times pause the video until answered (they cannot be skipped by seeking past them) and are recorded; any exam question can show a clip (a lesson video or a link, with from/to).
- Written exams. New question type Written answer (reviewed), optionally answerable by voice (recorded in the browser;
Permissions-Policy: microphone=(self)). An exam containing one is submitted to Exams to review (DB.learnAttempts); HR, managers and team leads mark each answer correct or not with a comment; the result, score and certificate follow the review and the learner is notified and can open the feedback. - Catalogue: 🎬 Video course badge with total length, “Continue” / watched % on the card, “⏳ Exam awaiting review”.
- Tests:
tests/test_video_courses.py (9), tests/frontend/video-courses.test.js (7). Real-browser run with a generated 2.7 MB WebM uploaded in 1 MB chunks: upload, checkpoint that cannot be skipped, 90 % unlock, written answer, review, certificate, member without upload rights, and YouTube tracking via simulated player messages — 16/16, zero console errors.
2026-09-23 — v1.2.0: document templates on every report download; tabs survive a refresh
- Document templates. Every report download now opens a dialog first: pick a saved template, create one on the spot, or download the plain file. A template carries the company logo (left or right), name, address, phone, email, website and tax number, an introduction above the table, a footer on every page and a signature — drawn on a signature pad (mouse, pen or finger) or uploaded — with the signer's name, role and date, plus an accent colour. Templates are workspace-wide, managed under Settings → Document templates (edit, duplicate, delete, default) and seeded from the invoice / report branding already set. The default is pre-selected and the last choice is remembered per browser.
- Where it applies: everything that goes through the shared exporter — Finance (receivables, payables, P&L, budget, forecast, cash-flow projection), HR reports and statutory reports, Sustainability registers (the Sustainability module itself is unchanged), the business-health snapshot — as Excel and PDF; the ZUGFeRD invoice PDF (logo, extra contacts, text, signature, footer; the embedded e-invoice and the seller's legal data stay the invoice's own); the mind-map PNG; and the CRM report, learning-path completion and risk-register CSVs, which download as branded Excel when a template is chosen (a CSV has no place for a logo). Machine-readable files — DATEV, XRechnung XML, backups, audit logs, raw import files — are never branded.
- Server:
app/doc_brand.py sanitises the template sent with an export (lengths, a real hex colour, logo and signature accepted only as PNG/JPEG/GIF/WebP data URLs ≤ 1.5 MB, decoded and re-encoded as PNG — an SVG or a URL is refused) and draws it: a letterhead and footer on every PDF page and a signature block at the end; a letterhead (side by side, or stacked on narrow sheets), signature, footer and print header/footer in Excel. - Tabs survive a refresh: CRM (Pipeline / Contacts / Reports), Sustainability (all seven tabs), Tasks views, Calendar mode, Portfolios view, My Day and the Settings tab now reopen where you were (
16-tab-memory.js; saved values are validated against the known tabs). - Tests:
tests/test_doc_templates.py (5), tests/frontend/doc-templates.test.js (5); settings visual baseline re-recorded (the extra tab). Real-browser run: template created with an uploaded logo and a drawn signature, dialog before AR Excel/PDF, Sustainability, CRM report, invoice; refresh keeps CRM, Sustainability and Settings tabs — 19/19, zero console errors.
2026-09-23 — v1.1.1: P&L import reads typed account files and ID/description templates
- Accounts files with an account-type column (number · name · type · amount) are classified by the file's own types before any chart rule: the name and type columns are no longer mistaken for account numbers (headers such as "Account Name" / "Account Type" contain the code word "account"), revenue deductions, the two halves of the financial result (income vs expense, by name and sign) and taxes land on their own roles, operating expenses are split into families by name, and a role the file writes negative as a whole (financial expense) is turned to NovuHub's positive-cost convention (
meta.signsTurned). - 4-digit charts: a BWA ratio key (8003, 8050 …) no longer drops a real revenue account unless the row is shorter than the accounts or carries the ratio's caption. SKR03 class 3 purchases (3000-3969) are cost of sales, 3970-3999 stock; the neutral class 2 (interest, taxes, other income/expense) is part of the P&L.
- P&L templates with a header row are read by their headers: the "P&L Line Item" column is the label (not the longer description), a line-ID column (1000, 1000_NET) is never read as account rules, a "Header / Summary Line" marker decides lines vs subtotals, the category column supplies a role, and ranges written in a description ("Sum of 6200-6239") become rules. New standard subtotals: net revenue, total OPEX and net financial result.
- Placing accounts on a template that quotes no numbers: each account goes to the line whose own words (label, description, category) describe it — salaries to personnel, discounts to sales deductions — instead of every account of a role landing on its first line. A remembered line now applies only to the same account (same name), not to another file's account that shares the number; remembered column layouts that were mis-read are dropped.
- Tests:
tests/test_finance_typed_accounts.py (6), tests/frontend/pnl-template-place.test.js (6). Verified end to end in a real browser with the reported template and 400-account file: every line equals the account file's own sums, Net Income 16,830.60 = the signed total of all 400 balances.
2026-09-23 — v1.1.0: the eight hardening groups closed (commercial → code quality)
- Commercial. Sign-up requires accepting the Terms and the Privacy Policy; the acceptance is recorded with time, IP and user agent, accounts that predate the checkbox are asked once in the shell, and
GET /api/compliance/consents lists who accepted which version. The privacy policy and DPA now carry a sub-processor table and retention periods derived from the deployment's configuration (app/legal_facts.py: hosting, the mail provider you set, object storage by endpoint, Sentry if configured; NOVUHUB_SUBPROCESSORS overrides), an SLA page (/legal/sla) fed by NOVUHUB_SLA_*, and the Impressum answers 503 until every field is set rather than serving placeholders. VERSION (semver, +sha when GIT_SHA is set) is what the app reports; LICENSE, THIRD_PARTY_LICENSES.md and a CycloneDX sbom.cyclonedx.json are generated by tools/release/licenses.py; tools/release/bump.py bumps and prints the tag command. Pricing and billing enforcement were excluded from this round on request. - Security & platform. The Content-Security-Policy drops
unsafe-inline: the 1,594 inline on*= handlers are compiled at build time into delegated data-wh-* handlers (tools/build/csp_handlers.py, runtime wh-events.js), template scripts carry a per-request nonce, and a real-browser walk records zero violations. COOP/CORP/COEP headers, HSTS preload. The workspace document is validated against a schema before it is stored (app/doc_schema.py; 400 invalid_json / 422 invalid_document with the offending paths). Alembic migrations (Flask-Migrate) run on boot; a legacy database is stamped at the baseline and upgraded — app_state, mail_outbox, idempotency_key, api_token.scope. - Accessibility. Every icon-only button carries a label;
:focus-visible rings on all controls (58 misses → 1); dialogs get aria-labelledby and synchronous focus; the top bar is a banner landmark; the document title follows the view; clipped controls fixed. - Performance. The front-end is a core bundle plus 24 on-demand chunks (analysis, mind map, settings, docs, learning, finance, the P&L workbench, journal, forecast, HR, portfolios, CRM, messages, calendar, people, editors, …) with a dependency map and typed stubs; no chunk over 120 KB. nginx serves pre-gzipped, immutable, versioned assets.
- Testing. Coverage 79 % of
app/ with a 60 % gate in the CI reference; the built core is booted in jsdom; the end-to-end smoke registers through the real form and walks every section; a visual-regression suite (13 views, PIL diff, 0.5 % tolerance, toasts hidden, clock frozen); a locust load test. 302 Python and 59 front-end tests. - Observability.
GET /metrics (Prometheus; loopback or NOVUHUB_METRICS_TOKEN; multiprocess-aware), slow-request and slow-query warnings with the request id, a client error beacon (/api/client-error, rate-limited, forwarded to Sentry), a worker heartbeat surfaced on /api/ops/status, and monitor definitions for Uptime Kuma and Prometheus under deploy/monitoring/. - API & mail. Token scopes (read + write / read-only; legacy tokens keep full access; webhooks and
/inbound need write), Idempotency-Key replay for 24 h bound to token and route, deals (CRUD) plus invoices and members (read-only, confidential HR fields never included), OpenAPI and /developers updated, a dependency-free Python SDK (sdk/python/) with tests. Failed e-mails are queued in mail_outbox and retried with back-off by the worker (2 → 720 minutes) before being marked failed; automation e-mails and webhooks back off the same way. Web Push on cryptography (VAPID ES256, RFC 8291 aes128gcm) with python -m app.push --generate, a service-worker push handler and Enable push / Send test push under Settings → Notifications. - Code quality.
_guess_columns (complexity 63) and the connector search (48) and the automation queue flush (52) are split into small functions (worst in app/ now 40); the table-reading and connector helpers moved out of the route modules into app/finance_tables.py and app/agent_helpers.py; 94 conflicting CSS redeclarations resolved by tools/css/dedupe_conflicts.py with the cascade-final value of every property proven unchanged and the visual baseline pixel-identical; a staging compose profile (NOVUHUB_ENV=staging: noindex, ribbon, Sentry environment) and a production worker service; the Docker base image pinned by Debian release with a digest slot (tools/release/pin_base_image.sh). - Deploy note:
docs/deploy-notes/DEPLOY-v15-hardening.md (migrations on first start, the systemd unit, the two protected files, new variables).
2026-09-22 — The four v14 findings closed: HR confidentiality, working time, pay transparency, bank & e-invoice, German UI
- Confidential HR fields no longer leave the server for people who may not see them. v14 measured it: every member's
GET /api/data carried every colleague's tax number, insurance number, disability status, exit reason, hourly rate. A new app/hr_privacy.py strips tax, insurer, insuranceNo, sb, exitReason, rate, salary and gender from every outgoing document — the read, both conflict responses and the backup download — leaving each person their own record, a team lead their team, and owner/admin/manager everyone. The return path is guarded too: who the actor is, is decided by the stored document (an incoming one could claim any level), so a redacted client's save can neither wipe a colleague's fields nor forge them. GET /api/data/hr-confidential is the one endpoint that serves them, to exactly the people who may see them. Seven tests pin it. - Working time is now beginning, end and duration — not a duration alone. Optional start/end fields at all five entry points (task modal, project page, timer, employee record, log editor); the hours stay the derived value (
22:00 → 06:00 = 8 h, a bad pair = no hours and nothing stored), and the timer fills them from its own timestamps. The working-time report gained a Days with start & end tile, a per-day records table — day, person, start, end, hours, complete or duration only — and a note naming how many days still carry a duration only. §16 ArbZG / §17 MiLoG asks for exactly that table. - Gender pay gap (EU Directive 2023/970). Annual salary and gender join the confidential block of the employee record and feed a new HR-only report: mean and median gap per hour and per year, the four pay quartiles by share of women, and categories of work of equal value with the 5 % threshold that obliges a joint pay assessment flagged. Pay is compared per hour, so part-time is not read as underpayment. Any group under five people is suppressed, and nothing is reported at all until five men and five women have both fields — below that a figure identifies individuals. The statutory calendar carries the 7 June 2027 first report on 2026 data.
- Bank statements import as CAMT.053 and MT940, not only CSV.
app/bank_statements.py recognises the file by its content: CAMT namespace-agnostically from 001.02 to 001.08 (a document declaring a DTD is refused before parsing), MT940 with its comma decimal, ?20–?29 purpose subfields, continuation lines and C/D/RC/RD marks so a reversal comes back positive. Both produce the shape the CSV path already produced, so review-and-commit is unchanged; an unreadable file answers 422 with a sentence, never a 500. Twelve tests, including the two formats through the endpoint. - The e-invoice is pinned by tests, and an invalid one is refused before it is built. Nineteen tests read the CII XML back with a parser: the EN 16931 guideline id, type code 380 (381 for a credit note), the
format="102" issue date, one line per billed item with unitCode="C62", the VAT category and rate, and the four totals reconciled (GrandTotal = TaxBasisTotal + TaxTotal, DuePayable = GrandTotal) — plus a discount lowering the basis, a currency symbol becoming a real ISO code, an ampersand surviving escaping, and the PDF opened with pikepdf to find factur-x.xml attached as /Alternative. The endpoint now answers 422 naming the EN 16931 rule that failed (BR-1, BR-6, BR-7, BR-16) instead of building a PDF nobody can file. - German: ~400 phrases translated, and coverage is now measured rather than diffed. Every view was walked in Deutsch and each interface text node the dictionary could not render was collected (
tools/i18n/harvest_de.py); the HR statutory reports, the forecast methods, the journal and bank views, the billing panel and the settings prose are now German. The translator gained three rules so one entry covers many strings: a trailing date stays a date ("fällig 6. Nov."), a phrase whose only variable part is numbers matches through a # template ("in # Tagen"), and a trailing emoji is left alone ("Einfügen 👍"). Timezone selects are marked as identifiers rather than language. The audit now asks the app's own translator whether each visible string is covered — a term German keeps ("Board", "Journal") counts as covered, a line mixing a person's name with interface words does not count as translated — and reports 94 % across 29 views, with the ESRS Sustainability section excluded by name because it is deliberately left in its reporting language. - Tests: 250 Python, 48 front-end. The v14 product tier goes from 41 to 44 of 53.
2026-09-22 — Assessment v14: sale readiness, and three defects it found fixed
- v14 measures the product, not only the code. 345 criteria (the 255 from v13 re-run unchanged, plus a product tier of 90): the anonymised BWA through the real import endpoints, the template reader, the export endpoints with the workbook and PDF opened, an ordinary member reading the workspace document back, the SGB IX quota function evaluated at nine boundaries, and a browser walk of a seeded 23-person workspace whose seven HR reports are compared with an independent Python computation. Plus 39 live probes re-measured and a competitor table from eight vendors' published pages. Result 260/345 (75%); like for like 187/255 (v13: 188); product tier 70/85; browser-verified 32/32. Verdict: not sellable tomorrow — seven blockers, none a feature — sellable in about four weeks. Report:
docs/assessments/WorkHub-SaaS-Readiness-Assessment-v14.pdf. - Fixed: the P&L, Budget and Forecast exports ignored the company template. They built rows from a hard-coded standard section list, so a template-shaped workspace exported Product Revenue 0 / Service Revenue 0. All three now use the same row plan as the screen (
pnlPlanRows); the browser check downloads the workbook and finds the GP2 row. - Fixed: the import review hid its own verdict. "All 15 group totals reconcile to the cent" was rendered inside the guide banner, which the shell collapses into the "?" icon. It is now a status strip above the table — green when everything reconciles, amber naming the groups that do not, with the count of subtotal rows set aside.
- Fixed: a finance module had passed the repository's 1,000-line guard unnoticed.
75-finance-reports.js was 1,021 lines; the front-end unit test caught it but CI runs pytest only. The review and the exporters moved to the workbench module (973 lines); 24/24 front-end tests pass. Adding node --test to the workflow is in the report's action plan. - Found, not fixed (the blockers): every member's browser receives every colleague's tax number, insurance number and disability status through
GET /api/data (the UI hides them, the API does not); no terms/DPA acceptance at sign-up; no named sub-processors or retention periods in the DPA; nine placeholders in the live Impressum; billing off and no /pricing; the live server one release behind with version "dev". The report orders them with effort estimates.
2026-09-22 — HR → Reports: the monthly management pack and the statutory filings
- A Reports tab under HR Reports, next to Leave calendar. It opens on a hub: a statutory calendar with what is due, for which period, to whom and in how many days (Schwerbehindertenanzeige 31 March, BG Lohnnachweis 16 February, the Anhang with the accounts, the EU pay-gap report from June 2027), then one card per report — name, what it measures, a live headline figure, the data it reads — grouped into Monthly management pack and Statutory & annual. Every report opens with an About this report panel (what, why, how it is computed, data, and buttons to the tabs it is connected to), KPI tiles, a chart, tables, and Excel/PDF export; the open tab and report survive a refresh.
- Headcount & FTE bridge. Opening + joiners − leavers ± contract changes = closing, in heads and FTE, as waterfall charts, month by month and by department.
- Turnover & attrition. Monthly, annualised and rolling-12-month rates; voluntary vs. involuntary from a new Exit reason field on the employee record; leavers by tenure band, reason and department; first-year leavers flagged.
- Absence rate & leave liability. Fehlzeitenquote (sick days ÷ planned working days on the workspace work-week), vacation and other leave, absence by department, the Bradford factor over twelve months, untaken annual leave in days and euros (Urlaubsrückstellung, §249 HGB), and — for managers only — the BEM check for anyone over 42 sick days in twelve months (§167 SGB IX).
- Personnel cost per FTE, reconciled to the P&L. Reads the imported Profit & Loss months directly: personnel cost (the P&L lines are detected by name and can be chosen), split into wages & salaries and social charges by account name and SKR03/SKR04 number, divided by average FTE and headcount, and compared with the cost the HR master data implies (contracted hours × hourly rates). The P&L statement links across to it.
- Working time & overtime. Logged vs. contracted hours per person and month (leave removed), the overtime balance and its value, and flags for days over 10 hours and weeks over 48 (ArbZG); people without time logs are shown as such rather than as zero.
- Anhang figures (§285 Nr. 7 / §275 HGB). Average employees by the §267(5) quarter-end method and by monthly average, by employment type and department, apprentices separately (new Apprentice flag), wages & salaries vs. social charges from the P&L, and a ready-to-paste German and English sentence for the notes.
- Schwerbehindertenanzeige (§§154–163 SGB IX). Jobs per month (§156: under 18 h/week and apprentices excluded), mandatory places (5 %, or 1–2 for 20–59 jobs), places filled with §159 multiple counting, unfilled place-months and the Ausgleichsabgabe at the 2025 Staffelbeträge (155/275/405/815 €; 155/235 € and 155/275/465 € for the small-employer bands), editable for future adjustments; the filing panel names the deadline and the IW-Elan route. A confidential Severely disabled / equal status field in the employee record (managers only) feeds it.
- Tests: a static contract for the hub, the editor fields, the chunk wiring and the P&L link. Suite is 211 tests.
2026-09-22 — Import as four guided cards, a Templates folder, Finance tabs that survive a refresh, an honest Forecast on thin history
- Import is four cards, each with its guide under its button. Import a month of actuals, Import a budget month, Enter a month by hand and Use your own P&L template each get a card: the button, what the file is, and the three steps that follow in order — column check, review, save — instead of three buttons in a row and a paragraph of prose.
- Templates is a folder. Every file you feed the P&L is kept: the P&L template sheet and each accounts file whose column layout NovuHub learned. They live in Finance → Archive → Templates and in the Templates panel under Import — download the original, see what NovuHub keeps from it (41 rows · 44 rules; col 1 = account no., col 3 = this month…), replace the template, remove it (the statement returns to the standard layout, your account choices stay), or forget a file layout so the columns are asked about again. The bytes sit in the same server-side store as invoice PDFs; the workspace document only references them.
- A Finance tab now survives a refresh. Reloading on Forecast, Budget or Profit & Loss lands you back on that tab (and, under Profit & Loss, on the sub-tab you had open) instead of Overview — the same remembered view-state the Archive folder uses.
- The Forecast says when it has too little history instead of pretending. One month of actuals is a point, not a line, and every history-based method quietly repeated it across the horizon. Each method now declares how many months it needs (moving average, trend, drift, CAGR: 2; Holt and damped trend: 3; seasonal: 13); with fewer, its card says so, the numbers follow straight-line growth from your last month using the revenue and cost growth rates shown in a banner at the top, and the statement header names the fallback. Follow-the-budget is offered when a budget exists. Import more months and the methods switch on by themselves.
- The Forecast table shows the actual months it starts from. The last three actual months sit in grey columns marked actual to the left of the forecast columns, and the header says which actuals feed the projection — so March's figures under April, May and June read as a continuation of March, not as March mislabelled.
- Tests: the template file store (kept, served with its own name and type, refused for unknown types, dropped). Suite is 207 tests.
2026-09-22 — Your own P&L template becomes the statement; the P&L bar is just months, jump and export
- Upload the P&L you already report with and it becomes the statement. Under Import → Use your own P&L template, upload the spreadsheet: line names in one column, account numbers or ranges beside the lines where you have them. NovuHub reads every row and shows what it understood before anything changes — line, section, subtotal or ratio; the role of each line; the account rules — each editable, and any row can be left out. Apply builds the lines, the layout and the account rules in one go and re-maps every month already imported. A subtotal means what its label says: Gross Profit 2 (GP1 − commissions & freight) is computed as GP1 minus those two lines, wherever they sit in the sheet; a subtotal with no formula is a running total; a term that names no line is shown and ignored. An in % of sales row under a line, a section or a subtotal is a ratio of that thing. A section's thereof lines are its lines, and the one without numbers is the catch-all for the rest.
- Every month after reads into that template. On import, an account with a rule goes where the rule says; an account without one goes to the only line of its type, or to the type's catch-all line; when the template offers several lines of that type and no rule — three sales lines, say — it goes to the first and is flagged placed by type under Accounts, where a filter shows exactly those and a Confirm ticked button clears them. A line chosen by hand is remembered and never flagged again. When the same number is quoted on two lines, the one whose type matches how the account was recognised wins — 6220 lands under Abschreibungen, not under a cost block that mentions it.
- Nothing ever disappears from the statement. A line that holds accounts but that no layout row shows appears in a Not placed in your P&L section at the bottom with links to fix it, so the statement always adds up to the file even while the mapping is being tidied. Measured on the real March file in the group's own template: Sales 778,385.61 · COGS 713,782.62 · GP1 64,602.99 · GP2 −166,524.37 (GP1 − 45,657.61 − 185,469.75) · GP4 −140,744.86 (GP1 − 205,347.85) · EBITDA −765,339.55 · EBIT −802,752.06 · EBT −862,857.84 · Net −863,956.43 — the subtotals equal to the BWA to the cent and the GP lines to what their labels define.
- Budget and Forecast follow the same layout. The three views now share one row plan — the layout expanded into ratio, subtotal, section, line and total rows — so the template shapes all of them, and the This tab hit a snag error in Budget and Forecast is gone (a statement-only ratio row had been copied into both). Budget section totals now sum the lines the section shows rather than the whole role.
- The P&L bar is what you asked for. Months to tick on and off, Jump to month, and the Excel/PDF exports on the right. P&L lines, Import month and Add month are gone from it — they live under Import and Lines & layout. Under the bar: a search box that filters lines and accounts as you type (matching accounts expand their line), and Hide zero lines. Columns are resizable by dragging the divider in the heading; double-click resets; widths are remembered per workspace. Saving an import now lands on the statement showing that month.
- Accounts is the account → line half of the template — it stays. Lines & layout holds the lines and their order; Accounts holds which account goes to which line, and is where a new account gets its place. Its table sorts by any column, filters by line and by status (placed by type, new this month, unassigned), shows the all-months total, and ticks all rows shown. Its count is the number of accounts in your months, not the size of the remembered mapping.
- Tests: the template reader on an invented management-report sheet (row kinds, roles, rules, ranges, duplicate numbers, a non-spreadsheet upload refused). Suite is 206 tests.
2026-09-22 — Broad cost groups are split the way a management report splits them
- A BWA group such as Werbe-/Reisekosten or Kosten Warenabgabe is one line in DATEV's eyes and several in a management report's. Within a recognised group, each account now takes its finer line from its own position in the chart — Marketing apart from Travel & Entertainment, Commissions (675x, 677x) apart from Packaging & Freight (671x–674x, 676x, 678x–679x), Consulting & Audit (6825–6836) apart from Licences & Concessions (6837–6839) and Office & Admin. The split never leaves the group's role, so every group total still reconciles to the file. An unrecognised group keeps its own caption, untouched, and takes its role from the numbers under it or from its position in the statement.
- Investigated the difference between the March figures in the group's Excel and the platform: the platform reproduces the imported BWA to the cent, and the Excel is fed from a different export of the same books (an external workbook, keyed by a different BWA form). The two disagree for March itself — Sales 1,111k against 777k, Cost of Sales 529k against 714k — but nearly agree over the fiscal year (Sales 18,278k against 18,002k; Personnel 2,919k against 2,934k; Rent 193k against 193k). That is the signature of bookings dated to different months in the two exports, not of a sign error: costs are positive and credits negative in the file, and its own subtotals prove they are summed correctly. Matching the Excel's numbers needs the Excel's source file imported, not a change to the platform.
2026-09-22 — Profit & Loss becomes a workbench: any export, any chart, any layout
- The import no longer needs to know anything about the file. A subtotal row equals the sum of the rows it summarises, and those rows are printed next to it — after it in a BWA, before it in a trial balance. So the importer now finds the structure of an account list from the numbers alone: which rows are group totals, which are derived totals (gross profit, total costs, the operating result — sums and differences of the groups, chased to a fixed point), and which accounts belong to which group. No chart, no language, no vendor dictionary. On the March BWA it finds 15 of the 16 groups and all 10 derived lines in 44 ms and reconciles every one; the DATEV key rows and the German vocabulary remain only as a second opinion for rows the arithmetic cannot see (a zero-valued group, the two ratio rows). A French, Dutch or Xero export gets the same treatment, with the company's own group captions as its P&L lines.
- Four sub-tabs under Profit & Loss. P&L is the statement. Import is a two-step flow: NovuHub first shows what is in the file — encoding, separator, header rows, a preview, and a role for every column (account number, name, this month, year to date, percentage, ignore) — and you confirm or change it before anything is classified; then the recognition review. It reads the month out of the header ("Mär 2026" → March 2026), which would have caught the March file that was saved as October. The layout is fingerprinted and remembered, so the same accountant's export is never asked about twice. Accounts lists every account ever imported with the line it maps to, searchable, changeable one at a time or in bulk — and a change applies to every month already imported and to every month from now on. Lines & layout is where the lines live and where the statement's shape is chosen.
- The statement's shape is now a layout, not an accident. Lines are what accounts map to; a layout arranges them into sections, subtotals and ratios. Four presets ship: NovuHub standard, DATEV BWA (Betrieblicher Rohertrag → Gesamtkosten → Betriebsergebnis → Vorläufiges Ergebnis), HGB §275 Gesamtkostenverfahren, and a management report with GP1 → GP2 (less commissions & freight) → GP4 (less personnel) and thereof lines — the structure of the group's own Excel. Any preset can be edited row by row: reorder, rename, delete, add a section, a subtotal with its own formula, or a % of revenue line. Lines can be renamed, re-roled, reordered and merged, and every month updates at once. The stale seeded lines that showed as €0 (Product Revenue, Service Revenue, Marketing, General & Admin) can be removed with one click, and the statement says when it is showing empty lines.
- A line chosen by hand wins over every guess. The remembered mapping was written on save but never read on import; it is now applied before anything the server proposed, and the review says remembered from an earlier month.
- The column guesser replaces the old scorer everywhere. The old rule — "the amount column is the one with the most numbers" — chose the account-number column on any DATEV export. Both the guided path and the plain API now use the same guesser, which reads header words, recognises percentage columns by their shape, and prefers the first money column as the month with a later, larger one as year to date.
- Measured on the real March file in a browser: Revenue 778,385.61 · Cost of Sales 713,782.62 · Gross Profit 64,602.99 · EBIT −802,752.06 · EBT −862,857.84 · Net Income −863,956.43 — every one equal to the corresponding BWA line to the cent, and zero console errors across the whole flow. Tests: an anonymised BWA-shaped fixture drives the inspect → map → import path; the structure finder is tested on total-first, total-last, flat and foreign-language inputs. Suite is 202 tests.
2026-09-22 — The P&L import reads German accounting properly
- A real DATEV BWA imported as 303 of 387 accounts in one "Operating Expenses" bucket, and the totals were wrong. Not only were Personnel and Marketing empty — the file's own subtotal rows were being imported as if they were accounts, so the whole revenue and cost base was counted twice, two result lines totalling −1.7 m (
6995 Ergebnis vor Steuern, 6997 Vorläufiges Ergebnis) sat inside operating expenses, and two percentages (8003 Rohgewinnaufschlag, 8050 Umsatzrendite) were added as euros. The statement showed revenue of €1,137,350 and EBITDA of −€2,509,650 against a file whose own revenue line reads €777,266. - A BWA is now read as the document it is. DATEV prints each account underneath the group row it belongs to —
4100 Personalkosten followed by its wage accounts, 4400 Werbe-/Reisekosten followed by its advertising accounts. Reading that layout assigns every account exactly, and the statement reconciles by construction rather than by how well a rule happens to fit. On the March file all 16 group totals now match the file's own subtotals to the cent, and so do the derived lines: Gross Profit €64,602.99 = BWA 4095, total costs €867,355.05 = BWA 4990, EBIT −€802,752.06 = BWA 4995 Betriebsergebnis. - Subtotal and ratio rows are recognised and left out of every sum. They still appear in the review list, struck through and labelled with the reason, so you can see nothing was lost — and a row put there wrongly can be included with one click.
- Account numbers are read before names. SKR03 and SKR04 are both supported and the chart is detected, never assumed: SKR04 puts revenue at 4xxx and expenses at 6xxx while SKR03 is the mirror image, so assuming one would invert the other's P&L in silence. Numbers of 4, 5, 6 or 8 digits all resolve to the same chart position, which is what let the six-digit numbers in this file be understood at all.
- The name matcher can read German now. It failed because it compared raw substrings: "Werbekosten" does not contain "werbung", "Gehälter" does not contain "gehalt", "Umsätze" does not contain "umsatz". Names are folded (ä→ae, ö→oe, ü→ue, ß→ss) and the vocabulary covers the German cost structure — Personalkosten, Raumkosten, Fahrzeugkosten, Werbe-/Reisekosten, Kosten der Warenabgabe, Abschreibungen, Versicherungen/Beiträge, Rechts- und Steuerberatung and the rest. Names are now only a fallback for rows with no usable number.
- Balance-sheet accounts stay out of the P&L, and the AI pass — when a key is configured — only refines rows nothing else could place, so it can never overrule a mapping that already reconciles.
- The review screen says how the file was read (BWA or chart ranges, which chart, how many digits) and whether the group totals reconcile, so a bad file is visible before it is saved rather than after.
tests/test_finance_accounts.py pins all of it, including a guard that no category name is used under two different P&L roles — which caught a real collision while this was being written. Suite is 195 tests.
2026-09-22 — Signing up is quiet: one e-mail, sent when the access actually exists
- A new person used to get three e-mails for one sign-up. "Verify your NovuHub email", then "your request to join was received", then — later — "you've been approved". The first two arrived before they had been let in and asked them to act on nothing. The verification link in particular gated nothing at all:
NOVUHUB_REQUIRE_EMAIL_VERIFY was off, so clicking it flipped a flag and changed no behaviour. They now get one message, at the only moment something has actually changed for them. - Registration is silent. Filling in the form creates the account, signs the person in, and puts a request in front of the admins. The screen they land on says their request is with the team, that they can close the page, that they will be e-mailed at their address as soon as it is approved, and that nothing else is needed from them — so nobody sits refreshing a page that will not change on its own.
- You still get told. One e-mail to the owner and admins — X has requested to join — and the request appears in the join-request panel as before.
- Approving is where access is decided, and it can no longer be skipped past. The review dialog's button starts disabled and reads Choose their sections first; it becomes Approve & grant access only once you have ticked sections, chosen Shared areas only, or accepted what the person's invitation asked for. Settings → Access stays what it has been since Sunday: the place to change someone's access later, not to set it for the first time.
- E-mail verification is gone, not merely switched off — the
/verify-email/<token> and /resend-verification routes, the verification mail and its serializer, the NOVUHUB_REQUIRE_EMAIL_VERIFY flag and its gate, the verify_email.html screen, and the "verified / not verified" chip and Resend verification button in Settings. Approving a request is what establishes that a person is real and wanted; a link to their own inbox never was. The user.email_verified column stays because SSO and SCIM use it to record that an identity provider vouched for an address, but nothing gates on it. - Unchanged: the first person to register still bootstraps the workspace as owner with no approval, people arriving through an invite link still skip the request, and declining still sends nothing.
- Measured end to end before and after: registration now produces exactly one e-mail (to the owner) and zero to the newcomer; approval produces exactly one (to the newcomer); the newcomer then reaches the workspace both on the open tab and on a fresh sign-in. Suite is 187 tests.
2026-09-21 — Assessment v13: a harder bar, and the four things it found
- The bar was raised, and it found real defects. v13 re-runs the 175 criteria of v11 and v12 unchanged — the product now passes 132 of them, against 131 in v12 and 90 in v11 — and adds 80 new strict criteria plus four more checks on every one of the 30 views (420 section checks, up from 300) and twelve more live probes (34, up from 22). Overall 188 of 255. Everything below was measured, reproduced, fixed, re-measured and pinned by a test in the same pass.
- Decision Intelligence was owner-only in the interface and nowhere else. A second account seeded into the workspace as an ordinary member asked the server for the workspace document and got all of it,
analysis section included; GET /api/analysis/report/<id>/export.pdf answered 200 with a 22,914-byte branded diagnostic PDF, and the sample report answered with 143,398 bytes. The export route said as much in a comment. The server now removes the section from every document it hands to anyone but the owner — the live read, the conflict response and the backup download — and both export routes answer 403. Since a non-owner no longer receives the section, their save would have erased it, so the server restores it before storing: an admin's ordinary edit is saved, the owner's material survives it, and a member's edit still works exactly as before. - A password-reset link could be used more than once. Spending a link and then posting the same token again changed the password a second time, for the full hour the link lived. The token now carries a fingerprint of the password hash it was issued against, so using it once invalidates it; a second attempt is told the link has already been used.
- Changing your password left everyone else signed in as you. Two sessions, a reset in one, and the other stayed fully authenticated — the one thing changing a password after a compromise is supposed to fix. The session identifier is now bound to that same fingerprint, so a password change signs out every session opened with the old one. Sessions written before this change keep working, so nobody is signed out by the deploy itself.
- One colour-contrast regression had crept back onto all 30 views. The "you" avatar in the top bar, added with the profile-photo work, painted 10 px bold white initials on
--green-fill — 3.29:1. It now uses a dedicated --green-badge (5.88:1 light, 6.63:1 dark). Widening the sweep caught two more: the project chip was painting white on the raw accent (4.47:1 in dark) and the mind-map tick was white on --green-fill. A new test walks every rule that puts white text on a coloured token and fails below 4.5:1. The audit re-run measures colour-contrast back at zero in both themes. - What the strict tier says is still open. The cross-tenant sweep is clean (12/12) — a stranger gets nothing from 88 GET routes and no write is accepted anywhere. Session lifecycle is now 8/8, resilience 7/7, data lifecycle 7/7. The weak spots are interaction-level accessibility (6/13 — a dialog that never takes focus and has no accessible name, a document title that never changes, focus rings missing on 21 of 535 sampled controls across six views), the tighter bundle budgets (553 KB minified against a 300 KB bar), build reproducibility (the base image still moves under the build, no SBOM, no dependency automation) and the live server, which is unchanged: no compression or caching on static assets, HTTP/1.1 only, and no SPF, DKIM, DMARC, MX or CAA records.
gunicorn now recycles workers (max_requests), the release build pins esbuild instead of fetching whatever is newest, and docs/deploy-notes/dependabot.yml is ready to paste.
2026-09-21 — Continuous integration is green again after 28 failed runs
- Every push has been failing CI since the workflow was added, and the nightly failure e-mails were the symptom. Two independent breakages, both the same class of mistake: an apt package name that carries a library soname. Those names are renamed between distribution releases, so a list that worked when it was written fails the day the base image or the runner image rolls forward.
- The image build (
docker-build, 14 s): python:3.12-slim moved from Debian 12 (bookworm) to Debian 13 (trixie), where the qpdf runtime library is libqpdf30. The build died on E: Unable to locate package libqpdf29, exit code 100, before a single layer of the application was copied. - The test job (
test, 18 s): libjpeg62-turbo is a Debian package name and does not exist on the ubuntu-latest runner (Ubuntu 24.04) — E: Package 'libjpeg62-turbo' has no installation candidate. libqpdf29 is broken there too, renamed to libqpdf29t64 by the 64-bit-time transition; apt simply stopped at the first of the two. - The fix is to stop naming them. None of the three fragile packages ever had to be listed: libjpeg arrives with
libgdk-pixbuf-2.0-0, libffi is already in the base image because CPython links it, and the pikepdf wheel ships its own vendored libqpdf-*.so — measured, not assumed: the installed wheel reports qpdf 11.9.1 while the system library is 11.9.0, and ldd resolves it inside pikepdf.libs/. The image and the workflow now install only ABI-stable names, every one of which is verified present in both Debian trixie and Ubuntu 24.04. - A third failure was waiting behind the first.
tests/test_static_gzip_and_caching asked for novuhub-app.min.js, which is a build product of build_frontend.sh and is deliberately gitignored — so it is absent from any fresh checkout and the test job would have gone red again the moment the apt step was fixed. The test now exercises whichever bundle is present; the behaviour it checks (immutable versioned URLs, Vary: Accept-Encoding, one-hour cache for unversioned URLs) is the same for both. - The workflow now also does what the deploy chain does, so a green run means more than it did: it lints with ruff, runs
build_frontend.sh (which syntax-checks all 30 source modules and produces the minified bundles the static tests exercise), and fails if the committed bundles have drifted from app/static/src. The three GitHub actions were moved to v7, which clears the Node 20 deprecation warnings that appeared on every run. tests/test_ci_contract.py is the guard: it rejects soname-versioned package names in the Dockerfile, fails if the workflow and the Dockerfile stop installing the same libraries, and keeps pikepdf optional and its installation non-fatal. Suite is 179 tests.
2026-09-21 — Access is decided once, when you let someone in · your own photo in the top bar
- The access you choose when inviting someone was being thrown away.
/api/members said nothing about it, so whichever browser noticed the new member first created their entry in the workspace document with shared areas only — and because every open session re-syncs the roster every few polls, that was almost always the admin's own browser, moments before the invited person had even finished loading. The admin then had to define the access a second time under Settings. The server now reports what was granted (from the invitation, or from the approval), the client seeds the person's entry with it, and an entry that was already flattened is repaired once — so the people you have already invited are corrected on the next sync. An admin's later edit in Settings is never overwritten. - Approving a join request is now where access is chosen. The request list shows the sections the invitation asked for, and Review & approve opens the section list, pre-ticked, with Select all and Shared areas only. The decision is stored with the approval, so the person starts with exactly what you picked. Settings → Access now says what it is for: changing someone's sections later.
- Your profile photo appears in the top-bar avatar. The presence avatars are built from the server's online list, which had no photo and no way to match a person to their entry in the workspace document; the list now carries the e-mail, so your own avatar is your photo and is correctly marked as you. Teammates without a photo still show initials.
2026-09-21 — My Emails triages the right window again · mentions now reach people by e-mail
- My Day → My Emails was listing the whole mailbox. It is meant to hold the mail that arrived while nobody was working — Settings → Working hours says exactly that — but the filter had been removed. It is back, and it follows the clock: before the day starts you see everything since the last working day ended; during the day the list holds last night's batch steady instead of shifting under you; after hours it collects what has come in since you stopped. Weekends are skipped, so Monday morning includes them. A Working days setting sits next to the hours, the panel says which window it is showing, and a Show all switch reveals anything that arrived during working hours rather than hiding it.
- Being mentioned or assigned now sends an e-mail. Every notification — a task or project assignment, a mind-map mention or reply, a message mention, a document review request, a risk-radar mention, a support reply — is queued for the background worker and delivered through whatever mail provider the server has configured. Nothing is lost while none is configured: the queue holds (capped), and Settings → Notifications says how many are waiting. There is a workspace switch for direct notifications and a personal one, so anyone can stop their own copies without changing it for the team.
- Each section carries its own red badge. Every notification records which section it belongs to, and the sidebar shows a red circle on that item — rolled up onto the group header while the group is collapsed — that clears when you open the section. The bell keeps the full list until you read it.
- Clicking a notification for a message or a support ticket used to do nothing: those were stored as a bare section name where runnable code was expected. They now open the section.
2026-09-21 — Colour contrast: every failing pair fixed, light and dark
- The v12 assessment measured 205 text/background pairs in light and 208 in dark that fell below the WCAG AA 4.5:1 bar across the 30 audited views. They came from four causes, and all four are now fixed at the source rather than screen by screen. The audit re-run reports 0 colour-contrast violations in either theme (axe: 330 → 125 serious nodes in light, 333 → 125 in dark, everything left being other rules), and the per-view scorecard rose from 231/300 to 254/300.
- Every semantic colour now has two values instead of one.
--green, --amber, --red, --blue, --gray, --purple and --pink are the ink — the value used when the colour is text — and are deep enough to be read on their own soft background and on the page (amber chips were at 2.86:1, green at 2.85:1, red at 3.95:1). The vivid value lives on as --x-fill for dots, meters, markers and badges, so nothing that is only a splash of colour changed. - Colours people chose are made readable when they are drawn, not assumed to be. A member's avatar colour, a project's colour on a calendar event, a team tag, a deal chip: the bundle now computes the tint and the text colour for whatever the value is, and keeps the source colour alongside so a theme switch re-derives them without re-rendering the page (an open dialog used to keep its light-mode ink). White initials on the four palest palette colours were as low as 2.77:1.
- The accent is readable as text in dark mode. The chosen accent is written onto the page as an inline custom property, which outranked the dark theme's own value — so dark mode painted the light indigo on a dark indigo tint at 2.39:1, on the selected sidebar item of all 27 views. Text uses of the accent now go through
--accent-ink, derived from whatever accent is chosen; solid accent fills gained --accent-on, which flips to near-black for the pale accents (yellow, lime, cyan, teal) that white cannot be read on. All 22 selectable accents were re-checked in both themes. - Other-month calendar days were dimmed with
opacity, which dimmed their text as well and pushed the day numbers under 3:1; the cell is muted with colour instead. tests/test_contrast.py recomputes the WCAG ratios from the committed token values, so a future palette edit that drops a pair below the bar fails the suite.
- + Add Receivable and + Add Payable no longer sit in the page header on the far right. Each now appears directly under the Finance tab strip, on the left of the same row as the currency picker and above the period filter — next to the ledger it adds a row to. The other Finance tabs are unchanged.
2026-09-21 — Finance → Archive remembers the folder you are in
- Opening an Archive folder and refreshing used to drop you back at the Archive root, and the same thing happened on its own while you were working — a background sync could hand the view the workspace document a moment before the folder list had merged in, and the view then forgot where you were. The open folder (including a subfolder, and the imported P&L months / Budget months folders) is now kept across reloads, and a sync that arrives without the folder list shows the root for that one render without forgetting the folder.
- It is only forgotten when it is genuinely gone: a deleted folder drops you into its parent, an imported-months year with no months left drops you to the root, and a link to a folder that no longer exists lands on the Archive root instead of an empty page.
2026-09-21 — Table sweep: every table in the product checked and aligned
- Settings → Access (reported): the Sections granted cell carried the small-text class, which had made the cell an inline box — each cell then drew its own short divider at its own height, the row grew to 63 px and a person's name was pushed onto a clipped second line. Utility classes can no longer change what a table cell is (
th.mini,td.mini{display:table-cell}), and a guard test scans every class written on a <td>/<th> in the front end and fails if a plain .class{display:…} rule could do it again. - Centred and left-forced headers (Portfolios → Capacity "Tasks", CRM → Reports "Deals"/"Attainment", Finance → Consolidation "Rate → €"):
.fin-table th{text-align:left} carries the same weight as the ta-c utility, so those headers stayed left above centred columns. The alignment utilities are now element-qualified and win in every table. - Wide tables can be scrolled instead of cut (CRM → Contacts, Quotas): a table wider than its panel was simply clipped — 292 px of columns were unreachable at phone width. After every render, any table that overflows and has nothing scrollable around it is wrapped in a horizontally scrollable box; the Sustainability/ESG section is left untouched.
- Avatars are inline: an avatar written next to a name inside a cell was a block box and forced the name onto its own line (Settings → Access, Read analytics, CSAT by agent). As a flex/grid item it still behaves exactly as before, so no other placement changed.
- Right-to-left: the alignment utilities, money columns, action columns and invoice columns now all have RTL mirrors, so a table's headers and numbers stay on the same side when the interface direction flips.
- Verified by measuring, in a real browser, 71 tables / 2,266 cells across ~110 view states (every section and sub-tab, detail pages and modals) at 1440 px light, 1440 px dark, 390 px mobile and
dir="rtl": header-to-cell offsets, padding, ragged rows, clipped content, clipped popovers and page overflow are all clean. 144 before/after screenshots: 124 pixel-identical, the rest only the intended header/avatar changes (plus the forecast's random simulation and clock-dependent timestamps).
2026-09-21 — Product fixes: tables, navigation state, previews, team filter, subscribers
- Table alignment (Portfolios → Capacity, Finance → Receivables/Payables, Archive): three CSS root causes fixed at once —
.fin-check was declared twice, so display:inline-flex landed on the <td>/<th> and knocked the checkbox column (and every column after it) out of the table layout; the ta-r / ta-c utilities were used on ~100 cells but only ever styled for <th>, so right-aligned headers sat above left-aligned numbers; and date cells wrapped mid-value ("19 Sept" broke in two). Headers and data now line up exactly in every finance, HR, CRM and portfolio table. - Cash flow (and 32 other places):
.mini was a flex container, which turned each sentence into separate flex items — the empty state read "Import a bank statement on the | Bank | tab and …". .mini is plain inline text again (a <div class="mini"> is a paragraph), so notes, hints and footnotes read as sentences. - Gantt: day columns were a fixed 28 px, so a short schedule drew a narrow strip and left the rest of the panel blank. The calendar now stretches its columns to fill the width available (up to 64 px/day) and keeps 28 px + horizontal scrolling for long ranges; dragging converts pixels to days with the same scale.
- SOPs & Docs and Mind Map remember where you were: a refresh used to drop you back at the top level. The open folder, open document and open map are restored (and silently forgotten if the record was deleted meanwhile).
- Preview before downloading: a 👁 Preview button on SOP files, Archive invoice files and invoice attachments opens PDFs and images inline, shows text/markdown/CSV as text, and offers Open in new tab / Download from the preview.
- Projects belong to a team, and every task tab can filter by it: the project dialog's team field is now labelled and explained, and a Team filter sits in the Projects & Tasks toolbar — it applies to Board, List, Timeline, Gantt and Mind maps, narrows the project dropdown to that team, and is remembered in saved views and across reloads.
- Active vs inactive people: the FTE detail explains the rule (status Left, an end date in the past, or a start date in the future) and each greyed row now says which of the three applies; a person's profile gained a one-click Deactivate / Activate for people managers, and the Directory marks inactive people.
- Subscribers dashboard (owner only): Settings → Subscribers shows MRR, annual run rate, paying customers, trials, billed seats and average revenue per customer, a per-plan breakdown and every workspace with its plan, status, seats, value and renewal date. Gated to the platform owner by the same allow-list as Decision Intelligence — the tab is absent and the endpoint returns 403 in a customer workspace, and no payment data is involved.
2026-09-21 — Hotfix: sharding-safe server writes · Connector v2
- Fix (important): with per-record sharding on (the Phase 1 default), several server-side features still read the stored document the old way — the automation engine, the connector/agent API, the public REST API, the ticket portal, the calendar feed, bookings, lead forms, help-centre feedback, the finance mailbox, GDPR export and account scrubbing. On a sharded workspace they saw no tasks/tickets/docs, and a write from them would have shadowed the sharded rows. All of them now go through one sharding-aware data layer (
app/docstore.py: load_doc / cas_mutate), covered by tests/test_docstore.py, plus a guard test that forbids reading WorkspaceData.data directly anywhere else. If NOVUHUB_SHARD_RECORDS=0 was added as a stop-gap it can stay (single-document mode) or be removed after this deploy. - Connector v2 (Claude Desktop MCP,
mcp/novuhub_mcp_server.py + /api/agent/*): new tools search, list_projects, list_tasks, list_docs, get_doc, list_members, create_folder, create_doc, update_doc (versioned), create_project (with first tasks), create_mindmap (project + ideas + tasks + edges, or for an existing project), add_idea, update_task (status by label, assignees by name/e-mail), create_ticket. People can be given as ids, names or e-mails; statuses as ids or labels. Guardrails unchanged: no delete, no settings, no e-mail, no money; every write audited as "AI agent (connector)". Copy the script to ~/mcp/ and restart Claude Desktop to get the new tools.
- Internationalization: full-UI translation layer (every rendered text, placeholder and tooltip is translated at render time; the Sustainability section is intentionally left in its regulatory English) with a 2,700-phrase German dictionary loaded on demand (
app/static/i18n/de.js); numbers, currencies, dates and times follow the chosen language; per-person time zone (Settings → General and the member editor) applied to every time display; right-to-left direction switch (<html dir>) with mirrored layout rules. - Front-end performance: the bundle is split into a core and 12 lazily loaded chunks (finance, HR reports, portfolios/workload, CRM, messages, calendar, people, mind map, docs, learning, settings, analysis) — the first page load ships 159 KB of JavaScript (gzip) instead of 251 KB, no owner-only code, and 249 KB of static assets in total instead of 515 KB; Poppins is served as subset WOFF2 (11 KB per weight instead of 156 KB); the app serves pre-compressed
.gz files itself with immutable caching for versioned URLs; the playbook data loads only with the Analysis section. build_frontend.sh now produces the chunks (Node.js is required on the server for the build step). - Support & documentation: in-app Help & feedback button (help-topic search, guide, tour, shortcuts, docs, changelog, status; questions / problems / ideas become Customer Service tickets with page context); product documentation under
docs/guide/ served at /docs/ on every installation (with search) and buildable as a site with mkdocs.yml; /changelog fixed (it rendered "Coming soon" because of a wrong path); NOVUHUB_SUPPORT_EMAIL shown in the panel and docs footer. - Also:
.gitignore covers all generated build outputs; assessment scripts updated for the new bundle layout.
2026-09-21 — Phase 1: security, architecture, code quality (assessment v11 follow-up)
- Security: fixed a stored XSS (member display names in Risk Radar signals and the project header); API tokens now hashed at rest (legacy rows migrated on boot); SSRF guard for outbound webhooks (
app/netsafe.py); request-body cap (JSON 413); the app refuses to start with the placeholder SECRET_KEY; 14-day sessions / 30-day remember cookies; 12-character minimum + common-password block list; CSP without unsafe-eval; /.well-known/security.txt; dependency advisories cleared (Flask 3.1.3, python-dotenv 1.2.2, pdfminer.six, WeasyPrint 70); bandit at zero medium/high. - Identity: OpenID Connect single sign-on (
/auth/oidc/*, any IdP via discovery, PKCE) and SCIM 2.0 user provisioning (/scim/v2), both env-gated; accounts can be deactivated. - Architecture: per-record sharding on by default; Redis-backed shared API rate limit (with
Retry-After) and real-time pub/sub when REDIS_URL is set; background engine runs as its own process (worker.py + deploy/novuhub-worker.service); cross-workspace isolation test. - Code quality: front-end split into 27 source modules under
app/static/src/ (bundle is generated by build_frontend.sh); app-shell CSS and runtime moved to static files (template 2,520 → 81 lines); PDF renderer split; the four most complex functions sectioned into small builders (verified byte-identical output); ruff clean with committed config; typed route handlers; deploy notes moved to docs/. - Tests: 111 backend + 17 front-end (new: netsafe, hardening, SSO/SCIM, scale, engine rules, XSS guards).
2026-09-20 — Reach, polish and hardening
- Public reach: marketing landing page with SEO/social metadata, sitemap and robots; OpenAPI 3.0 spec at
/api/v1/openapi.json; public /status, /changelog and /developers pages. - Scale & quality: minified front-end build with source maps, service worker, accessibility fixes to zero axe violations, end-to-end tests; CI runs backend and front-end suites.
- Onboarding: verify-first sign-up, join-request notifications, invite by email with per-section access, interactive login tiles; redesigned auth pages.
- Inbox triage (My Emails) alongside AP/AR invoice reading; visual overhaul of the app, Mind Map overhaul (resizable nodes, fixed zoom box, pill fixes), Gantt narrow-bar labels.
- P0 hardening: CSRF Origin guard on all state-changing APIs, Impressum and consent, monitoring and go-live runbooks. Env:
NOVUHUB_CSRF_TRUSTED_ORIGINS, NOVUHUB_COMPANY_*, NOVUHUB_TOS_VERSION, NOVUHUB_PRIVACY_VERSION. - Alpha waves deployed the same day: real-time SSE updates (
NOVUHUB_REALTIME_SSE), compliance & legal (GDPR export/erasure, PII log, retention), per-seat billing through a merchant of record (NOVUHUB_BILLING_*), access reinforcement.
2026-09-19 — Portability, storage and access
- Portable file storage: attachments and photos on local disk or any S3-compatible bucket (
NOVUHUB_S3_*); Docker Compose; everything env-driven (APP_BASE_URL). - Access polish: server-side section enforcement, removed users stay out, new users gated until approved; Decision Intelligence visible to the founding workspace owner only; permanent account deletion.
- Core polish 3–7: planning & scheduling (start dates, critical path, baselines), calendar/portfolio/report/message/learning deep fixes, back-navigation fix, toolbar and layout polish, login fix, pikepdf install fix.
2026-09-18 — Core polish and the server-side engine
- Phase 1: background automation engine on the server (
NOVUHUB_SCHEDULER, NOVUHUB_ENGINE_MINUTES), Mind Map upgrade, first-run welcome tour. - Phase 2 installments A–D: finance-grade & commerce (double-entry journal, XRechnung/ZUGFeRD, DATEV export), AI signals & command bar, platform hardening (offline, tamper-evident audit, faster shell), per-section delight.
- High-, medium- and lower-priority gap waves from the first assessment; core polish 1–2.
2026-09-17 — Decision Intelligence and the visual overhaul
- Analysis section with the NovuHub MCP connector for Claude Desktop, decision-intelligence templates and branded PDF export; report branding settings.
- Overhaul 1–5: navigation, dashboard, typography and section layouts.
2026-09-14 — 2026-09-15 — Sustainability, CRM and finance intake
- Sustainability / ESRS double-materiality module (five installments); CRM & deals; AP/AR invoice detection from documents; HR reports; language switcher; icon set; password reset and new login page.
2026-09-09 — 2026-09-11 — Foundation
- NovuHub skeleton on the server, full UI, steps 2.1–2.11 (projects & tasks, portfolios, calendar, workload, docs, learning, messages, tickets), finance UX and graphics revamp, public marketing landing page.
Feature overview (all waves)
- Projects & tasks, portfolios, CRM, finance/controlling with double-entry + XRechnung/ZUGFeRD/DATEV, HR, LMS, customer service, knowledge base, mind maps.
- Sustainability / ESRS double-materiality and CSRD-ready reporting.
- Decision Intelligence: AI growth diagnostics on the owner's own Claude connector.
- Public REST API v1 + signed outbound webhooks; per-record sharding + delta-sync (opt-in); per-seat billing engine (test mode).