MSPercury

Changelog

What's changed in the latest patches. Newest on top.

RSS
2 weeks ago

Three tiers · 30-day Pro trial for every signup · Founders Program

43
  • Added New Pro tier at $99/€99/£84/CA$134/MX$1849 per month. Same single-user feature set as Solo with 4× the Bob token allowance (8M/month). Sits between Solo and Team for operators who run Bob as their main surface.
  • Changed Sign-up now starts a 30-day Pro trial automatically — no card, full Pro feature access including Bob. After 30 days you pick Solo, Pro, or Team. The "free for the first 100" cohort is closed to new signups; existing Founding-100 members keep their permanent free access.
  • Added Founders Program at /founders. Apply if you're still building your MSP — get a 90-day Pro trial (instead of 30) and an optional "free until your first paying customer" arrangement. Every application reviewed by hand within 5 business days.
  • Added Trial-end emails. We send a heads-up 7 days before, 1 day before, and on the day your trial expires — each with the link to pick a plan. The workspace flips to locked-status automatically once the trial is over; your data stays intact.
  • Changed /pricing rebuilt as a three-card grid (Solo / Pro / Team) with Founders Program and Founding Members (50% lifetime discount for the first 100 paid signups) cards below. Currency switcher above repricing all three with one click.
  • Changed /billing upgrade picker shows three cards instead of two; active-tier banner now recognises Pro alongside Solo and Team.
  • Added Landing page got two new sections — three short "Bob does X" use-case cards (quick invoice, customer lookup, assessment-to-quote) and a three-step "how it works" walkthrough above the feature list.
3 weeks ago

Team time-sheet · /assistant is one page now

11
  • Added Team time-sheet at /time. Log billable hours per project — type them in yourself, or ask Bob ("log 2h on Acme for cabling"). Each entry carries date, hours, hourly rate, and an optional note; totals roll up per customer for invoicing. (DE/EN/ES)
  • Changed /assistant is one page now. Pick a conversation from the sidebar and it opens in the right rail — no more full-page reload between conversation switches. Old /assistant/<id> links keep working (they redirect).
3 weeks ago

Invoices, workspace timezone, Network → Pro · big quality pass

2231
  • Added Invoices. Accept a quote, hit "Create invoice", get a numbered invoice ready to send. Gap-free numbering (RE-YYYY-NNNN), DIN-5008 layout, per-period VAT block, reverse-charge handling — all inherited from the quote PDF. Send as PDF in one click, mark paid; sent invoices lock against further edits. Structured e-invoice / ZUGFeRD output is the next step. (DE/EN/ES)
  • Added Workspace timezone setting. Workspace owners can pick the org's IANA zone in Settings → Workspace; the date on outgoing quote PDFs now pins to it, so a quote issued at 23:30 local doesn't print yesterday's date. Leave it on "Automatic" to derive from your Country. (DE/EN/ES)
  • Changed Partner Network is a Pro feature now. A Free workspace clicking "Network" lands on /billing with a short explanation; Solo and Team get the directory + lead-forward flow as before.
  • Changed Free tier is now capped at 3 quotes per month — Solo and Team stay unlimited. A small counter on the quote list shows what you've used, and the wall enforces at every create path (manual, from a Checkup, from the wizard auto-quote).
  • Fixed Big sweep — roughly 70 fixes across the app. Highlights: the customer portal no longer crashes on a project with task comments; the iOS app no longer hits a redirect loop after the trial ends; the embed and self-check questionnaires finally show their conditional follow-up questions (they were silently dropped); the email-send modal now surfaces a real error when delivery fails instead of pretending it worked; the AI summary overlay no longer hangs on a failed generation.
  • Fixed Checkup-derived quotes (the ones without a linked project) now show up everywhere they should — operator quote list, customer portal, share link, PDF and CSV export. They used to disappear silently when a project was deleted or when the quote came from a project-less Checkup.
  • Fixed PDF and portal dates render the right calendar day across timezones now. Audit dates on Checkup reports stay anchored to the day you picked in the date input, regardless of where you or your customer sit.
  • Security Hardened the partner-network's lead-forwarding flow and tightened a few authenticated-session checks.
3 weeks ago

Fixes - square app icon, cleaner changelog feed

11
  • Fixed The PWA / home-screen / taskbar icon showed the wide MSP banner shrunk onto a white square, so it read as a tiny floating logo. It is now a proper square app icon — a green tile with the MSP mark centred. If your device still shows the old one, remove and re-add the app (OS icon caches are stubborn).
  • Changed Each change in the /changelog RSS feed is now its own line with a kind marker (added / changed / fixed …) instead of one run-on paragraph — so Discord feed embeds and desktop RSS readers render it as a scannable list.
3 weeks ago

Settings rebuilt - collapsible sections, mobile & iOS-app polish

11
  • Added Settings is now organised into collapsible sections — tap one to open it instead of scrolling a single long page. On desktop the sidebar still jumps straight to any section.
  • Changed Settings got a mobile and iOS-app pass: bigger tap targets, no zoom-jump when you focus a field, safe-area spacing, a haptic tap when a change saves, and fields that scroll clear of the on-screen keyboard.
3 weeks ago

Dark-mode color-safety audit · automated check + token rules

21
  • Added Dark-mode color-safety audit script (npm run check:darkmode). Scans every .astro/.tsx/.ts file for stock Tailwind palette uses (stone-, gray-, emerald-, rose- etc.) that have no paired dark: variant. Strict mode (check:darkmode:strict) exits non-zero, ready to wire into CI as a pre-build gate once the existing offender count drops.
  • Added Color-token rules documented as a tall comment block at the top of src/styles/global.css — the six rules covering surfaces, text, borders, inverted buttons, accent, and pills. Every developer (and AI assistant) touching styles sees the rules at the source-of-truth file.
  • Fixed Ticket system pages (/superadmin/tickets list + detail, /help/tickets list + thread, support FAB dialog) render correctly in dark mode — conversation bubbles, status pills, reply composers, and modal backdrops no longer leak white-on-white or black-on-black text.
3 weeks ago

Customer support overhaul · ticket system v1 · in-app chat widget · /help hub

61
  • Added Help & Support hub at /help — single discoverable entry point for docs, changelog, feedback, Discord community, and direct support. Replaces four separate AppNav entries with one card grid. Shows a count badge when you have open tickets.
  • Added Sticky support-chat widget in the bottom-right corner of every authenticated page. Click the bubble, write a subject + message, pick a category (Support / Billing / Privacy). Submits land instantly in /superadmin/tickets and trigger an operator notification email — no IMAP roundtrip, no page reload.
  • Added Customer-facing ticket views at /help/tickets (list of your own threads, sorted by last activity) and /help/tickets/[id] (full conversation with inline reply composer + "mark as resolved" option). Strictly isolated per user by from_email; cross-tenant access redirects to the list without leaking existence.
  • Added Built-in support ticket system in /superadmin/tickets. A background worker polls support@, billing@ and privacy@ inboxes every 60 seconds, ingests new mail as tickets, auto-tags by destination mailbox, and auto-links to a tenant when the sender email matches a known workspace.
  • Added Operator reply path supports both web and Outlook: the inline composer on /superadmin/tickets/[id] sends a threaded reply through noreply@mspercury.com (RFC 5322 In-Reply-To + References headers). Replying from the operator personal mailbox (l.flores@it-flores.de) is also recognised by the IMAP ingest and forwarded onward to the customer transparently.
  • Changed Customer-facing email Reply-To moved from the operator personal inbox to support@mspercury.com. The mailbox forwards onward to the operator at the provider level, so the routing change is transparent to senders. Mentions in the blog and one-off operator replies updated to match.
  • Added Reply-blocklist for send-only / loop-prone addresses (noreply@, support@, billing@, privacy@). The reply UI shows a warning banner instead of accepting a reply that would silently bounce at the destination MTA. Prevents the most common operator foot-gun: replying to a smoke-test ticket whose from_email is noreply@.
3 weeks ago

Public-CheckUp asks for phone too · stale items pruned from roadmap + known-issues

21
  • Changed Public-CheckUp contact form (DE/EN/ES) now requires a phone number alongside email. Operator feedback was that email-only leads were too easy to ignore on the prospect side; a number lets the MSP call back the moment a high-score lead lands. Applies to both /check/{slug}/questions and the iframe embed at /check/{slug}/embed/questions.
  • Removed Stale entries pruned from /docs known-issues (DE/EN/ES): "5 MB iOS photo cap" (actual cap is 8 MB), "custom footer not in PDF yet" (renders since the May 2026 quote-PDF refresh), "no bulk customer import" (CSV import lives at /customers/import), "SVG logo breaks branding" (SVG is in the allowed-mime list), "AI ChatGPT path untested" (both Claude + OpenAI providers ship at parity).
  • Changed Roadmap (DE/EN/ES) refreshed: Stripe direct-key integration moved from "Now (in flight)" to "Already shipped" — that one already lives at /settings/integrations. Public Read API + Webhooks slipped from Q2 to Q3 2026 to match reality. Native iOS app entry kept.
3 weeks ago

Tiered pricing · Solo €29/mo · Team €49/mo · Mexican peso support

34
  • Added New "Solo" tier at €29/$29/CA$39/MX$549 per month, unlocking every Pro feature for single-operator MSPs (CRM, marketplace, branding, advanced reporting, AI with BYO key, multiple Public-CheckUp URLs, Stripe direct-key integration, API). Existing Pro features now start at Solo.
  • Added "Team" tier at €49/$49/CA$65/MX$929 per month — everything in Solo plus multi-user workspace (additional admin + member seats). Existing Pro subscribers at €49 are grandfathered onto Team via the migration.
  • Added Mexican peso (MXN) added as a supported billing currency across /pricing, /billing, and the Stripe checkout flow. Locale + CF-IPCountry pick MXN by default for Mexican workspaces.
  • Changed /pricing rebuilt as a three-card layout (Free / Solo / Team) with a central currency switcher above the cards so a single click re-prices both paid tiers. FAQ updated to explain Solo vs Team and the Stripe Customer Portal tier-switch path.
  • Changed /billing rebuilt: shows the active tier (Free / Solo / Team) with a status pill, two side-by-side upgrade cards for unsubscribed workspaces, and a "Manage subscription" link for active subscriptions (where the Stripe Customer Portal handles tier changes pro-rata).
  • Changed Plan-gating helpers (`isPro` / `getPlan` / `getLimits` / `requirePro`) updated to recognise three tiers. `isPro` stays true for Solo + Team (so every pre-existing call-site keeps working). New `requireTeam` gate added for future multi-user features.
  • Changed Stripe webhook now decodes the active price ID into a tier (Solo / Team) on every subscription event and writes it to `organizations.subscription_tier`. Tier changes via the Stripe Customer Portal are reflected within seconds of the event arriving.
1 month ago

Signup is open · branded error pages · welcome tour · multi-admin lead alerts · per-invite question set

242
  • Removed Workspace-signup invite codes are gone. Anyone can create a workspace at /signup with email + PIN, no code required. The old gate was broken anyway — codes were never marked used, the beta-flag never propagated, and the gate was bypassable by POSTing the action URL directly. /superadmin/invites and /settings/invites pages were deleted.
  • Added Welcome modal on first signup now shows a 4-card feature tour (Public CheckUp, CheckUp wizard, Service catalog, Quotes + PDFs) above the setup form, each linking to the surface so you can click around before filling anything in. Listens for the canonical ?welcome=signup cue from the post-signup redirect (was ?welcome=1, which also triggers a fresh Google Ads conversion event).
  • Added Branded English error pages for /check/{slug}/* and /self-check/{token} when the link is dead or revoked. Replaces the bare-text "Diese Seite ist nicht verfügbar." 404 that prospects in EN/ES locales used to see. Inline-CSS, MSPercury logo, noindex.
  • Fixed Lead-alert emails (DE/EN/ES) now fan out to every admin in a multi-admin workspace, not just one inbox. Each admin's subject + body is rendered in their own preferredLanguage, matching the per-admin push-notification fan-out behaviour. Same for the new-lead push title and body — those were hard-coded German.
  • Fixed Customer self-check is now pinned to a specific question set at invite-time (DE/EN/ES). If you flip your default catalog after sending an invitation, the customer still sees the catalog you originally meant — no surprise mid-flight changes. Existing pending invitations keep the previous behaviour (fall back to the org default).
  • Fixed Operator flash messages + AI quote-draft titles localised across the leads, customers, customer-contacts, and AI surfaces (DE/EN/ES). Removed the leftover German strings that flashed on EN/ES workspaces ("Ansprechpartner aktualisiert.", "Bereits konvertiert.", "Lead im Marketplace veröffentlicht.", quote titles starting with "Angebots-Entwurf aus CheckUp", etc.).
  • Fixed Post-signup redirect chain collapsed: /signup/complete now lands directly on /dashboard?welcome=signup instead of bouncing through /onboarding?welcome=1. One redirect, correct cue, no wasted hops.
  • Removed Dead defensive code around i18n key lookups: stripped the `t("nav.leads" as any) ?? "Leads"` fallback pattern in AppNav + the settings sidebar. The keys exist in every locale; the dead fallbacks had German strings ("Netzwerk", "CheckUp-Einstellungen") that would never activate but represented a latent silent-German risk.
1 month ago

Public pages default to English · redesigned changelog layout

2
  • Changed Every public surface (landing, /imprint, /legal/*, /pricing, /blog, /docs, /changelog, /feedback) now defaults to English when the visitor isn't signed in. The Accept-Language browser-locale auto-detect path was removed — a visitor who actively wants a different language uses the picker in the footer (English · Deutsch · Español) or appends `?lang=de` / `?lang=es` to the URL. The explicit pick persists across navigation via a dedicated marker cookie. Signed-in users keep their saved language unchanged.
  • Changed /changelog layout reworked for both desktop and mobile. Desktop gets a wider canvas (~1152 px), a sticky year-jump rail on the left, and a year-header rhythm above the first entry of each year. Mobile stacks date + title with a short-date label and bigger tap targets. New sticky toolbar offers per-kind filter chips (Added / Changed / Fixed / Removed / Security with live counts), expand-all / collapse-all buttons, and a copy-link affordance per entry that writes the deep-link URL to the clipboard. Hash navigation (e.g. `#2026-05-12` from the RSS feed) now force-opens the linked entry and scrolls it into view.
1 month ago

Inline-expand forms moved into modals — no more layout-sprenger when adding contacts, catalogs, tasks

14
  • Added New reusable `<FormModal>` component (same `<dialog>`-based pattern as the existing EmailSendModal / OnboardingWizardModal / PdfModal). Trigger button + native modal, ESC + backdrop close, auto-reopen with an error banner if the previous submit returned a validation error, optional URL cancel-href for unwinding edit-state-in-URL.
  • Changed Customer detail (/customers/[id]): "Add contact" + per-row "Edit contact" no longer expand inline and rip the card header apart. Both now open a proper modal. Same field set, same validation; the card layout stays put.
  • Changed Settings → CheckUps (/settings/checkups): "New question set", "New landing", "Edit landing", "New question" (per category), "Edit question" (per row), "Rename set", "Reset set to defaults" all moved into modals. The question-editor list is now a clean read-only stream with hover-reveal Edit/Delete actions; the giant multi-locale form only takes up screen space when you actually need it.
  • Changed Project detail (/projects/[id]): "Add task", "New service report", "Edit service report (draft)", "Propose maintenance window", "Edit maintenance window" all moved into modals. The Maintenance tab is now full-width with a single composer card above the list instead of a cramped left-column form that kept the right-column list narrow.
  • Changed Settings → Integrations: Stripe key rotation moved into a modal.
1 month ago

Industry templates for the public CheckUp — law, tax, finance, healthcare

21
  • Added Four ready-to-use question sets when you create a new public-CheckUp catalog: Healthcare (medical practice / MVZ), Law firm, Tax / Accounting firm, Financial services. Each ships 29 questions in all three locales (DE/EN/ES) — the standard cross-industry set plus four sector-specific add-ons per template (retention rules, regulator / court-filing portals, secure client portals, confidentiality training). The Settings dropdown localises the template's display name to the operator's UI language; the underlying question text is trilingual.
  • Added Pick the workspace language on signup. The email step now has a DE / EN / ES flag picker; whichever you choose sets the workspace language and seeds the default CheckUp questions in that locale (DE/EN/ES). Changeable later in settings.
  • Fixed Creating a second question set silently 403'd for early-access workspaces — the form redirected to the Landings tab and the catalog never appeared. Plan-cap check now recognises superadmin + early-access correctly. Error banners surface real failures instead of silent redirects.
1 month ago

Hotfix: industry-template questions showed up as raw keys in the lead transcript + PDF

3
  • Fixed Questions from the Healthcare / Law / Tax / Finance industry templates (DE/EN/ES) rendered as their internal keys — e.g. `patient_record_access_hc`, `bea_active_law` — in three places: the operator's lead-detail transcript, the prospect's emailed PDF report, and the AI quote-drafter context. Three read-side renderers were resolving question text against the Standard seed only; anything outside the Standard catalog fell back to the raw identifier. Resolution now walks the slug's assigned question set → any other set the workspace owns → the static seed union of all five catalogs. Prospect-facing PDFs are the most visible win.
  • Fixed Fix applies to all three locales (DE/EN/ES) at once — the resolver returns the full translation triple for every key and the call-site picks the active locale. English and Spanish workspaces that run the law / tax / finance / healthcare templates get the same correct rendering as German ones; no separate per-locale patch was needed.
  • Fixed Prospect-facing surfaces verified clean: the /check/{slug}/questions form (both standalone and embed variants) and the customer self-check at /self-check/{token} were already reading translations directly from the DB row, so they never hit the bug. The patch above only touches the three back-office / PDF / AI render paths that had been hard-coded against the Standard seed array.
1 month ago

CheckUp settings unified, formal Sie in German, multiple question sets per workspace

4211
  • Added One home for all CheckUp settings at /settings/checkups with three tabs: Public landings, Question sets, Findings library. Replaces the scattered surfaces that used to live under public-checkup, questions, and findings separately. The old Findings entry was removed from the sidebar nav.
  • Added Multiple question sets per workspace (Pro). Name a catalog ("Healthcare campaign", "Retail audit"), assign it to one or more public-CheckUp URLs. Each URL can run its own question list — /check/your-msp-medical with 12 healthcare questions, /check/your-msp-retail with PCI-flavoured ones. Free stays at one catalog.
  • Added Clone a public-CheckUp landing in one click. The landings table now has a copy action that duplicates the slug + intro text + audience label + question-set assignment; you get a new draft URL ready to rename.
  • Added Leads count in the sidebar. The Leads nav item now shows the same red badge as Customers / Quotes, so unread leads are visible without opening the page.
  • Changed Public-CheckUp wording (DE only) switched from informal "du" to formal "Sie" across all 63 default questions and tooltips. Existing prospect-facing copy on /check/[slug] now matches the tone most German MSPs use with their customers. English and Spanish translations were unaffected — they already used the formal address ("you" / "usted").
  • Changed Self-CheckUp result page polish: the score number and the maturity badge no longer crowd each other on narrow screens (they stack on mobile, sit side-by-side on desktop). The "Back to home" button now goes to the MSP's own landing page if one is configured, instead of always returning to mspercury.com.
  • Fixed Active MSPs counter on the admin dashboard was over-counting — ghost organisations that never finished signup were being tallied. Counter now matches the actual paying-or-trialing workspaces. (Visible only to superadmin; reported by an operator who noticed the math didn't add up.)
  • Security Hardened the password, passkey and 2FA login paths. No user action required.
1 month ago

Signup friction crushed: email + PIN, done — workspace/password/2FA move into the wizard. Plus PIN login, passkey nudge, PIN-gated account deletion, tenant-branded public CheckUp, OG image + SEO overhaul

631
  • Changed Signup collapsed from three pages to two. Was: /signup (email) → /signup/verify (PIN) → /signup/complete (workspace + name + password + password-confirm + 2 consent checkboxes) → /dashboard. Now: /signup with inline consent ("By clicking Send code you agree to Terms + Privacy") → /signup/verify → /onboarding wizard. The `confirmEmailVerification` action creates the org + admin user the moment the PIN matches: workspace name derived from the email prefix (renamable in the wizard), passwordHash=NULL, consent timestamps stamped, session cookie minted. The friction layer that was killing Reddit-traffic conversion is gone.
  • Added Passwordless login on /login: new "Email me a sign-in code — no password needed" button next to the classic password form. The button posts to `startEmailVerification` and bounces to /signup/verify; `confirmEmailVerification` detects the existing user, mints a fresh session and lands on /dashboard. That keeps the PIN-only cohort (anyone who signed up post 2026-05-09 in the friction-reduced flow and hasn't set a password yet) able to get back in. The login action now returns a readable error for `passwordHash=null` accounts instead of "Invalid credentials".
  • Changed Onboarding wizard is now a popup modal on the dashboard instead of a hard middleware redirect. The `enforceOnboarding` middleware is neutered (no-op); new `OnboardingWizardModal.astro` renders the full setup form (workspace + finance + branding + about) as a centred `<dialog>` with a backdrop, closeable via X / ESC / backdrop click. Auto-opens on first dashboard paint when `onboardingCompletedAt` is null; stays dismissible (soft nag) — ESC + Close don't change server state, the footer's "Maybe later" link explicitly sets `onboardingCompletedAt`. /onboarding standalone URL becomes a legacy redirect to /dashboard.
  • Added Password card at the top of /settings/security — single form, branched server-side on hasPassword. With `passwordHash=null` the UI shows "Set a password" with just New password + Confirm fields; the `changePassword` action now accepts `currentPassword` as optional and only verifies it when a hash already exists. On initial set the success notice reads "Password set — next login can use email + password OR keep using the email code". Plus a tooltip noting that a passkey is the phishing-resistant option vs a password. The existing /settings/password page is untouched (slated for consolidation into /settings/security).
  • Added Passkey-recommendation banner on the dashboard — small accent-tinted card pitching Touch ID / Windows Hello / Yubikey. Renders only when the user has 0 passkeys AND onboarding is done AND the `mspc_passkey_nudge_dismissed` cookie isn't set. Setup button links to /settings/security#passkeys; X sets a 365-day dismiss cookie. PIN-only signups now have an immediate path to hardware-bound passwordless sign-in once they finish (or skip) the wizard.
  • Added PIN-gated account deletion for passwordless accounts. /settings/account/delete now detects whether the user has a password set. If not, the password field is replaced with an "Email me a delete code" button; click fires the new `requestDeleteAccountCode` action, which sends a 6-digit PIN to the account email (15-minute TTL, 5 attempts, IP-rate-limited like the signup PIN flow). Once sent, the page reveals the PIN input plus the email-verbatim confirm. The `deleteAccount` action now accepts either `password` OR `pin`, branches on `passwordHash` state and verifies the PIN against the `email_verifications` table (same infra as signup, reused).
  • Changed Public CheckUp /check/{slug}/* (standalone variant) now shows ONLY the MSP's branding, not MSPercury's. Before, the three pages (index/questions/result) wrapped themselves in `MarketingLayout` — leads saw the MSPercury logo, Pricing/Partners/Blog nav and the full MSPercury footer with Imprint/Terms/Privacy/DPA/Cookies links. New minimal `TenantPublicLayout.astro` shows only the org name (or uploaded logo) in the header and a tiny "Powered by MSPercury" footer link. Plus `noindex,nofollow,noarchive` so per-MSP slug URLs don't rank on the MSPercury domain. The embed routes were already clean.
  • Added Open-Graph image (1200×630) plus square variant (1200×1200) generated via Sharp from `public/og.svg`. Full meta-tag overhaul in `MarketingLayout.astro`: og:image:secure_url + type + alt, twitter:domain + url + image:alt, application-name, apple-mobile-web-app-* suite, format-detection (no auto-tel linking), referrer policy strict-origin, DNS-prefetch + preconnect for googletagmanager, theme-color split light/dark. The English landing SEO is decoupled from DE/ES: title + description rewritten around the US/Canada Google Ads messaging (MSP CRM, quoting, lead tracking, no spreadsheets, free tier), GDPR / EU-hosted language stripped. JSON-LD now carries alternateName + dual applicationCategory + a curated featureList + locale-aware priceCurrency (USD for EN).
  • Fixed Multiple DE-string leaks in the EN UI fixed. Dashboard MRR chart showed "Projekte (einmalig)" hardcoded across every locale; now three i18n keys (`chartOneTime`, `chartLblOneTime`, `chartRangeMonths`) DE/EN/ES. Range toggle buttons ("3 / 6 / 12 Monate") now go through the template-i18n. AiOverlay component (used everywhere): aria-label "Schließen", Cancel + Error-Close strings switched to locale-aware via SSR front-matter; inline-JS error strings for the local-AI path via an injected `window.__mspcAiI18n` bag (DE/EN/ES). Plus: dashboard 500 ReferenceError fixed (`isDe`/`isEs` were never declared at the top of the page), Checkup AI-tasks modal localised, Quotes AI-reply-draft overlay localised via define:vars. Settings/admin customer-edit had a DE-example address placeholder (`Hauptstraße 1`), now US-shaped (`1500 Market St`).
  • Added New patchnote RSS feed at `/changelog/rss.xml` — pre-rendered, locale-aware (defaults English), newest entry first. Up to seven bullets per entry summarised into the `description` field. Mirrors the same `@astrojs/rss` pattern the blog feed at /blog/rss.xml uses.
1 month ago

Mehrere Ansprechpartner pro Kunde + Self-Check-Einladung per Token an einzelne Kontakte

6
  • Added Mehrere Ansprechpartner pro Kunde. Statt einem Single-Contact-Feld auf der customers-Row (contactPerson + email) gibt es jetzt eine eigene customer_contacts-Tabelle mit beliebig vielen Kontakten je Kunde — jeder mit Name, E-Mail, Telefon, Rolle (IT-Leitung / Geschäftsführung / Buchhaltung …) und Notizen. Auf der /customers/[id]-Seite eingebettet als kollabierbare Liste mit Add/Edit/Delete-Forms; ein Eintrag kann als Hauptkontakt markiert werden. Lazy-Backfill: beim ersten Öffnen wird der bestehende customers.contactPerson + customers.email als erste Kontakt-Row angelegt — keine Daten gehen verloren, alte Reads funktionieren weiter.
  • Added Token-gegate Self-Check-Einladung. Pro Ansprechpartner mit E-Mail-Adresse zeigt die Kontakt-Liste einen Self-Check-senden-Button. Klick erzeugt einen 30-Tage-gültigen Token, persistiert eine customer_self_assessment_invites-Row und schickt eine E-Mail an den Kontakt mit dem Subject: customerName lädt Sie zum IT-Selbstcheck ein. Die Mail ist im Branding des MSP gerendert (Wordmark + Footer = workspace name, nicht MSPercury). Optionale Operator-Opener-Nachricht wird oberhalb des CTA als italic-Block eingefügt.
  • Added Public-Page /self-check/{token} drei-stufig: Welcome → Questions → Done. Vor-pop-up keine E-Mail-Re-Erfassung, da Customer schon bekannt. Locale-aware aus invite.locale. Mit ?step=questions / ?step=done query-state. Submit erzeugt eine checkups-Row mit status=draft, notes=JSON mit Score und Antworten, plus Push-Notification an alle Workspace-Admins.
  • Added Submit-Action submitSelfAssessment validiert Token (existence + nicht-completed + nicht-expired), parst Antworten, computet Score, legt CheckUp-Row an, markiert Invite als completed mit completed_checkup_id. Idempotent: zweiter Submit auf dieselbe Invite returnt success-shape ohne Duplikat-CheckUp.
  • Added Invite-History-Block auf der Kunden-Detailseite. Pro Eintrag: E-Mail, Versanddatum, Status (offen / abgelaufen / abgeschlossen). Offene Invites haben Link-kopieren + Widerrufen-Buttons. Completed-Invites haben einen Link auf den resultierenden CheckUp.
  • Added Drizzle-Migration 0005 legt zwei neue Tabellen an: customer_contacts und customer_self_assessment_invites. FK-Cascades sauber: Customer-Delete cascadet auf Contacts und Invites; Contact-Delete setzt invite.contact_id auf NULL; CheckUp-Delete setzt invite.completed_checkup_id auf NULL.
1 month ago

Mehrere maßgeschneiderte Public-CheckUp-Landingpages pro Workspace (Pro-Feature) mit per-Slug Frage-Filter und Lead-Attribution

52
  • Added Pro-User können jetzt mehrere /check/-URLs pro Workspace betreiben — eine pro Zielgruppe. Schema: .unique() auf public_checkup_slugs.organization_id gedroppt, drei neue per-Slug-Spalten ergänzt (name für den internen Operator-Label wie "Healthcare-Kampagne", audience_label für sichtbares "Speziell für Arztpraxen", question_keys_json als JSON-Array zum Einschränken des Frage-Sets pro Slug). Plus prospect_leads.source_slug_id zur Lead-Attribution. Migration 0004_multi_slug_public_checkup.sql ist additiv-only mit IF-NOT-EXISTS-Guards plus Backfill der bestehenden Single-Slug-Rows auf name='Default'.
  • Added Plan-Limits: Free behält den Cap auf 1 Slug pro Workspace; Pro hat unlimited. Ein neues publicCheckupCustomQuestionSet-Flag erlaubt Pro zusätzlich pro Slug ein eingeschränktes Frage-Set (z.B. nur die 12 Healthcare-relevanten Fragen für die /check/dein-shop-medical-Variante statt aller 30). Validation server-side stripped die Filter-Daten bei Free-Submits stillschweigend, kein 403.
  • Added Settings → Public-CheckUp komplett umgebaut: statt einem Single-Slug-Editor jetzt eine Liste aller Slugs als Cards mit Link, iframe-Snippet und kollabierbarem Edit-Form pro Slug. Plus "Neue Landingpage anlegen"-Block am Ende, Pro-gated wenn Limit erreicht — zeigt sonst einen Upgrade-Pitch. Frage-Set-Filter ist als Checkbox-Picker pro Slug eingebaut: scrollbare Liste aller Workspace-Fragen, Häkchen setzen, Hidden-JSON-Input wird beim Submit autosynchronisiert.
  • Added Lead-Attribution: jeder neue Lead trägt jetzt die source_slug_id der Landingpage, über die er reingekommen ist. Damit kannst du im /leads-Inbox nach Kampagne filtern. Bei Slug-Löschung bleibt die ID erhalten als toter Verweis (kein CASCADE), die Lead-Karte rendert dann "(deleted slug)" statt des Kampagnennamens — historische Daten gehen nie verloren.
  • Added Question-Set-Sicherheit: bei einem aktiven Filter pro Slug werden Submit-Antworten server-side auf die im Filter erlaubten Keys reduziert + die Score-Berechnung läuft nur auf der gefilterten Subset. Damit kann ein Angreifer nicht via tampered Form-Payload Antworten zu Fragen einreichen, die der Visitor real nie zu sehen bekam — sonst wäre der Score über die Filter-Schwelle manipulierbar gewesen.
  • Changed Pricing-Page Pro-Feature-Liste erweitert um Mehrere Public-CheckUp-URLs pro Workspace plus Stripe-Integration auf eigenem Konto. FAQ-Eintrag in DE/EN/ES ergänzt mit Use-Case-Beispielen.
  • Changed Features-Doku auf docs.mspercury.com (DE/EN/ES) bekommt einen neuen Abschnitt Mehrere Public-CheckUp-URLs pro Workspace direkt unter dem Standard-Public-CheckUp-Block, mit Beispiel-URLs für Healthcare/Retail/Legal und Erklärung des Frage-Filters.
1 month ago

Stripe-KB-Artikel + AGB komplett überarbeitet (monatlich kündbar, kein Mindestlauf, Beta-Schutzklauseln) + Datenschutz mit allen Sub-Auftragsverarbeitern (Hetzner / Stripe / NinjaOne / BYO-AI / Apple / Google), alles trilingual

22
  • Added Neuer Knowledgebase-Artikel „Stripe-Integration & Kundenabrechnung" auf docs.mspercury.com (DE/EN/ES, order=3.7). Zehn Abschnitte mit Hints + Tooltips: Stripe-Konto vorbereiten + Stripe-Tax aktivieren + Restricted-Key statt Secret-Key, Workspace- und Kunden-Steuerstammdaten, Schritt-für-Schritt Quote→Stripe-Push, Mapping-Tabelle der Periode-Typen, Reverse-Charge bei EU-B2B mit Beispielen, Tropezones-Tabelle (häufige Fehler + Behebung), klare Abgrenzung „was MSPercury macht / nicht macht", Roadmap. Markdown mit Frontmatter-Order (3.7), wird automatisch in der Docs-Nav zwischen Public-CheckUp und Known-Issues einsortiert.
  • Changed AGB komplett überarbeitet auf das aktuelle Free+Pro-Pricing-Modell. Pro €49 / $49 / CA$65 monatlich — explizit „keine Mindestlaufzeit, monatlich kündbar zum Ende des laufenden Abrechnungsmonats" (Ziffer 5). 14-tägige kostenfreie Probezeit ohne Card on signup. Founding-100-Cohort dauerhaft Free, mit Garantie dass Preisänderungen am Pro-Plan diese Plätze nicht zwangsweise deaktivieren (Ziffer 4.1+4.4). Beta-Status-Disclaimer als eigene Ziffer 3 mit Hinweisen auf Funktionsänderungen, Bug-Fix-Priorität und Backup-Verantwortung. Neue Acceptable-Use-Klausel (Ziffer 12) gegen Spam, Phishing, Malware-Distribution, Reverse-Engineering. BYO-API-Key-Verantwortlichkeit klar geregelt: Operator haftet für Stripe/Anthropic/OpenAI-Konfigurationen + Kosten + Compliance. Haftungsbegrenzung verschärft: dreifacher Monatsbetrag pro Schaden, mind. 100 €; ausdrücklich ausgeschlossen sind Datenverluste bei nicht-erfüllter Backup-Pflicht, KI-Output-Schäden, indirekte Schäden, Beta-Phase-Beeinträchtigungen ohne Vorsatz/grobe Fahrlässigkeit. Spanische Fassung neu ergänzt (vorher fehlend, fiel auf EN zurück).
  • Changed Datenschutzerklärung um alle aktuellen Sub-Auftragsverarbeiter erweitert: Hetzner mit detailliertem Zweck (VPS + Postgres + SMTP-Relay über mail.your-server.de + Storage-Box-Off-site-Backup), NinjaOne (Server-Monitoring + Patch-Management, EU-US-DPF-zertifiziert), Stripe in zwei separaten Rollen (a) MSPercury-Subscription-Billing (b) MSP-eigene Stripe-Integration für Kundenrechnungen — wichtige Klarstellung dass der MSP in Fall (b) selbst Verantwortlicher ggü. Stripe ist und wir nur transportieren. Anthropic / OpenAI / Ollama / OpenAI-kompatibel als BYO-Key-Provider mit explizitem Datenfluss pro AI-Funktion (was wann an wen geht: Executive-Summary, Quote-Drafter, Lead-Outreach, Quote-Reply, Service-Report u.a.). Apple bleibt für iOS-Distribution. Google Ads nur auf Marketing-Landing nach Cookie-Consent. Drittland-Übermittlung (Section 6) erweitert. Spanische Fassung neu ergänzt (vorher fehlend).
  • Added Neuer Sub-Abschnitt 5a „Datenfluss bei BYO-AI-Funktionen" in der Datenschutzerklärung, der pro AI-Feature transparent macht welche Felder an den konfigurierten AI-Provider geschickt werden. Bei Self-hosted-Endpoints (Ollama auf eigenem Server) verlässt nichts das Operator-Netz; bei Cloud-Providern gelten deren AVV-Bestimmungen. AI-Funktionen sind komplett deaktivierbar via Einstellungen → AI → Provider auf „—".
1 month ago

Cohorts-500 gefixt + Country-aware VAT/USt-IdNr/EIN/GST-HST in Angeboten, Rechnungen und Stripe-Push

41
  • Fixed /superadmin/cohorts warf 500. Ursache: postgres.js kann ein JS-Date nicht direkt durch ein raw sql\\`...\\`-Template-Literal serialisieren — der Drizzle-typed gt()-Operator löst das in .where(), raw-SQL-Strings nicht. Fix: earliestCohort.toISOString() mit explizitem ::timestamptz-Cast in den beiden raw-SQL-Blöcken (CTE-Self-Join für Aktivitäts-Grid + signup-Aggregat). Selbe Bug-Klasse hatte uns bei der Postgres-Migration mehrfach erwischt; Inline-Code-Comment dokumentiert den Trade-off.
  • Added Country-aware Tax-ID-Handling für Angebote + Rechnungen. Kunde + Workspace haben jetzt je country (ISO-3166-1 alpha-2) und tax_id_type (Stripe-aligned: eu_vat/us_ein/ca_gst_hst/ca_qst/gb_vat/ch_vat/au_abn/ae_trn/...). Bisher rendete die Quote-PDF immer ein hartcodiertes USt-ID — jetzt wechselt das Label automatisch auf den Begriff den die Behörde im jeweiligen Land erwartet (USt-IdNr für DE, VAT für andere EU-Länder, EIN für USA, GST/HST für Kanada-Bund, QST für Quebec, …). Per-locale-i18n: deutsche Operatoren sehen USt-IdNr., englische VAT ID, spanische NIF/IVA. Auto-Default vom Land: pickst du DE, springt der Type-Picker automatisch auf eu_vat; pickst du US, auf us_ein. Mehrere kanadische PSTs sind als Override verfügbar (BC/MB/SK).
  • Added Quote-PDF zeigt die Kunden-Steuer-ID jetzt in der Bezugszeichenzeile (DIN 5008-konform), die Workspace-Steuer-ID weiterhin im Page-Footer — beides mit dem gleichen country-aware Label. Bei Empfängern in einem anderen EU-Land wird oben über den Posten ein gelb umrandeter Reverse-Charge-Banner gerendert mit Verweis auf Art. 196 MwSt-RL / §13b UStG, in der Kundensprache. Wird automatisch unterdrückt bei innerdeutscher (gleiches Land) oder Drittland-Lieferung.
  • Added Stripe-Push (createStripeInvoiceFromQuote) hängt Adresse + tax_id_data an die Stripe-Customer-Erstellung. Damit kann Stripe-Tax die richtige Jurisdiktion auflösen (DE → 19% MwSt, EU-cross-border-B2B → 0% mit Reverse-Charge-Annotation, US → state-level sales tax). Bei bereits existierendem Stripe-Customer wird die Adresse synchronisiert und neue Tax-IDs nachgereicht via customers.createTaxId(), ohne Duplikate zu erzeugen. Strukturierte Adresse (line1/city/postal_code/country) statt der freitext-address-Spalte, damit Stripe-Tax sauber arbeitet.
  • Added Customer-Edit + -Create-Form bekommen Country-Dropdown (DACH + US/CA + UK gepinnt oben, Rest A-Z) und Tax-ID-Type-Dropdown. JS auto-fillt den Type wenn das Land geändert wird und der Type leer oder vom alten Standard war. Settings → Workspace genauso für die MSP-eigenen Daten. Drizzle-Migration 0003_add_country_tax_id_type.sql (4 ALTERs, additiv-only).
1 month ago

Stripe-Architektur korrigiert: direkter API-Key statt „on-behalf-of" Connect, end-to-end Quote→Invoice

211
  • Changed Stripe-Integration komplett neu gedacht. Statt Stripe Connect Express („MSPercury rechnet on-behalf des MSP ab") läuft jetzt der saubere Direct-API-Key-Pfad: der MSP trägt unter Einstellungen → Integrationen seinen eigenen Stripe-Secret- oder Restricted-Key (sk_*/rk_*) ein, MSPercury verschlüsselt ihn AES-256-GCM at rest (gleiche Helper wie für TOTP- und AI-Keys) und ruft stripe.invoices.create() direkt gegen das MSP-Konto auf. Geld fließt zwischen MSP und Endkunde — MSPercury sieht nie Geld, weder als Treuhänder noch als „Plattform". Kein Stripe-Connect-Onboarding-Flow, keine Express-Account-Erstellung, kein „on-behalf"-Header mehr. Konzeptionell wesentlich sauberer und in der Praxis flexibler: jeder MSP, der bereits Stripe nutzt, bringt seinen Account direkt mit.
  • Added End-to-End Quote→Stripe-Invoice live. Auf jedem akzeptierten Angebot taucht jetzt — sofern Stripe verbunden ist — ein violetter Block „Stripe-Rechnung erstellen" auf, mit Zahlungsfrist (default 14 Tage) und „Sofort schicken"-Checkbox. Klick erzeugt: 1) Stripe-Customer auf dem MSP-Konto (find-by-metadata oder neu anlegen, getaggt mit mspercury_customer_id), 2) ein InvoiceItem pro MSPercury-Quote-Line-Item mit Beschreibung + Stückpreis + Menge, 3) eine Stripe-Invoice mit collection_method=send_invoice und einem Footer „MSPercury reference: Q-2026-XXXX". Bei aktiver Sofort-Versand-Checkbox wird zusätzlich finalizeInvoice() + sendInvoice() gerufen; Stripe schickt die Hosted-Invoice-Mail aus der eigenen Stripe-Adresse. Tenant-Isolation strikt: Quote, Customer und Line-Items werden alle auf ctx.locals.organization.id re-geladen, der Stripe-Key wird pro Request frisch entschlüsselt (kein Modul-Cache).
  • Added Verbindungsverwaltung in /settings/integrations: Restricted-Key-Empfehlung mit Scope-Hinweis (Customers Write + Invoices Write), automatische Erkennung von Test-vs-Live-Modus aus dem Key-Prefix, Account-Display-Name + Account-ID werden via accounts.retrieve() direkt nach dem Paste gefetcht und gecached. Test-Connection-Button ruft erneut accounts.retrieve() und aktualisiert das „last verified"-Timestamp; Disconnect-Button löscht alle Felder. Test-Mode-Warnung wird prominent angezeigt, damit niemand versehentlich Test-Rechnungen an echte Kunden schickt. Pro-only, Admin-only, alle Felder verschlüsselt.
  • Removed Alte Stripe-Connect-Foundation entfernt: src/actions/stripe-connect.ts, /api/stripe/connect/return.ts, der account.updated-Webhook-Handler und die Connect-Account-Onboarding-UI sind weg. Die Drizzle-Spalten stripe_connect_account_id + stripe_connect_charges_enabled aus Migration 0001 bleiben dormant in der DB (additive-only Migration-Disziplin) und werden in einer späteren Cleanup-Migration gedroppt. Roadmap (DE/EN/ES) auf docs.mspercury.com angepasst — „Stripe Connect" durch „Stripe-Integration (eigener Account, kein on-behalf)" ersetzt.
1 month ago

i18n-Audit: Findings-Library, CheckUp-Wizard, PDF-Report — keine deutschen Enum-Werte mehr für EN/ES-Tenants

5
  • Fixed Findings-Library-Editor zeigte für EN- und ES-Workspaces die Priority/Effort-Dropdowns mit deutschen Werten („hoch / mittel / niedrig") an, obwohl alle Labels und Buttons sonst lokalisiert waren. Ursache: die Werte sind in der DB kanonisch deutsch (FINDING_PRIORITIES = [„hoch",„mittel",„niedrig"]) und wurden 1:1 als Option-Labels gerendert. Fix: drei neue Helper localizePriority / localizeEffort / localizeTaskPriority neben dem bestehenden localizeCategory in lib/checkups/wizard.ts, plus i18n-Schlüssel checkup.priorities.*, checkup.efforts.*, checkup.taskPriorities.* in DE/EN/ES.
  • Fixed Gleicher Bug auf der Findings-Library-Listenseite — Filter-Pills und Item-Chips zeigten rohe deutsche Kategorie-Strings („Sicherheit", „Berechtigungen", „Sonstiges") und Priority-Badges („hoch", „mittel", „niedrig") für nicht-deutsche Operatoren. Im CheckUp-Wizard-Review (/checkups/wizard/.../review) ebenso: Priority-Chip + Effort-Inline-Text + Add-Finding-Dropdowns. Alles auf die neuen Helper umgestellt.
  • Fixed CheckUp-PDF-Report (kundenseitiges PDF!) leakte deutsche Priority/Effort-Werte in den Findings-by-Category-Listen, Risiko-Matrix-Achsenbeschriftungen und im Anhangs-Tabelle. Ist ein WeasyPrint-Worker, hat keinen Zugriff auf die Request-t()-Funktion — Lösung: per-Locale-Maps direkt im PDF-Template (PRIORITY_LABELS, EFFORT_LABELS, PDF_CATEGORY_LABELS), gespiegelt zur i18n-Datei. Plus Spanisch-Übersetzungen für alle Cover-Page-, TOC-, Methodology-, Appendix-Strings, die vorher beim ES-Locale auf Englisch zurückfielen (CheckUp-Bericht / Audit-Datum / Inhalt / Risiko-Matrix / Anhang etc.).
  • Fixed Inline-CheckUp-Findings-Form (CheckupFindingsForm.astro) — Vanilla-JS-Island, das Card-für-Card die Findings rendert — hatte die gleichen rohen deutschen Werte in allen Dropdowns + Priority-Chips. Server-side label-Maps werden jetzt via define:vars ins Script injiziert; priorityChip(...) + categorySelect + priSel + effSel benutzen sie.
  • Fixed AI-generierte Tasks (Tier-1G-CheckUp-zu-Tasks) zeigten ihre priority-Property im Operator-UI als rohe englische Werte „high / medium / low" — auch im DE-Workspace. Eigene Lokalisierungsschicht via checkup.taskPriorities.*, da das Enum unabhängig vom Findings-Schema-Enum (DE) ist (LLM-Output).
1 month ago

Stripe Connect-Foundation, Roadmap-Refresh, Lexware/Polar/sevDesk-Pipeline

31
  • Added Stripe Connect (Phase 1) — Schema, Actions, Webhook-Hook und Settings-Seite stehen. Unter Einstellungen → Integrationen können Pro-Workspaces ihr eigenes Stripe-Konto via Express-Onboarding verbinden, MSPercury stellt danach Kundenrechnungen und -Subscriptions on-behalf aus. Auszahlungen gehen direkt aufs MSP-Konto, Stripe übernimmt die komplette USt-Logik (DE 19 %, EU-Reverse-Charge mit USt-IdNr., 0 % Drittland). Tenant-Isolation strikt: Connect-Account-ID lebt pro Org-Row, Webhook-Handler und Return-Callback gleichen sie bei jedem Event gegen die signed-in-Workspace ab — kein cross-tenant-Leak möglich. Onboarding-Status-Pill (Bereit / Stripe prüft / Onboarding offen / Nicht verbunden) plus Express-Dashboard-Login-Link plus Disconnect-Knopf.
  • Added Roadmap auf docs.mspercury.com (DE/EN/ES) komplett refresht. „Jetzt" ist jetzt Public Read API + API-Keys, Stripe Connect, Webhooks, native iOS App. „Nächst" ist Per-Tenant-DB-Snapshot+Restore, Cohort-Heatmap-Drilldown, AEO/GEO-SEO-Push. „Später" listet Lexware Office, sevDesk, Polar.sh, DATEV-Schnittstelle, FastBill/Billbee/Faktura+ und ein paar weitere Long-Tail-Wünsche. „Bereits umgesetzt" enthält das volle Mai-2026-Inventar — Pricing-Modell, Partner-Netzwerk + Marktplatz, KI-Tier 1A–1G, Lead-Gen + Marketing, Customer-Branding, Workspace/Quotes/CheckUps, Auth/Security, Operations.
  • Changed Settings-Sidebar hat einen neuen Eintrag „Integrationen" zwischen „Specialty-Profil" und Superadmin-Block. Sub-Label „Stripe Connect · API" macht klar wo es hingeht. Aktuell nur Stripe Connect; Public Read API + Webhooks landen in derselben Page sobald sie shippen.
  • Added Drizzle-Migration 0001_add_stripe_connect.sql committed: zwei neue Spalten auf organizations (stripe_connect_account_id, stripe_connect_charges_enabled). Konflikt-frei mit dem bestehenden Stripe-Customer-/Subscription-Setup für die MSPercury-Pro-Subscription — Connect bezahlt der KUNDE des MSPs, die Pro-Subscription bezahlt der MSP.
1 month ago

Public-CheckUp ohne PIN-Gate, AI-Features mit BYO-Key, Beta-Hinweis, Cohort-Heatmap

421
  • Changed Public-CheckUp-Funnel: das PIN-Eingabe-Schritt nach der E-Mail ist weg. Lead tippt Adresse, klickt „CheckUp starten“, landet direkt bei den Fragen — keine 6-Stellen-aus-dem-Postfach-tippen-Friction mehr. Anti-Spam-Floor weiterhin in Place: 3 Starts/h pro E-Mail, 10 Starts/h pro IP, plus die natürliche Friction der 20 Fragen + Privacy-Consent am Submit. Alte /pin-URLs in gecachten E-Mails forwarden sauber zur richtigen Stelle.
  • Added AI-Features dokumentieren ihren Bring-your-own-Key-Ansatz jetzt prominent auf der Landingpage. Anthropic, OpenAI oder beliebiger OpenAI-kompatibler Endpoint (Ollama, vLLM, LiteLLM, Self-Hosted-LLM-Proxy) — kein Token-Markup auf MSPercury-Seite, kein Vendor-Lock-in. Wer kein Modell hinterlegt, läuft im Klassik-Modus ohne KI.
  • Added Beta-Hinweis-Banner auf /login und Dashboard: erinnert dezent daran, dass wir während der Beta fast täglich kleine Fixes shippen — kurze Unterbrechungen können passieren. Per-Device dismissable, Default sichtbar bis zum ersten Klick auf X.
  • Changed Partner-Netzwerk und Lead-Marktplatz sind jetzt klar als künftige Pro-Features markiert (Sidebar-Tag „Bald · Soon“, Banner oben in /network/*, Tag in der Marktplatz-Card auf /leads/[id]). Während der Beta für alle freigeschaltet — der Pro-Gate kommt mit dem GA-Pricing.
  • Added Superadmin → Cohorts ist nicht mehr „Coming Soon“: Retention-Heatmap pro Signup-Kohorte (12 Wochen × 12 Offset-Wochen) mit Week-0-Activation und Week-4-Retention KPIs oben drüber. CSV-Export an Bord für Board-Decks.
  • Removed Pricing-Modell-Migration ist fertig: PWYW („pay what you want“)-Reste sind aus dem User-facing Surface raus. /superadmin/pwyw bleibt für historische Pledges erreichbar, ist aber nicht mehr in der Sidebar.
  • Added Multi-Region-Hreflang: Marketing-Layout liefert jetzt zusätzlich zu en/de/es auch en-US, en-CA, de-DE, es-ES und es-MX an die Suchmaschinen aus — eigene SERPs für die Märkte, die wir aktiv targeten, derselbe Content-Pfad pro Sprache.
1 month ago

Public-CheckUp-Embed-Bugfixes, Bell auch auf Mobile + Lead-Stream, neue Push-Surface „newLead", iframe-Snippet auf /leads, Embed-Quelle automatisch getaggt

636
  • Fixed Public-CheckUp-Iframe lief in einer Endlosschleife: nach Email + PIN landete der Besucher wieder auf dem Email-Step. Ursache war doppelt: (1) der signierte PIN-Cookie hatte `SameSite=Lax`, also wurde er von modernen Browsern in iframe-Kontext (= Drittanbieter) gar nicht mehr mitgeschickt — die `/embed/questions`-Seite sah kein Cookie und redirectete zurück. (2) Selbst nach erfolgreichem Submit wurde der User wieder auf Schritt 1 geworfen, weil die `submitPublicCheckup`-Action den Cookie am Ende absichtlich löscht (Single-Use), aber der Page-Handler den Cookie-Check VOR dem Submit-Result-Check laufen ließ — der Erfolgs-Redirect zum Result wurde nie erreicht. Beide Fixes: (1) Cookie auf `SameSite=None; Secure` in Prod (Lax bleibt als Dev-Fallback, weil `None` über HTTP silent dropped wird), (2) Page-Handler-Reihenfolge gedreht: Submit-Result wird zuerst geprüft, Cookie-Gate erst danach. Gilt analog für UTM-Cookie und Lead-Detail-Seite (Delete-Action löscht den Lead-Row → 404 statt Redirect-zur-Liste, gleicher Reihenfolge-Bug).
  • Fixed PIN-Eingabe im Iframe-Embed wurde mit „Bitte halten Sie sich an das vorgegebene Format" abgewiesen, sobald iOS-Autofill oder Copy-Paste ein Leerzeichen mitbrachte. Das Eingabefeld hatte `pattern="\d{6}"` als Native-Browser-Validierung, der Server akzeptiert aber bereits 4-8 Ziffern. Pattern entfernt, stattdessen ein `oninput`-Stripper der nicht-Ziffern direkt aus dem Wert raushaut — Paste mit Leerzeichen, Bindestrichen, etc. funktioniert jetzt. Server-Validierung bleibt unverändert als zweite Linie.
  • Fixed Notification-Bell auf iOS-PWA war nicht antippbar. Drei kombinierte Ursachen: (1) der `<script is:inline>`-Block in `NotificationBell.astro` wurde pro Bell-Instanz dupliziert (Mobile + Desktop = 2 Skripts) und hat doppelt Listener auf dieselben Buttons attached — Klick toggelte 2× und endete bei „geschlossen". Auf hoisted `<script>` umgestellt, Astro emittet jetzt genau einmal pro Page. (2) Das Badge-Span auf dem Button war absolut positioniert ohne `pointer-events:none` — Tap auf den Counter wurde von iOS nicht zum Button durchgereicht. (3) `touch-action: manipulation` auf dem Bell-Container deaktiviert iOS' 300ms-double-tap-zoom-Verzögerung.
  • Fixed Push-Aktivierungs-Button in `/settings/notifications` hatte keine Funktion — der zentrale `<Button>`-Component reichte `data-*`-Attribute nicht ans `<button>`-Element durch, also fand das Wiring-Skript `document.querySelector("[data-push-enable]")` nichts und der Early-Return brach das gesamte Push-Setup ab. Button.astro spreaded jetzt `Astro.props` per `...rest` aufs Element — alle data-/aria-/hidden-Attribute kommen durch. Effekt: Push-Subscribe-Flow funktioniert ab sofort auf jeder Plattform inkl. iOS-PWA.
  • Fixed Notification-Center-Panel rutschte auf Mobile komplett aus dem sichtbaren Bereich nach links. Ursache: der Bell-Button steht auf Mobile in der Mitte der Button-Reihe (Search → Bell → Theme → Menu), nicht am rechten Rand. Mit `position: absolute; right: 0` ankerte das Panel an der Bell-Wrapper-Rechtskante, die 360px-Panel-Breite zog dann ~50–100px past the viewport. Behoben durch JS-basierte Positionierung beim Öffnen: `position: fixed`, `right: 8px` (immer 8px vom Viewport-Rand), `top: bell-bottom + 8px`. Plus `max-width: calc(100vw - 2rem)` clampt das Panel auf engen Geräten (320px → 288px Breite, 414px → 360px Breite).
  • Fixed Lead-Karte zeigte „(direkt)" als Quelle, sobald der Besucher den Iframe auf einer Site mit `Referrer-Policy: no-referrer` gesehen hat — dann fehlt sowohl Referer-Header als auch URL-UTM. Embed-Endpoint setzt jetzt automatisch einen Fallback `utm_source=embed_iframe`, `utm_medium=iframe`, sobald nichts anderes erfasst wird. Echte UTMs in der Iframe-URL und ein vorhandener Referer überschreiben den Fallback weiterhin. In der Lead-Liste taucht das als „Embed (Website)" auf statt im Direct-Bucket. Tipp: für saubere Per-Landing-Auswertung trotzdem eigene UTMs an die iframe-`src` hängen.
  • Added Notification-Bell ist jetzt auch in der mobilen Top-Bar verfügbar (vorher nur auf md+ sichtbar) — der Bell-Aggregator wurde in `lib/notifications/bell-feed.ts` extrahiert, beide Top-Bars (Desktop `TopHeader`, Mobile `AppNav`) ziehen die identischen 5 Streams: Quote-Messages, Task-Comments, Service-Bericht-Comments, Status-Update-Comments, neue Public-CheckUp-Leads.
  • Added Neuer Bell-Stream + Push-Surface „Neuer Lead". Sobald ein Public-CheckUp-Lead in der Pipeline `new` liegt, taucht er in der Glocke oben rechts mit Score, Branche und Größe auf — verschwindet automatisch, sobald der Lead auf „Kontaktiert" gezogen wird. Parallel feuert die submitPublicCheckup-Action einen Web-Push an alle Workspace-Admins mit `surface: "newLead"` — opt-in/-out wie alle anderen Push-Kategorien unter `/settings/notifications`. iOS-PWA muss einmalig installed sein + im Settings-Notifications die Subscription registrieren, Voraussetzung wie bei jedem Web-Push auf Apple.
  • Added Public-CheckUp-Iframe trägt jetzt einen subtilen „Powered by MSPercury"-Footer (border-top, kleine graue Schrift, klickbarer Link auf mspercury.com). Nur auf den Embed-Pages (alle 4 Steps), nicht auf den standalone-Landings — die nutzen die normale Marketing-Layout-Footer-Chrome. Plus auf dem Email-Step ein dezentes Datenschutz-Hinweisband: „Mit ‚Code anfordern' akzeptierst du unsere Datenschutzerklärung" mit Link nach `/legal/privacy`. Kein gating Checkbox — die hard binding consent bleibt am Submit-Schritt am Flow-Ende.
  • Added Spam-Folder-Hint im Email-Step („Bitte ggf. auch im Spam-Ordner nachsehen") in DE/EN/ES — half users finden den 6-stelligen Code, wenn er von strenger Mailfilter-Heuristik aussortiert wird. Erscheint sowohl auf der Iframe-Variante als auch auf der standalone Landing.
  • Added Auf `/leads` ist der Iframe-Embed-Snippet jetzt auch außerhalb der Settings copy-fertig zu sehen — als einklappbarer „Als Widget auf eigener Website einbetten"-Block direkt unter dem Public-Link. Mit eigenem Copy-Button und einem UTM-Beispiel als Tipp. Wer sich neu in den Lead-Funnel einarbeitet sieht beide Varianten (direkter Link + Iframe) in einem Blick.
  • Added Drei neue Doku-Seiten auf docs.mspercury.com (DE/EN/ES): „Public-CheckUp & Widget einrichten". 8-stufige End-to-End-Anleitung von leerem Workspace bis zur Iframe-Integration auf der eigenen Marketing-Site, inkl. UTM-Tracking-Beispiele, RGPD-Notes, häufige Stolpersteine. Verlinkt im Docs-Index auf Position 3.5.
  • Changed „Abgelehnt"-Spalte im Lead-Kanban hat eine semantisch passendere rosé-rote Farbe (vorher unauffälliges Grau, das visuell mit „Neu" konkurriert hat). Sortiert sich klar in die Pipeline-Farbpalette ein: Violett (Neu) → Sky (Kontaktiert) → Amber (Angebot) → Emerald (Kunde) → Rose (Abgelehnt).
  • Changed Side-Nav auf der `/settings`-Seite ist jetzt auch auf Mobile sichtbar (vorher `hidden lg:block`, also nur auf Desktop ≥1024px). Damit erreicht man von der iOS-PWA die Sub-Pages „Sicherheit", „Push-Benachrichtigungen", „Vereinbarungs-Vorlagen" usw., die vorher de facto unauffindbar waren. Auf Desktop bleibt's bei der gewohnten Sidebar-Position links.
  • Changed Service-Worker-Cache von `mspercury-v4` auf `v5` gebumpt — sorgt dafür dass installierte PWAs (Desktop + iOS) nach diesem Deploy einmalig die neuen Assets ziehen statt am alten JavaScript zu hängen. Wer eine PWA hat: einmalig aus dem App-Switcher killen und neu öffnen, dann ist alles gleich aktuell.
1 month ago

KI-Tier-1C/1D/1E, Lead-Source-Tracking + UI, Stundensatz, MRR pro Kunde, Embed-Snippet, Web Push live, PDF DIN 5008 fix

10421
  • Added KI-Meilenstein-Generator (Tier-1C) — auf jedem Projekt im Status-Tab aufklappbar als „✨ KI-Meilenstein-Entwurf". Sie tippen das Projektziel in 1–2 Sätzen ein („50 Endpoints auf Win11 migrieren, fertig bis Ende Q3") plus optional eine Gesamtdauer in Tagen, die KI liefert 3–7 geordnete Meilensteine mit groben ETAs zurück. Sie sehen die Vorschläge als Liste mit Checkboxen + editierbaren Labels und Datumsfeldern, deselektieren was nicht passt, korrigieren wo nötig, und „Ausgewählte einfügen" schreibt alle in einer atomaren Bulk-Insert-Aktion. Der Meilenstein-Tracker im Kundenportal zeigt sie sofort als Fortschrittsbalken.
  • Added Meilenstein-Reorder per Tastendruck — bei Hover über einen Meilenstein erscheinen rechts ↑/↓-Pfeile, die ihn um eine Position nach oben oder unten schieben. Die Reihenfolge im Kundenportal-Fortschrittsbalken folgt sofort. Beim ersten Eintrag ist ↑ deaktiviert, beim letzten ↓ — die Pfeile springen visuell aus, wenn das Ende erreicht ist. Hilfreich nach KI-generierter Reihenfolge oder wenn ein neuer Meilenstein nachträglich eingeschoben wird.
  • Added KI-Antwortvorschlag im Quote-Thread (Tier-1D) — direkt unter dem Antwort-Textfeld auf jeder Angebots-Detailseite ein neuer Button „✨ Antwort entwerfen". Klick liest die letzten ~10 Nachrichten zwischen Ihnen und der Kundschaft und liefert einen höflichen, sachlichen Antwort-Entwurf in der Sprache der Kundschaft (DE/EN/ES — folgt `customer.preferredLanguage`). Tonalität: kein Anrede/Verabschiedung (Chat-Bubble-Format), max. 4 Sätze, geht konkret auf die letzte Kundennachricht ein. Operator prüft + sendet immer manuell — nichts geht autonom raus, der Audit-Trail bleibt sauber. Wenn Sie schon was getippt haben, fragt der Button vor dem Überschreiben.
  • Added Service-Bericht → Status-Post Brücke (Tier-1E). Auf jedem versendeten oder bestätigten Service-Bericht steht jetzt ein „✨ Auch als Status posten"-Button. Klick destilliert die KI den Bericht zu einem 1-Zeilen-Status-Post mit der passenden Kategorie (meist „erledigt"), wechselt automatisch in den Status-Tab, befüllt den Komposer (Textarea + Kategorie-Radio) mit kurzem violetten Outline-Flash, und Sie senden ihn nach Review mit einem Klick raus. Sie müssen denselben Einsatz nicht mehr zweimal formulieren — einmal als ausführlichen Bericht, einmal als knappen Statusfeed-Eintrag.
  • Added Stundensatz wird pro Workspace gespeichert. Unter Einstellungen → Finanzen gibt es ein neues Feld „Standard-Stundensatz" — einmal setzen, dann bei jedem KI-Aufwand-Angebot aus einem CheckUp vorbefüllt. Leer lassen heißt: jeder Operator gibt den Satz pro Angebot manuell ein, wie bisher. Praktisch für MSPs mit einem festen Tarif (z. B. 120 €/h Standard, vereinzelt überschrieben für Großkunden) — kein Doppel-Tippen mehr.
  • Added Kunden-MRR jetzt direkt sichtbar in der Kundenliste UND auf der Detailseite. Auf der Listenseite eine neue Spalte „MRR" rechts mit dem effektiven Wert pro Kunde + einem ●-Badge wenn ein manueller Override gesetzt ist. Auf der Detailseite eine neue MRR-Karte mit dem Headline-Betrag, der Quelle (manueller Override oder berechnet aus N akzeptierten Angeboten) UND einer Zeilen-Aufstellung der beitragenden Angebote inkl. monatlichem Anteil pro Angebot. Jede Zeile ist klickbar zum Angebot oder direkt zur Bearbeitung — wenn die Kundschaft 5× RMM angenommen hat und Sie wollen das auf 7× hochziehen, klicken Sie „Bearbeiten", passen die Menge an, und der MRR auf der Kundenseite + im Dashboard reflektiert es sofort. Override für Vertragspauschalen bleibt verfügbar (jetzt collapsed unter eigenem Toggle).
  • Added Lead-Quellen-Attribution für den öffentlichen CheckUp. Jeder neue Lead trägt jetzt die volle First-Touch-Attribution: Source-URL (mit Query-String), Referrer-Host und alle fünf UTM-Parameter (`utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`). Erfassung läuft über ein signiertes Cookie auf der ersten Seite des Flows — überlebt den Multi-Step (Email → PIN → Fragen → Submit), wird beim Lead-Insert in die DB geschrieben, dann gelöscht. First-touch gewinnt: ein wiederkehrender Besucher mit anderer UTM überschreibt nicht den Original-Kanal. Kein Setup nötig außer `?utm_source=google&utm_medium=cpc&...` an die iframe-/Landing-URL anhängen.
  • Added Lead-Quellen-Operator-UI. Auf `/leads` über dem Kanban gibt es jetzt einen „Top-Quellen"-Strip mit den 5 wichtigsten Channels (Lead-Anzahl + Prozentanteil), und jede Lead-Karte trägt eine kompakte 📈-Source-Badge wenn UTM oder Referrer-Daten da sind. Die Pills im Strip sind klickbar — filtern den Kanban auf einen einzigen Channel; „Alle" rechts zum Reset. URL-driven (`?source=google · cpc`), also bookmarkbar/teilbar. Auf der Lead-Detailseite eine neue „Quelle"-Karte mit allen UTM-Feldern, Referrer-Host und der vollen Landing-URL als klickbarer Link, plus expliziter Erklärtext wenn (direkt).
  • Added Iframe-Embed-Snippet jetzt copy-fertig direkt im Settings-Block. Unter Einstellungen → Public-CheckUp wird der vollständige `<iframe src="…/embed" …>`-Snippet inline im grünen Link-Block angezeigt mit eigenem „Kopieren"-Button — vorher steckte er hinter einem `<details>`-Aufklapper und wurde übersehen. Der Embed-Endpoint (`/check/{slug}/embed`) ist iframe-optimiert (CSP `frame-ancestors *`, X-Frame-Options ALLOWALL), trägt den Multi-Step-Flow inkl. PIN-Eingabe komplett innerhalb des Iframes, und passt sich responsive an die Container-Breite an.
  • Added Web Push aktiviert. VAPID-Schlüssel sind in die Server-Konfiguration eingebaut, der `/api/push/key`-Endpoint antwortet jetzt 200 mit dem Public Key, und der Service-Worker kann Subscriptions registrieren. In `/settings/notifications` lässt sich pro Event-Typ (Kundennachricht-empfangen, Quote-Entscheidung, Vereinbarung-signiert, Service-Bericht-Kommentar, Status-Update-Kommentar, Task-Kommentar, Wartungsfenster-Antwort, Billing) opt-in. Browser-natives Push, kein Drittanbieter-Gateway, VAPID-Keys workspace-scoped.
  • Changed Hilfe-Section in der Sidebar startet jetzt by default eingeklappt — egal welcher Viewport. Die meisten Operator-Aufrufe gehen an Kunden / Angebote / Projekte; Docs / Changelog / Feedback nutzt man unregelmäßig. Der Aufklapp-Zustand wird weiter pro Browser gespeichert: einmal geöffnet bleibt geöffnet, bis Sie ihn wieder zumachen.
  • Changed Conversion-Funnel auf dem Dashboard: pro Stufe steht jetzt der Drop-off-Hinweis IMMER sichtbar als Mini-Zeile unter dem Balken — vorher musste der Operator mit dem Cursor draufstehenbleiben und auf den nativen Browser-Tooltip warten (mit ~1s Verzögerung, je nach OS auch gar nicht angezeigt). Ohne Drop-off ist der Hinweis grau („Kein Drop-off"), bei tatsächlichem Verlust rot-italic („↓ 12 verloren ggü. Engaged (-30%)"). Der Operator sieht beim Reinkommen aufs Dashboard sofort wo's hakt, ohne zu hovern.
  • Changed Komplette Localization-Audit: 241 i18n-Keys in DE/EN/ES verifiziert, 0 fehlend. Alle vorher noch hartcodierten deutschen Strings auf dem Dashboard (KPI-Tooltips, MRR-Override-Hinweis, Chart-Hover-Delta-Phrasen, Funnel-Texte), in den Quote-Detail- und Edit-Seiten (Post-Accept-Banner und -Hinweis) und im Projekt-Status-Inline-JS (KI-Strukturierer- und KI-Bericht-Helper-Meldungen) wurden auf `t()`-Aufrufe umgestellt. Die spanischsprachigen Operatoren bekommen jetzt die kompletten KI-Helper- und Dashboard-Texte auf Spanisch — vorher rutschten an einigen Stellen deutsche Originalstrings durch.
  • Changed Doku-Seite (docs.mspercury.com) komplett aktualisiert in DE/EN/ES. Die Funktionsliste erweitert von 12 auf 24+ Blöcke und reflektiert den jetzigen Stand inkl. KI-Tier-1A bis 1D, Sprint-4-Dashboard, Public-CheckUp-Lead-Gen, Iframe-Embed, UTM-Attribution, Manual MRR Override, Quote-Post-Accept-Editing, Vereinbarungs-Vorlagen, Web-Push, Branchen-Routung. Roadmap restrukturiert in „Jetzt / Nächst / Später / Bereits umgesetzt" mit Mai-2026-Daten für die neuen Features. Getting-Started erweitert von 7 auf 8 Schritte (explizites Onboarding, KI-Aufwand-Pfad neben Katalog-Pfad, optionaler Public-CheckUp-Schritt mit UTM-Beispiel). FAQ-Cancel-Frage spiegelt das tatsächlich shipped Self-Service-Löschen statt der Roadmap-Versprechung.
  • Fixed Angebots-PDF-Folgeseiten (Seite 2 und folgende) hatten nahezu keinen Top-Margin — Inhalt klebte am oberen Seitenrand und brach DIN 5008. Ursache: die @page-Margin-Regel hatte `top: 0` als Default mit einer `@page :left, :right`-Override-Gruppe, deren Cascade in WeasyPrint nicht greift. Behoben: Default ist jetzt 25mm Top, nur die `:first`-Page (mit eigener `.din-header`-Padding-Logik) überschreibt auf 0. Bestehende, bereits exportierte PDFs sind nicht rückwirkend betroffen — neu generierte Angebots-PDFs respektieren ab sofort die DIN-konforme Top-Margin.
  • Fixed Manueller MRR-Override + Quote-Post-Accept-Editing-Audit (eigentlich schon im Code seit 2026-05-03) waren ohne die zugehörigen DB-Migrationen ausgerollt — die Spalten `customers.manual_monthly_revenue` + `_note` und `quotes.last_edited_at_post_accept` + `last_edited_by_user_id` existieren jetzt tatsächlich auf prod und der Override + die Audit-Spalten funktionieren. Plus die heute neuen Migrationen für `organizations.default_hourly_rate` und sieben neue Spalten auf `prospect_leads` für UTM-/Source-Capture.
  • Security SSH-Server-Endpoint ist von Port 22 auf Port 2222 migriert worden, um Hetzner-Edge-Anti-DDoS-Filter dauerhaft zu umgehen. Der Filter ziehlt auf `(src_ip, dst_port=22)`-Tupel und hat in der Vergangenheit den Deploy-Pfad mehrfach für Stunden lahmgelegt. Phase 1 (Port 2222 zusätzlich aufnehmen) ist live und verifiziert; Phase 2 (Port 22 entfernen) folgt nach 24h-Soak. Für Sie als Kunde keine Auswirkung — nur ein Operator-/Deploy-Pfad-Detail.
1 month ago

Customer-Communication-Hub: Service-Berichte, Wartungsfenster, Status-Stream + Diskussion

9111
  • Added Status-Stream pro Projekt — kurze Updates an Ihre Kunden ohne den Umweg über E-Mail-Texte. Auf jedem Projekt gibt es einen neuen Reiter „Status & Meilensteine". Sie wählen eine Kategorie (Info, Bestellt, Geliefert, Geplant, In Arbeit, Wartet auf Kunde, Erledigt — jede mit eigenem Icon und Farbe) und schreiben einen Satz wie „18× Lenovo M70q bestellt, geplante Lieferung 15.05.". Ihr Kunde bekommt sofort eine E-Mail im MSPercury-Branding mit direktem Link ins Portal und sieht den Eintrag dort als chronologischen Verlauf zu seinem Projekt. Über einen Toggle „Im Kundenportal sichtbar" können Sie auch interne Notizen für Ihr Team posten, die nie nach außen gehen.
  • Added Meilenstein-Tracker pro Projekt — die „Pizza-Tracker"-Variante für IT-Projekte. Sie definieren die linearen Stufen Ihres Projekts (z. B. „Vertrag unterzeichnet → Hardware bestellt → Hardware geliefert → Installation → Übergabe") mit optionalem Soll-Datum, hakeln sie mit einem Klick ab, sobald sie erledigt sind. Im Kundenportal wird das als horizontaler Fortschrittsbalken mit nummerierten Stufen sichtbar — der Kunde fragt nicht mehr nach „wie weit sind wir denn jetzt?", er sieht es. Manuelles Umsortieren per Drag funktioniert; ein freier Sortier-Index lässt Sie auch nachträglich Stufen einschieben, ohne alles neu nummerieren zu müssen.
  • Added Service-Berichte — der digitale Servicebericht ersetzt den Zettel nach dem Vor-Ort-Termin. Sie tippen nach einem Einsatz Titel, Einsatzdatum, Aufwand in Stunden und einen Beschreibungstext (was wurde gemacht, welche Geräte, Auffälligkeiten, Empfehlungen). Erst als Entwurf speichern, dann mit einem Klick „An Kunde senden" — der Kunde bekommt eine E-Mail und liest den Bericht im Portal. Auf seiner Seite kann er „Erhalten bestätigen" (für die Soft-Quittung, dass er den Bericht gesehen hat) oder mit einem getippten Namen rechtsverbindlich unterzeichnen. Status-Linie: Entwurf → Gesendet → Erhalten → Unterzeichnet, mit IP-Audit beim Signieren. Drafts können Sie noch beliebig bearbeiten oder löschen; gesendete Berichte bleiben als Audit-Trail erhalten und sind nicht mehr nachträglich änderbar.
  • Added Diskussion direkt am Service-Bericht. Wenn Ihr Kunde nach dem Lesen eine Rückfrage hat („War die SSD im Server 1 oder 2?", „Können wir die nächsten Reboots auf abends 19:00 legen?"), schreibt er das als Kommentar direkt am Bericht — kein Mail-Tennis mehr, alles bleibt dokumentiert am richtigen Objekt. Sie sehen die Antwort als ungelesene Markierung in der Glocke oben rechts und antworten ebenfalls inline. Auf der Kunden-Seite ist der Verlauf als Chat dargestellt (Ihre Antworten links grau, seine Nachrichten rechts in Ihrer Markenfarbe). Lese-Markierungen automatisch beim Öffnen — kein Klick auf „als gelesen" nötig.
  • Added Wartungsfenster-Workflow — geplante Wartung mit Kunden-Bestätigung. Auf jedem Projekt gibt es einen neuen Reiter „Wartung". Sie schlagen einen Termin vor (Titel, Start- und Endzeit über datetime-Picker, optionale Beschreibung wer erreichbar ist während der Wartung) und entscheiden, ob mit Service-Unterbrechung zu rechnen ist (das blendet beim Kunden ein Warn-Icon ein). Der Kunde bekommt eine E-Mail mit Termin und Detailinfo, kann im Portal entweder bestätigen oder einen anderen Termin vorschlagen — letzterer mit einem freien Begründungstext, den Sie dann auf Ihrer Seite als „Kunde wünscht Verschiebung" eingeblendet sehen. Wenn Sie das Datum nachträglich verschieben, springt der Status automatisch zurück auf „Vorgeschlagen", damit der Kunde erneut bestätigt — Metadaten-Änderungen am gleichen Slot triggern keine erneute Bestätigung. Vier Lifecycle-Mails: Vorschlag, Verschoben, Aktualisiert, Abgesagt — alle in der Sprache Ihres Kunden (DE/EN/ES).
  • Added Diskussion direkt am Status-Update. Genauso wie bei Service-Berichten kann Ihr Kunde jetzt auch unter jedem Status-Stream-Eintrag eine Rückfrage hinterlegen — kein extra Telefonat, kein eigener Ticket-Loop. Inline-`details`-Aufklappbereich pro Eintrag mit Composer und chronologischem Verlauf. Interne (nicht-kundensichtbare) Status-Posts haben naturgemäß keinen Diskussions-Faden; die Funktion erscheint dort nicht. Beidseitige Lese-Markierungen wie bei Berichten.
  • Added Persönlicher Ansprechpartner pro Kunde. Auf der Kunden-Detail-Seite gibt es ein neues Dropdown unter den Kontaktdaten („Ansprechpartner (MSP-Seite)"), in dem Sie aus den Mitgliedern Ihres Workspaces einen für diesen Kunden zuweisen können. Im Kundenportal erscheint daraufhin oben auf der Übersichtsseite eine „Ihr Ansprechpartner"-Karte mit dem Namen Ihres Mitarbeiters statt des allgemeinen Firmennamens — der Kunde sieht namentlich, wer für ihn zuständig ist, mit direktem Mailto-Link. Ohne Zuweisung bleibt es bei den allgemeinen Workspace-Kontaktdaten.
  • Added Glocke oben rechts zeigt jetzt vier Quellen statt zwei: Quote-Nachrichten, Task-Kommentare, Service-Bericht-Diskussionen (grün) und Status-Update-Diskussionen (hellblau) — alles in einem Badge mit der Gesamtzahl. Klick auf einen Eintrag landet direkt am richtigen Surface (Bericht aufgeklappt, Status-Post aufgeklappt) und markiert den Eintrag automatisch als gelesen, damit Sie ihn nicht zweimal sehen. Der Operator hat damit eine zentrale Inbox für jede Kunden-Reaktion in jedem Kanal — kein Suchen mehr durch Tabs.
  • Added „Aktuelles" auf der Kundenportal-Übersicht erweitert. Die chronologische Activity-Liste zeigt zusätzlich zu Vereinbarungen, Angeboten und CheckUp-Berichten jetzt auch Service-Berichte (mit „neu — bitte bestätigen"-Badge wenn ungelesen) und Wartungsfenster (orange-amber wenn Bestätigung ausstehend, grün wenn bestätigt) — sortiert nach Aktualität, max. 8 Einträge. Der Kunde sieht beim Reinkommen ins Portal auf einen Blick, was es Neues gibt und was er noch tun soll.
  • Changed Polish im Kundenportal-Status-Feed: jeder Eintrag hat jetzt eine farbige Emoji-Icon-Spalte links (kategorie-getönt, halbtransparenter Hintergrund), das Kategorie-Label oben in CAPS-Letterspacing — das Layout liest sich als Timeline, die Kategorie ist auf einen Blick erkennbar.
  • Fixed Info-Kategorie-Chip im Status-Stream-Composer war im Dark-Mode unleserlich (heller Hintergrund mit hellem Text). Ursache: das Tone-Token `dark:bg-ink-800` war versehentlich gesetzt — das semantische Token `ink-800` wird im Dark-Mode automatisch invertiert, der `dark:`-Override hat das nochmal umgekehrt und so einen weißen Hintergrund produziert. Die `dark:`-Modifier auf der `info`-Tone wurden entfernt; die anderen Kategorien (Amber, Sky, Violet, Rose, Emerald) nutzen echte Tailwind-Farben und brauchen ihre `dark:`-Varianten weiterhin.
  • Security Portal-Token-Sperre wirkt jetzt sofort. Beim Audit der neuen Customer-Portal-Endpoints (Acknowledge, Sign, Maintenance-Accept/Reschedule, Comment-Posts) ist aufgefallen, dass alle fünf Token-Validierungen einen Lifecycle-Check auf eine Spalte `expiresAt` machten — die es auf `customer_portal_tokens` aber gar nicht gibt (das Schema kennt nur `revokedAt`, gesetzt beim Operator-seitigen Rotate / manuellen Widerruf). Der Check war damit toter Code und hat nie ausgelöst, gerollte oder widerrufene Tokens blieben technisch nutzbar. Tenant-Isolation durch `customerId`-Binding war davon nicht betroffen — kein Cross-Customer-Leak möglich. Aber ein Operator, der einen Token rotiert hat, konnte sich nicht darauf verlassen, dass der alte Link sofort tot ist. Behoben: alle fünf Endpoints (inkl. der älteren task-comments-Aktion mit derselben Bug-Quelle) prüfen jetzt `revokedAt` und werfen 401, sobald gesetzt. Zusätzlich wurden in derselben Runde defense-in-depth `organizationId`-Doppel-Bindings in alle UPDATE/DELETE-Statements der neuen Actions eingezogen — falls ein zukünftiger Schema-Drift jemals Customer/Org-IDs entkoppelt, fehlschlagen die Writes laut statt leise.
1 month ago

Sign-flow + signed-PDF fixes, customer status auto-promotion, maintenance banner

27
  • Added Customer pipeline status now updates itself when reality moves the deal forward. First quote that goes out to a customer with no classification yet → Warm prospect. First accepted quote (operator-side flip OR customer accepts via the share link) or signed agreement → Active customer. The funnel-stage promotions (cold/warm/hot → active) only happen when there's a clear conversion signal — manually-set values otherwise stick, so a deliberate "cold" or "hot" doesn't get clobbered by an unrelated quote going out. Archived customers are never auto-revived; if an archived customer signs a one-off addendum, that's the operator's call to reactivate, not the system's. Promotion runs as fire-and-forget — failures are logged to journalctl but never block the action that triggered them, so a customer signing an agreement always succeeds end-to-end even if the promotion update hits a transient lock.
  • Added Maintenance banner. Configurable via three env vars on the systemd unit — MAINTENANCE_BANNER_TEXT for the message, MAINTENANCE_BANNER_UNTIL for an optional ISO timestamp that renders a server-computed countdown alongside the text, MAINTENANCE_BANNER_TONE for info / warning / danger colour. Visible at the top of every page including the login screen and marketing pages, so an unauthenticated visitor also sees "Wartung in 15 Minuten" before they try to sign in. Activate by adding the vars to mspercury.service and `systemctl restart mspercury`; deactivate by clearing the text var and restarting again. No DB row, no client JS — countdown is fresh on each page load by re-reading the timestamp at request time.
  • Fixed Customers were getting "Link not found" after successfully signing an agreement. The DB row was correctly flipped to status=signed and the signed-PDF email actually went out, but the page they landed on couldn't see any of that because Astro's default form-action redirect uses the Referer header — and modern browsers (Strict-Origin-When-Cross-Origin is the default) strip the query string from Referer on POSTs. The page reloaded as `/agreements/<id>/sign` with no `?token=…`, the token-based lookup returned nothing, and we rendered the not-found state. Fixed: the action now returns the token in its success payload so the page handler can do an explicit Astro.redirect with the query string preserved. After the redirect the page sees status=signed and renders the green confirmation panel + Download CTA. On error paths the token is also recovered from `result.input` so "Already signed" / "Token expired" messages render properly instead of the generic not-found.
  • Fixed Submit button on the public agreement-sign page was rendering white-on-white and effectively invisible. Cause: the button used the workspace's brand colour as background — a near-white brand colour combined with white button text turns the only call-to-action on the page into nothing. The branded header banner above the document can stay accent-coloured, but the actionable buttons — Sign and Download signed PDF — are now hard-coded to a dark slate so they're always readable regardless of brand-colour choice.
  • Fixed Signed-agreement PDF metadata block was laying out wrong: "Datum 30. April 2026 um 21:49" was wrapping into a tall column, "Kunde ACME GmbH" was running into "IP-Adresse 209.198.153.96", and the document hash was splitting into 4 stacked chunks. Cause: the meta `<dl>` used `display: table` with `display: table-cell` on each `<dt>` and `<dd>` — without explicit row wrappers all eight cells were crammed onto a single CSS-table row, getting ~1/8th of the page width each. Fixed by wrapping each label/value pair in a row container with `display: table-row`, plus `white-space: nowrap` on the labels so they can't wrap into multi-line columns. Existing already-signed PDFs are baked to disk and won't change retroactively; only newly-signed agreements pick up the fix.
  • Fixed Signing email's sign-link was pointing at http://127.0.0.1:4327. The detail page was building the URL from `Astro.url.origin`, which behind nginx is the upstream Node socket — useless to the recipient. Now uses the publicBaseUrl helper (PUBLIC_APP_URL → X-Forwarded-Host → request URL fallback chain), and the sendAgreement action also rebuilds the canonical URL server-side from the freshly minted token, so even a stale URL the operator pasted in can't ship a broken host.
  • Fixed Agreement emails arrived as plain text in some inboxes. Cause: the body was wrapped in raw `<p>` tags but had no full HTML document — Outlook (and a few others) fell back to text/plain. Now both the sign invite and the signed-confirmation mail go through the existing branded renderTransactional shell (full <!doctype>, MSPercury wordmark, IT Systeme Flores UG legal footer, dark CTA button), with a localised "Jetzt unterzeichnen" / "Sign now" / "Firmar ahora" button pointing at the canonical sign URL on the invite. URL auto-linkification in operator-authored email bodies was also added in the same pass, so any URL typed into a quote / agreement / customer-thread reply renders as a clickable `<a>` regardless of which client the recipient uses.
  • Fixed Native <details> disclosure marker was leaking outside the styled card on the /changelog page in some browsers, showing a stray triangle next to one of the rows. Hidden globally via summary::-webkit-details-marker + summary::marker, so the only chevron is the SVG one rendered inside the summary.
  • Fixed Filter pills on /customers were unreadable when selected in dark mode — the active state used bg-ink-900 text-white, but ink-900 flips to a near-white token in dark mode while text-white doesn't, producing white-on-white. Switched the active pill to the brand accent (bg-accent-700 text-white), which has fixed contrast in both themes.
1 month ago

Notification dismiss, customer pipeline status, internal team notes, branded agreement emails

5
  • Added Notification bell can be cleared without opening every quote. Each item in the dropdown has a small × button on hover that marks just that one as read; the header gained a "Mark all read" link that drains the entire unread queue in one click. Both actions are idempotent — clicking dismiss on something that's already read is a no-op, so a stale tab can't undo a fresh action. Localised in DE/EN/ES ("Alle als gelesen" / "Mark all read" / "Marcar todo leído").
  • Added Customer pipeline status. Customers now carry an optional classification — Cold prospect, Warm prospect, Hot prospect, Active customer, Archived — surfaced as a coloured pill on the customer detail page and as a new column on the customer list. The new/edit form has a dedicated picker with an empty default of "Unclassified" so fresh imports don't get a fake "active" status. The list page has a pill-row filter (All · each status · Unclassified) above the table that composes with the existing search field, so you can answer "which warm prospects haven't I followed up on?" in two clicks instead of scrolling. DB column is nullable + additive — existing customers default to Unclassified until you pick something.
  • Added Internal notes thread on the customer detail page. Operator-only — no public surface, no email dispatch — meant for the team's running conversation about the relationship: who's the champion, why the deal stalled, when the renewal date hits, who you spoke to last. Multi-author, timestamped, with a pin-on-post checkbox so the high-signal stuff ("renewal 2026-08", "champion left, talk to Jana now") floats to the top. Pinned/unpinned by anyone in the workspace; only the original author can delete their own notes. Author name is captured both as a foreign key (so future filters work) and as a snapshot string, so a note still renders the original author's name even after they leave the workspace. DE/EN/ES throughout.
  • Added MSPercury-branded HTML template for agreement emails. The DPA-sign invite + the signed-PDF confirmation now go through the same renderTransactional shell quote-thread mails already use: full <!doctype> document, white card on a warm grey background, MSPercury wordmark on top, IT Systeme Flores UG legal footer at the bottom. Sign invitations get a prominent black CTA button — "Jetzt unterzeichnen" / "Sign now" / "Firmar ahora" — pointing at the canonical sign URL.
  • Added Auto-linkification in transactional email bodies. The plain-text-to-HTML helper (used by every operator-authored email) now detects http/https URLs in the body and wraps them in clickable <a> tags with proper styling and target="_blank". Without this, Outlook in particular leaves bare URLs un-clickable; now the link works regardless of which client the recipient uses. Three duplicate copies of the helper were consolidated into one src/lib/mail/format.ts.
1 month ago

CheckUp wizard, customer language, AI summary controls, sender branding, PDF page breaks

103511
  • Added "Nicht zutreffend" on every CheckUp wizard question. Skip is no longer gated by a per-question flag — any question (Firewall on a solo-self-employed customer, server hardening on a 0-server site, you get the idea) can be marked irrelevant. Skipped questions create no findings, so they're score-neutral, and they're listed under "— nicht zutreffend" on the review screen + PDF report so the audit trail stays honest.
  • Added Score badge in the /checkups list. Finalised CheckUps now show a coloured score pill (0–100 %) right next to the status badge, using the same emerald → amber → rose palette as the report. Drafts stay unchanged because their finding set isn't frozen yet.
  • Added Customer-level preferred language. Customer create/edit form has a new "Preferred language" picker (Org default · Deutsch · English · Español). When set, it overrides the workspace default for every customer-facing artefact: the email-draft default, the share-link CTA copy, the rendered PDF locale on quote and CheckUp report sends. Empty value ⇒ falls back to the workspace's brand language.
  • Added Language picker in the email-send modal. Above subject + body, a small DE/EN/ES tab row lets you swap the entire draft to a different language with one click. Per-language edits are remembered while the modal is open, so flipping back to the original tab restores what you wrote there.
  • Added Sender branding: "<Your name> via MSPercury". Customer-facing emails (quote PDF, quote share link, CheckUp report) now arrive with the From-display-name set to the operator's name (or workspace name if the user has no display name on file) — so the recipient sees who the email is from immediately, not a generic "MSPercury". The mailbox stays at noreply@mspercury.com so DKIM/DMARC keep aligning. System mails (password reset, verify, invites, Stripe webhooks) are unchanged on principle.
  • Added AI executive summary modal with language + detail-level controls. The old one-click button is gone, replaced by a proper modal: pick the output language (defaults to your brand language but freely overridable per generation), pick a detail level — Brief (~150 words, scannable for the C-suite), Standard (~300 words, the v1 default), or Detailed (~700 words, structured with numbered recommendations and a 30/90/180-day plan). On submit the modal shows a foreground progress overlay with elapsed-time counter and rotating stage labels ("Calling Anthropic" → "Streaming response" → "Polishing" → "Saving") so you actually see that the AI is working, instead of just the browser-tab spinner.
  • Added Standalone executive-summary PDF — accompanies the full CheckUp report. New endpoint at /checkups/{id}/summary.pdf renders a single-page A4 with letterhead, recipient block, audit-date + score pill, and the AI-generated prose. Download button on the CheckUp detail page next to the saved-summary preview, plus a small DE/EN/ES locale pill so you see at a glance which language the saved text is in.
  • Added "Also attach summary as PDF" toggle in the CheckUp send-to-customer modal. When the CheckUp has a saved AI summary, the modal grows a checkbox (default-on) that ships the standalone summary PDF as a second attachment alongside the main report. Renderer fails hard rather than silently dropping the attachment — you opted in, so a half-delivered envelope would be more confusing than a clear error you can retry.
  • Added AI integrations card redesign. Settings → AI integrations now shows a structured connection panel (provider, model, masked API key, encryption note, scope-is-this-tenant-only note) with a dedicated red "Disconnect" button that wipes provider, model and the encrypted key from the workspace row in one click. The free-text "Currently connected to anthropic." line that used to sit under the description is gone — it's the panel now.
  • Added Settings → Language now syncs to workspace brand language for admins. Until today, picking Deutsch in Settings only updated your personal UI language (users.preferredLanguage); the workspace brand language (organizations.preferredLanguage, used for outgoing letters and AI summaries) stayed wherever it was. They drifted. Now an admin language change updates both at once, so "my workspace is in German" and "emails go out in German" stop disagreeing. Non-admins still only flip their own UI.
  • Fixed AI executive summary was English even when the workspace was set to German. Two bugs piled on each other: the action read ctx.locals.locale (which a stale `mspercury_locale=en` cookie could pin to English even after you'd set Deutsch in Settings), and admin language changes never propagated to the workspace row. Fixed both. AI generation now follows organizations.preferredLanguage explicitly, with a per-call override exposed via the new modal — and the tag is baked into the saved summary marker (`--- AI-SUMMARY:de ---`) so the embedded section in the main report and the standalone summary PDF render labels in the same language the AI wrote in.
  • Fixed AI executive summary errors used to vanish — the action threw ActionError but the page never read the result, so a click did nothing visible. The detail page now reads getActionResult and renders a red banner with the actual Anthropic response (model name typo, revoked key, rate limit) under the button. Server-side, the catch path also writes to journalctl for debugging without re-running.
  • Fixed AI executive summary was blocked on finalised CheckUps. Lifted: the summary lives on `notes` and only feeds the executive-summary section of the PDF — it doesn't change findings, scores, or any audit-bearing data, so the freeze rule that protects findings doesn't apply. Generating one on a frozen CheckUp now works.
  • Fixed PDF reports cut content off at the page edge. The CheckUp report cover used a hard-set 297mm height that clipped overflow, and the findings sections forced page-break-inside: avoid on whole categories — categories with many findings then bled past the bottom edge. Same family of issues on the quote PDF (line-group avoid-break) and the internal monthly report (kpi/chart/health cards). All three templates now use targeted break-inside: avoid on the smallest meaningful units (cover stat row, individual finding card, single line row, kpi card, chart frame, health card), repeat <thead> on continuation pages via display: table-header-group, and let categories / line groups break naturally across pages with the heading break-after: avoid'd to its first row.
  • Fixed ecosystem.config.cjs in the repo implied PM2 was the prod process supervisor. It isn't — production runs the Astro build under a systemd unit (mspercury.service), which is what restarts now go through. The pm2 path on the server returns "command not found". The repo file is left intact for the moment to avoid breaking any unknown dev-side workflow but should be considered cosmetic.
  • Changed Email-draft default language now follows the tenant's effective system language. Previously every workspace got drafts in the org's preferredLanguage regardless of which language the operator was reading the UI in. Now the cascade is: customer.preferredLanguage (if set as an explicit override) → user.preferredLanguage → org.preferredLanguage → en. Net effect: the modal opens in whatever language you're currently using, with one click to switch per-customer.
  • Changed Email-drafts code consolidated. The 4 inline switch-by-locale blocks (PDF / share-link / CheckUp-report) that lived in two detail pages — ~120 lines of nearly-identical copy-paste — moved to lib/mail/drafts.ts with one helper per delivery mode. Each page now does six lines instead of fifty, and tweaking the greeting copy is a one-place change.
  • Changed Customer language picker extracted to a shared component. The select markup was duplicated between customers/new and customers/[id]/edit; now it's one CustomerLanguageField.astro that both pages consume.
  • Removed Unused `skip` translation key from de/en/es locale files. The wizard switched to the always-available `notApplicable` button last cycle, so the old key was just sitting there.
  • Security AI tenant isolation audit. The action path was already structurally clean — every read goes through ctx.locals.organization (session-bound), providerForOrg() builds the HTTP client only from that one org row, AES-256-GCM encryption at rest with a SESSION_SECRET-derived master key, random per-message IV, auth tag included. The Settings card now visibly says it: provider, model, masked key with "encrypted at rest, AES-GCM" hint, scope line "This workspace only — tenant-isolated". One Disconnect click wipes the row.
1 month ago

Account security overhaul: 2FA, passkeys, sessions, trusted devices, email + password change, account delete, GDPR export

1245
  • Added Forgot password? — finally. /login has a 'Forgot password?' link that sends a single-use reset link by email. After you choose a new password, every existing session and trusted device on your account is wiped, so a leaked password can't keep being used. Link is valid for 60 minutes.
  • Added Active sessions panel on Settings → Security. Every device that's currently signed in to your account is listed with the browser, IP and last-active time. Sign out a single session, or 'Sign out everywhere else' to nuke every cookie except the one you're holding.
  • Added Trusted devices for 2FA. On the /login/2fa prompt you can tick 'Trust this browser for 30 days' — that browser then skips the code on every sign-in for the next month. Manage and revoke individual trusts on Settings → Security. Trusts are wiped automatically when you reset your password or disable 2FA. Backup-code logins never set trust on principle (you're on an emergency device by definition).
  • Added New-device sign-in alert email. Whenever someone signs in to your account from a browser/IP combination we've never seen, we send you an email with the device, IP, time and a one-click 'Reset my password now' button. Browser auto-updates don't trigger false alerts (we strip version numbers from the fingerprint), and the very first device on a fresh signup is silently enrolled.
  • Added Recent activity feed on Settings → Security shows the last 10 security events on your account: sign-ins, 2FA changes, passkey changes, password resets, support resets. If something happened that you didn't do, you'll see it here.
  • Added Support-reset hatch for lost 2FA. If a customer loses both their authenticator app AND all backup codes, we (operator) can now reset their 2FA from /superadmin/users after out-of-band identity verification — the action requires a ticket reference and writes a permanent audit trail. Previously the only recovery was direct DB editing.
  • Added Settings → Security link in the /settings sidebar. The page existed but wasn't reachable from anywhere — fixed.
  • Added Change email address. Settings → Profile → Change. Type your new address + current password, we send a 6-digit code to the new inbox; once you confirm it, the address is updated and the old one gets a security notice (so a hijacker who learned your password can't silently swap the email out from under you).
  • Added Change password while signed in. Settings → Profile → Change password. Verify current password, set a new one. Other sessions on your account are signed out automatically; this device stays signed in. Trusted-device cookies are preserved (re-trusting your laptop on every quarterly rotation would be hostile UX). Distinct from forgot-password, which wipes everything as a recovery measure.
  • Added Self-service account deletion. Settings → Profile → Delete account…. Two-factor confirmation (current password + typing your email verbatim), with a blast-radius preview that calls out the exact counts (X customers, Y projects, Z quotes) for sole-user workspaces, or 'data stays with the other N members' for multi-user. Single users delete their workspace alongside; otherwise just the user goes. Notice email + audit event fire BEFORE the data is wiped (the only window we have).
  • Added Download my personal data. Settings → Profile → Download .json. Single-file dump of every byte we hold for your account: profile, sessions with UA/IP, 2FA + passkey state (without the secrets), trusted + known devices, email-change + password-reset history, full security event log. Excludes workspace data (separate export for that), encrypted secrets, and other users' info. GDPR Article 15.
  • Added Workspace-level 2FA enforcement (admins only). Settings → Workspace → Require two-factor for everyone. When on, every member without TOTP is bounced to /settings/security on every page until they enrol. Lock-out-guard refuses to enable unless the toggling admin already has 2FA on their own account; the action requires the admin's password as defence in depth.
  • Fixed Adding a passkey was failing with 'Could not verify the credential. Try again.' The server was passing only the inner attestation response to the WebAuthn verifier instead of the full credential envelope. Adding a passkey now works end-to-end on Touch ID, Windows Hello, YubiKeys, etc.
  • Fixed Buttons on Settings → Security were inconsistent: some were ghosted/transparent in dark mode, others used a hard-coded green that ignored your workspace's accent color. All eight buttons now go through the unified Button component, which means they pick up the brand color you set in Settings → Branding automatically.
  • Fixed Backup codes after enabling 2FA were unreadable in dark mode — the code chips had a hard-coded white background that turned them into invisible white-on-light text. Now use the same surface tokens as the rest of the app and adapt to both themes. Same fix applied to the TOTP setup-key chip.
  • Security Rate-limiting on every auth endpoint. Login (form + JSON), TOTP completion, passkey assertion, signup PIN issuance + confirmation, and password-reset start + complete all have per-IP and (where applicable) per-email/per-uid throttles. Bcrypt is rejected before the slow check runs, so an attacker can't burn CPU. Successful logins reset the bucket so a typo or two doesn't lock anyone out.
  • Security Successful password reset wipes EVERY session on the account, not just the cookie that triggered the reset. Same for trusted devices. This closes the 'attacker had a stale session, victim resets password to recover, attacker session keeps working' gap.
  • Security Audit events for every security-relevant change: 2FA enable/disable, backup-code regeneration, passkey added/removed/renamed, password reset + change, support reset, new-device sign-in, email change, account deletion, workspace-2FA enforcement toggle, personal-data export. Surfaced both in the user's own Recent activity feed and in the operator's superadmin event log.
  • Security Migration safety rail: deploys now go through scripts/safe-migrate.ts, which captures drizzle-kit's planned SQL and refuses to apply it if it contains DROP TABLE, DROP COLUMN, RENAME COLUMN, DELETE FROM <table>, or TRUNCATE — unless the operator explicitly opts in with ALLOW_DESTRUCTIVE=1. Replaces the prior `drizzle-kit push --force` deploy step.
  • Security PRAGMA foreign_keys = ON now runs on every DB connection. SQLite ships with FK enforcement OFF by default; without this, a stray DELETE FROM <parent> would silently leave orphan rows in every dependent table. Now cascades work correctly for runtime ops; the safe-migrate rail prevents accidental destructive ops at the migration layer.
  • Fixed Honesty note from this morning: a drizzle-kit migration on the new workspace-2FA-enforcement column emitted an unexpected `delete from organizations;` before the ALTER, which wiped the org metadata for both production workspaces (workspace name, branding, AI provider config, contact details, billing fields). Recovered within ~10 minutes by reconstructing the org rows from the surviving orphan data (users, customers, projects, quotes, sessions, events all kept their original org_id pointers because FKs were OFF). The migration safety rail and FK pragma above are the structural fixes that prevent a repeat. If your workspace contact info, branding color, AI provider key, or logo upload reference looks blank in Settings — please re-enter; the actual files (logo, customer data) are intact.
1 month ago

Angebote per Link teilen

52
  • Added Angebote lassen sich jetzt per Link mit Kunden teilen. Der Kunde klickt in der E-Mail auf den Button und sieht das Angebot direkt im Browser — mobilfreundlich, druckbar, als PDF speicherbar. Funktioniert ohne MSPercury-Account.
  • Added Optionaler PLZ-Schutz: ist in der Kundenkartei eine Postleitzahl hinterlegt, fragt die Share-Seite diese ab, bevor das Angebot sichtbar wird. So wird der Link nutzlos, falls er an falsche Adressaten weitergeleitet wird.
  • Added Neuer "Copy link"-Button auf der Angebots-Detailseite: erzeugt den Share-Link und legt ihn bereit, damit ihr ihn in euer eigenes Mailprogramm einfügen könnt, statt über MSPercury zu versenden.
  • Added Öffnet der Kunde den Link, seht ihr das — Zeitpunkt des ersten Öffnens und die Anzahl der Aufrufe werden mitgezählt. Links laufen nach 30 Tagen automatisch ab und können jederzeit manuell entzogen werden.
  • Added Neues PLZ-Feld im Kundenformular — dient als Verifikations-Code für die Share-Link-Freigabe.
  • Changed Angebots-PDF kommt jetzt im DIN-5008-konformen Briefkopf (korrekte Position für Fensterkuverts, Bezugszeichenzeile, Betreffzeile).
  • Changed Steuern im Angebot jetzt klar nach §14 UStG aufgeschlüsselt: pro Periode Netto, Rabatt, Netto nach Rabatt, MwSt-Satz und -Betrag, Brutto-Gesamt.
1 month ago

Security audit + hardening, docs site, SEO pass

46
  • Security Fixed an open-redirect on login. A crafted link like /login?next=https://evil.example would send the user to the attacker's site after they signed in. Only same-origin paths are accepted now; anything else falls back to /dashboard. Same fix applied to the Microsoft SSO callback.
  • Security Signup no longer leaks which emails are customers. Trying to register an existing email used to return a visible '409: account exists' error; now the response looks identical to a new signup and the real account holder gets a 'someone just tried to create an account with your email' notification instead.
  • Security Login now rotates the session cookie. Any leftover session token is invalidated before a fresh one is issued, neutralising a narrow 'pre-plant a cookie in the victim's browser' attack.
  • Security Superadmin DPA flow could be abused, in the worst case, to delete arbitrary files on the server via a malicious path value. The field now only accepts a safe filename pattern.
  • Security SQLite database file permissions tightened from 644 to 600 (owner-only). Legacy TLS 1.0/1.1 lines removed from the main nginx config (the stricter TLS 1.2/1.3-only policy was already in effect at the vhost level).
  • Security Independent code-level security audit run across the whole app: session handling, multi-tenant isolation, Stripe webhook signature validation, PDF template XSS escaping, secret handling, CSP, middleware bypass guards — all came back clean. Findings above were the complete actionable set.
  • Added NinjaOne monitoring and patch-management agent installed on the VPS. Reports to the EU datacenter; no application data leaves the server.
  • Added Documentation subsite launched at docs.mspercury.com. Seven pages in English, German and Spanish (What it is / Getting started / Features / Known issues / Status / Roadmap / FAQ). The Status page has a live health widget, the header has coloured shortcut pills for the three high-traffic pages, and each page has its own collapsible table of contents in the sidebar.
  • Added Landing-page SEO + AI-search pass: hreflang alternates for all three languages, richer OpenGraph + Twitter Card metadata, JSON-LD structured data with SoftwareApplication + Organization + WebSite + FAQ nodes optimised for AI answer engines. New /robots.txt explicitly allows GPTBot, ChatGPT-User, ClaudeBot, PerplexityBot, etc. and a dynamic /sitemap.xml lists the public pages.
  • Added App nav reshuffle: Docs / Changelog / Feedback moved to a dedicated 'Help' section pinned to the bottom of the sidebar, always one click from the bottom regardless of how much scrolls above. Docs opens the external subdomain in a new tab.
1 month ago

Feedback form, superadmin overview, cookie notice, signup consent

533
  • Added In-app feedback form at /feedback. Pick Bug / Feature request / Other, write a message, send. It lands in the superadmin dashboard and fires an email to the operator, with Reply-To set to the submitter so replies go directly to the user.
  • Added Superadmin dashboard at /settings/admin with a cross-tenant overview: stat cards for Tenants / Users / CheckUps / Quotes / Pay-what-you-want / Feedback, per-workspace counts (users, customers, projects, services, findings, quotes), DPA signing status, and a 'Seed sample data' button for workspaces that haven't loaded the starter catalog yet. Recent-users and recent-feedback tables below.
  • Added Cookie notice banner on every page. MSPercury only sets strictly-necessary cookies (session + language + banner acknowledgement) — the banner is transparency, not a consent gate. The Cookie Policy page lists every cookie by name, purpose and lifetime.
  • Added GDPR-required explicit consent on signup: two separate checkboxes for Terms of Service and Privacy Policy, both mandatory, both open the policy in a new tab. Acceptance timestamps are stored on the user record for audit purposes.
  • Added Starter CheckUp findings library: 43 ready-made finding templates covering Security / Updates / Backup / Network / Compliance / Hardware / Permissions / Other. Each entry includes a description and suggested remediation in English, German and Spanish, a default severity + effort, and where applicable a link to the matching catalog service so a finding flows straight into a quote.
  • Changed Catalog seed is now locale-aware. When you click 'Load sample data', service names, descriptions and the default package name are picked in your workspace's language.
  • Changed English is now the default language for unauthenticated visitors, regardless of their browser's Accept-Language header. Logged-in users still get their saved language preference.
  • Changed Landing-page CTAs changed from 'Request invite' to 'Start free' — signup is open, no invite code needed.
  • Fixed PDF cover page had the company logo overlapping the company name (a WeasyPrint rendering bug). Fixed with a table-based header layout; the logo, name and address now sit cleanly next to each other.
  • Fixed Every POST form hit a CSRF false-positive under nginx-terminated TLS. Root cause: Astro's built-in CSRF check compared an https Origin header to an http request URL. The session cookie's SameSite=Lax attribute already blocks the underlying attack, so the Astro check has been disabled with a note so it's not silently re-enabled later.
  • Fixed Email delivery was broken for a week because Hetzner Cloud blocks outbound SMTP on port 465 for new accounts, and our mailer was pinned to that port. Switched to port 587 with STARTTLS; welcome, feedback and invite emails now go through.
1 month ago

Production deployment package + codebase audit

61
  • Added deploy/ folder with complete go-live package: README, DEPLOY master guide, ENVIRONMENT reference (20+ env vars), M365_SSO walkthrough (Entra app registration + multi-tenant), HETZNER (VPS + SMTP + Storage Box + DNS), STRIPE (products + tax + webhook), SSL_DNS (certbot + CAA + HSTS), PRE_LAUNCH_HARDENING (5 gap-closing features), RUNBOOK (day-2 ops + incident playbook)
  • Added deploy/nginx/mspercury.conf — reverse proxy with security headers, HSTS, gzip, long-cache for fonts + _astro assets, upstream timeouts tuned for WeasyPrint PDF rendering
  • Added deploy/systemd/mspercury.service — sandboxed unit running dist/server/entry.mjs as the mspercury user, strict NoNewPrivileges + ProtectSystem + ReadWritePaths limited to data/ + /tmp, restart-on-failure with backoff
  • Added deploy/scripts: deploy.sh (pull/build/migrate/restart with health check), backup-sqlite.sh (nightly SQLite .backup + tgz of uploads/attachments/dpa-signed → Hetzner Storage Box, 30-day retention), firewall.sh (UFW + fail2ban)
  • Added deploy/.env.production.template — sanitised env file with every variable documented inline, grouped by required / optional / third-party
  • Changed package.json: added start:prod running node ./dist/server/entry.mjs for production (existing start is dev-only)
  • Added Full codebase audit: no dead links, no orphan actions, all action imports verified in src/actions/index.ts, every href traced to an existing page, API endpoints cross-referenced with their callers, translation keys sample-verified
1 month ago

DSGVO launch-readiness: self-hosted fonts, rewritten privacy policy, Art. 28 DPA + download flow

91
  • Removed Google Fonts / Gstatic completely eliminated. Every <link rel='preconnect|stylesheet'> that pointed at fonts.googleapis.com or fonts.gstatic.com is gone — BaseLayout, MarketingLayout, AuthLayout, the onboarding page and the standalone CheckUp report. Source tree `grep googleapis` returns 0 hits
  • Added Self-hosted Geist variable woff2 under /public/fonts/ (plus a Mono variant for <code>) with one @font-face declaration in global.css and a font-display: swap. Each layout now <link rel='preload'> the font and ships a CSP meta tag locking font-src to 'self' data:
  • Added Privacy policy fully rewritten (DE + EN, same-file bilingual). 11 numbered sections with stable #anchor ids — Verantwortlicher, Datenschutzbeauftragter, Zwecke + Rechtsgrundlagen, Datenkategorien, Empfänger (Hetzner / Stripe / Microsoft / Apple), Drittlandübermittlung, Speicherdauer, Betroffenenrechte, Cookies (only technisch notwendig), PWA / iOS, Stand. Every company fact pulled from src/lib/legal/company.ts — no hardcoded placeholders
  • Added Auftragsverarbeitungsvertrag (AVV) / DPA at /legal/dpa — full Art. 28 Abs. 3 DSGVO coverage: Parteien, Gegenstand + Dauer, Weisungsrecht, Pflichten, Unterauftragsverarbeiter-Liste (Hetzner / Stripe / Microsoft), Kontrollrechte, TOMs (Zutritt / Zugang / Zugriff / Weitergabe / Eingabe / Verfügbarkeit / Trennung), Meldepflicht, Drittland, Rückgabe/Löschung, Unterschriftenfeld
  • Added GET /legal/dpa.pdf?lang=de|en — WeasyPrint-rendered PDF via the same pipeline as CheckUp / quote reports, with ?controller=<tenant> pre-filling the controller line on the signing page. Every download writes a dpa_records row so Settings / Superadmin can show status
  • Added Settings card 'Datenschutz & Auftragsverarbeitung' — two download buttons (AVV Deutsch / DPA English), Upload-signed-PDF dropzone that stores the countersigned file under data/dpa-signed/{orgId}/dpa-signed.pdf and advances the record to status='signed', plus a reset button for re-signing after TOM changes
  • Added Superadmin-only /settings/dpa-admin — table of every tenant with DPA status (not_signed / sent / signed), download + signing timestamps, and a 'Mark signed' shortcut for the common case where the customer emails the signed copy back
  • Added dpa_records schema + dpaActions (uploadSignedDpa, resetDpa, markDpaSignedForOrg) + upsertDpaRow helper with monotonic status progression (a re-download never demotes a 'signed' back to 'sent')
  • Added README §8 rewritten as a proper DSGVO go-live checklist: attorney review hard-gate, Hetzner / Stripe / Microsoft AVV checklist with archive paths, fonts/CSP verification, customer AVV workflow documentation, DNS CAA reminder
  • Added Smoke test: 13 new DSGVO assertions — no googleapis / gstatic strings on /, /fonts/geist-variable.woff2 200 + font/woff2 content-type, /legal/privacy?lang=de contains Verantwortlicher / Hetzner / Art. 6, /legal/privacy?lang=en contains controller / Article 6 variants, /legal/dpa.pdf?lang=de returns 200 (WeasyPrint) or 503 (graceful), /settings shows 'AVV herunterladen' + links to /legal/dpa.pdf
1 month ago

Health score, infrastructure wizard step, PDF photos, OS-scoped quote engine

62
  • Added 0–100 % health score on every CheckUp — shared helper drives an SVG ring hero on both the HTML report and the WeasyPrint PDF. Weights: −8 per high, −4 per medium, −2 per low finding, floored at 0; five color bands (excellent → critical) with locale-aware labels (EN/DE/ES)
  • Added Wizard infrastructure step at /checkups/wizard/{id}/infrastructure — captures per-OS workstation counts (Windows / macOS / Linux / ChromeOS / Other), server breakdown (Windows / Linux / Other), total users, infra notes and on-site photos before question 1. Runs first for every new wizard CheckUp; creates or updates the linked project transactionally
  • Added OS-scoped quantity types on the quote engine: per_windows_ws, per_macos_ws, per_linux_ws, per_chromeos_ws, per_windows_srv, per_linux_srv. buildQuoteFromCheckup resolves each finding's linked service against the OS counts captured in the infrastructure step, so service items whose unit says "Windows workstation" generate exactly the right line-item quantity
  • Added PDF report now embeds all attached photos as base64 data URLs in a dedicated "Photo appendix" section — 2-up grid, captioned with the question scope (infrastructure vs. question-id), printed at up to 95 mm height on A4 so they stay crisp
  • Added Report HTML redesigned mobile-first: class-based responsive CSS, meta grid collapses 4→2 columns under 640 px, action bar stacks on mobile and sits row-wise on desktop, title no longer breaks at em-dash (text-wrap: balance). Print CSS forces desktop layout back so PDFs stay compact
  • Added Wizard desktop layout: sticky top progress row, question content, and fixed bottom action bar share a consistent max-w-3xl inner column; fixed bottom bar gets md:left-60 so it starts past the 240 px sidebar instead of centering on the viewport
  • Fixed Image upload 400 error — null-tolerant schemas (questionId / caption) let multipart photo uploads succeed again. Verified end-to-end with a 631-byte test JPEG
  • Fixed enforceOnboarding middleware now skips beta accounts so demo / seed data always reaches the app without getting bounced to /onboarding
1 month ago

Guided CheckUp wizard with adaptive questions, photos, EN/DE/ES i18n

8
  • Added Prominent 'Start a new CheckUp' CTA card at the top of the dashboard — one tap kicks off the guided wizard
  • Added /checkups/wizard/{id}/step/{n} — mobile-first step page with sticky progress bar, type-aware inputs (yesno / single / multi / scale / text), keyboard shortcuts (Enter, J/N), optional note field, and camera-aware photo uploader with client-side canvas compression + EXIF strip
  • Added Adaptive conditional routing: questions with unmet `showIf` clauses are skipped server-side, so step numbers always point to the next visible question
  • Added Auto finding-rule engine: answers trigger deterministic findings (description + priority + effort + optional linked service code), tagged with sourceQuestionId so re-answering regenerates them in place without touching manual findings
  • Added /checkups/wizard/{id}/review — collapsible category groups, editable executive summary (auto-drafted from top-3 findings), add/remove findings, one-click Finalize, and Finalize+Quote when the CheckUp is on a project with service-mapped findings
  • Added 30 default questions across all 7 categories (Sicherheit / Updates / Backup / Netzwerk / Compliance / Hardware / Berechtigungen) — every prompt, context, option label and generated finding text in EN / DE / ES, stored as { en, de, es } JSON and resolved at render time via pickI18n()
  • Added /api/checkup-attachments/{id} streams uploaded photos with auth + org-ownership checks; uploads land under data/checkup-attachments/{orgId}/ and keep EXIF-stripped copies only
  • Added Smoke test: 12 new wizard + i18n assertions (catalog seeded with { en, de, es } JSON, pickI18n resolves across locales, conditional hiding, finding rules fire, EN/DE/ES wizard pages render locale-appropriate copy)
1 month ago

Pay-what-you-want, setup wizard, breadcrumbs, backup disclosure

513
  • Changed Rebranded away from 'closed beta': product is now Free + Pay-what-you-want, forever. No trial, no gated plans, no upgrade nags — voluntary contributions only
  • Added Pay-what-you-want form (Settings → Billing + public /pay): set a net amount, pick one-time / monthly / yearly, live 19 % VAT calc + gross total, collapsible invoicing-details block (legal name, address, country, VAT ID, tax no.) so Lucas can issue a §14 UStG-compliant invoice without back-and-forth
  • Added Two PWYW submit paths: email inquiry (from noreply@mspercury.com → info@it-flores.de for manual arrangement) + Stripe Checkout with custom-amount price_data + Stripe Tax when configured
  • Added Setup wizard at /onboarding — new workspaces are gated here until they confirm workspace name, language, currency, VAT rate and optional brand color
  • Added Breadcrumb navigation on every menu-driven page (Dashboard → Customers → Acme GmbH etc.) — auto-derived from route, with per-page tail overrides for detail entities; never leaks raw IDs
  • Added Terms of Service & DPA now explicitly disclose the backup policy: daily full-server backups DO happen; per-tenant snapshots / workspace restore points DO NOT EXIST YET. Users are urged to use the built-in ZIP export
  • Fixed PWYW form: amount input and interval select are now baseline-aligned on desktop (long label no longer wraps and pushes the input below its neighbour)
  • Fixed "Save project" / "Save CheckUp" / "New quote" forms no longer silently fail when a required select had no preselected value — forms now default to the first available record
  • Fixed Stale service worker cleanup in dev mode now does ONE forced reload per tab so in-flight SW handlers stop intercepting form POSTs after the purge
1 month ago

Docs + Changelog nav, SW dev-unregister, Post-Redirect-Get polish

23
  • Added Sidebar links to /docs and /changelog under a new Help section
  • Added /changelog page with date-grouped collapsible entries
  • Fixed Service worker is now disabled in dev mode and actively unregistered — stale caches no longer hide form-submit redirects
  • Fixed Settings actions (language, finance, branding, profile, AI) self-redirect after save so the page re-renders with fresh middleware locals, not stale ones
  • Fixed Primary buttons (New quote / New customer / New project / New CheckUp) get a dark-mode-aware accent variant so they're readable after theme inversion
1 month ago

Dark mode, Cmd+K search, AI integration, CSV + ZIP exports, /docs

10
  • Added Three-state theme toggle (system / light / dark) in the top header, no-flash-on-load
  • Added Global command palette via Cmd/Ctrl+K — searches customers, projects, CheckUps, quotes, finding templates, services, packages
  • Added Desktop top header with search trigger + theme toggle; mobile top bar gets matching icons
  • Added AI integration: Anthropic (Claude) + OpenAI (GPT) providers, AES-GCM-encrypted API keys, "Generate executive summary" button on CheckUp editor, auto-embedded into PDF reports
  • Added CSV export per entity (/api/export/<entity>.csv) for customers, projects, quotes, CheckUps, findings, services, packages
  • Added Workspace ZIP export (GDPR Art. 20) — full workspace or per-customer, streamed via archiver, contains JSON + CSV + uploaded logo
  • Added /docs page with 12-section feature walkthrough in English
  • Added Logo upload + primary-color picker in Settings → Branding; both flow through to PDF templates
  • Added Spanish locale (es) in addition to English and German
  • Added Settings → Finance: workspace-level currency picker and default VAT rate
1 month ago

CheckUp module, WeasyPrint PDF reports, invite-only beta

51
  • Added CheckUp audit module: finding templates library, priority + effort grading, quote generation from mapped findings, immutable snapshots on finalize
  • Added WeasyPrint-rendered PDF reports for CheckUps (cover + TOC + risk matrix + findings by category + methodology + appendix) and quotes (letterhead + line items per billing period + VAT breakdown + terms)
  • Added Invite-only beta signup gate: superadmin issues codes, signup requires ?invite=CODE, invited workspaces are flagged is_beta_account and exempt from Stripe lockout
  • Added Custom confirm modal replacing browser confirm() dialogs (mobile-first bottom-sheet, safe-area-insets)
  • Added PWA manifest + service worker + offline fallback + installable icons
  • Changed Pricing model paused during closed beta — free for beta participants, Stripe code retained for later activation
2 months ago

Mobile-first redesign, i18n, marketing + legal pages

7
  • Added Mobile-first nav: desktop sidebar + mobile top bar with drawer + bottom tab bar
  • Added EN/DE translation system with per-user preferredLanguage and Accept-Language fallback
  • Added Public marketing pages: landing, pricing, imprint, terms, privacy, DPA, cookies (EN + DE)
  • Added Design tokens: Geist font, warm stone-neutral ink scale, emerald accent, motion easing, shadow scale
  • Added Microsoft 365 OAuth login flow (multi-tenant, optional Graph organization read for auto-named workspace)
  • Added Hetzner SMTP mailer abstraction with EN/DE transactional templates (welcome, trial-ending, seat invite, payment receipt)
  • Added Stripe billing scaffolding: seat-based subscription, Customer Portal, Tax, webhook handler, trial lifecycle