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-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).