Commit Graph

66 Commits (6283e100721f536c995f11d2c974c170429bb839)

Author SHA1 Message Date
boban 6283e10072 snap 2026-05-16 15:30:38 +02:00
boban 063da474f9 feat(design): refined dark editorial design audit across all views
- CSS: metric-num glow, card-shadow depth, card-accent-top treatment, table-row-hover
  with left accent bar, badge utility, nim-reveal staggered page-load animation,
  nim-skeleton shimmer, bg-grid, polished scrollbar, tighter letter-spacing on headings
- Dashboard: accent-topped metric cards, gradient fill chart, JetBrains Mono tooltips,
  leading-zero rank numbers, improved top-links table
- Links: refined table with tracking-widest thead, badge status pills with dot indicator,
  blue slug hover on A/B action
- QR Codes: type-colored icons, display_type badge, bar-chart empty state SVG
- Bio: bars icon, slug with blue highlight, published dot indicator
- Analytics: grouped range button pill, progress bar distributions per country/device,
  nim-reveal stagger, three metric cards with card-accent-top
- Topbar: sticky + backdrop-blur, accent-gradient avatar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:19:17 +02:00
boban 257d1b94f1 feat: QR wizard, UTM Builder, A/B variants, AI features, analytics URL filters
- QR Code: split-pane wizard with type tabs, live preview canvas, style/frame/advanced options
- UTM Builder modal: generate URL with UTM params, copy + create short link
- A/B Test: LinkVariants modal with auto-balance weights (min 2, max 5 variants)
- AI: SlugSuggester service (3 suggestions via claude-haiku), AI-slug button in CreateLink
- AI pages: /ai/insights, /ai/anomalies, /ai/ab-generator with sidebar entry
- Analytics: #[Url] attributes for range/country/device/linkId, 24h/7d/30d/90d buttons
- QR enum migration (MySQL-only guard) to add email/phone/sms/location types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:14:19 +02:00
boban 1ff4114674 feat(sidebar+profile+buttons): sidebar active state, 3-tab profile, consistent button variants
- Sidebar JS: DOMContentLoaded + livewire:navigated updateActiveNav, data-nav-link on links
- Profile: exactly 3 tabs (Personal Info, Preferences, Security) with wire:navigate
- Buttons: all primary actions use bg-accent-gradient text-white
- UTM Builder + copy-to-clipboard Livewire event handler in app layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:14:04 +02:00
boban 278bc42b37 feat(ui): blade-heroicons, local fonts, smaller base font, no emoji
- Install blade-ui-kit/blade-heroicons, replace emoji with x-heroicon-* components
- Inter Variable + JetBrains Mono served from /public/fonts/, no CDN
- html { font-size: 14px }, h1/h2/h3 scaled down
- Remove Google Fonts CDN links from all layouts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:13:53 +02:00
boban 2510620f67 fix(modals): make create/edit/delete modals actually save + auto-refresh lists
- All create modals now validate→create→dispatch(kebab-case event)→toast→close
- All edit/delete modals dispatch kebab-case events to Index classes
- Index components listen via #[On] and call resetPage() to refresh
- QR Code save() validates url field conditionally per type
- Tests updated to assert kebab-case event names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:13:44 +02:00
boban c67bde8fc5 feat: Toaster, button-component, 10 new modals, links-page fix, vite-tuning
- Toaster Livewire component (toast event, dismiss, auto-hide, 6 tests)
- <x-ui.button> Blade component with wire:loading indicator + variants
- QR modals: CreateQrCode, EditQrCode, DeleteQrCode (TDD, 8 tests)
- Bio modals: CreateBioPage, EditBioPage, DeleteBioPage (TDD, 7 tests)
- Domain modals: AddDomain, VerifyDomain, DeleteDomain (TDD, 7 tests)
- Profile modal: DeleteAccount with password confirm (TDD, 4 tests)
- Links page: fix openModal event name, wire up Edit/Delete to modals, toast on all CRUD
- vite.config.js: add chart.js/auto + optimizeDeps.force
- QrCode model: add newFactory() + QrCodeFactory
- Layout: Toaster embedded before </body>
- 141 tests / 296 assertions all green, verify 29/29

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:55:25 +02:00
boban 502a777afd fix: Chart.js global, Vite 504, test DB isolation, verify script green
- bootstrap.js: Chart.js registered components + window.Chart; Alpine stores moved here
- app.js: minimal (CSS + bootstrap import only)
- vite.config.js: optimizeDeps pre-bundle axios/laravel-echo/pusher-js/chart.js
- nginx: proxy_connect_timeout + proxy_buffering off for Vite asset location
- nimuli-app.blade.php: swap @livewire(wire-elements-modal) before @livewireScripts (rule #7)
- PlanLimitsServiceTest: add RefreshDatabase (SQLite unit test had no migrations)
- verify-modals.sh: fix grep pattern, add Chart.js/optimizeDeps checks, set -e safe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:36:40 +02:00
boban 6c81dd1d48 fix: eliminate FOUC, Echo, modal duplicate, icons, test DB isolation
- FIX 1: Remove duplicate modal container (keep @livewire directive, drop <x-modal-container>)
- FIX 2: Add Laravel Echo + Pusher to bootstrap.js with Reverb config
- FIX 3: Cookie-based theme — exclude nimuli_theme from encryption, write cookie in Alpine store, read server-side in layout
- FIX 4: Generate missing PNG favicons (16/32/180/192/512px) via GD
- FIX 5: Create scripts/verify-modals.sh (27 checks, all green)
- BUG: Docker OS env DB_CONNECTION=mysql was overriding phpunit.xml sqlite config, causing RefreshDatabase to migrate:fresh production MySQL — fixed by forcing DB vars in tests/bootstrap.php alongside existing APP_ENV fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:23:44 +02:00
boban ba67f67d60 fix(ui+seed): FOUC eliminated + persistent dev-user
Seed:
- DevUserSeeder: idempotent — creates workspace + WorkspaceMember + sets
  default_workspace_id. Auto-recreates everything on migrate:fresh --seed.
- DatabaseSeeder: PlanSeeder first (nexxo needs a plan), then DevUserSeeder
  always, DemoDataSeeder only in local/dev env.
- DevUserSeederTest: create + idempotent + in-DatabaseSeeder + order check.
- Removed WithoutModelEvents trait from DatabaseSeeder (was suppressing
  model observers needed for workspace creation).

UI (nimuli-app.blade.php):
- Inline JS theme-init before first paint: reads localStorage nimuli_theme,
  falls back to server-side user preference. Eliminates theme flash on reload.
- Critical CSS in <head>: CSS vars, body opacity 0 → 1 fade (120ms),
  skeleton sidebar + topbar visible during JS load.
- app-spinner shown until app-ready class added by markReady().
- Skeleton + spinner hidden via body.app-ready CSS selector.
- livewire:navigated event re-triggers markReady() for SPA navigation.
- x-cloak on mobile sidebar overlay to prevent pre-Alpine flash.

Bash aliases added to ~/.bashrc:
- seed-dev  → db:seed --class=DevUserSeeder
- migrate-fresh → migrate:fresh --seed
- reset-db  → migrate:fresh --seed (with confirmation message)

Tests: 109/109 passing. Smoke: all routes . DB sanity .

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 12:53:29 +02:00
boban 5748dd3443 style: apply Pint formatting to modal files and tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 12:42:16 +02:00
boban fa9985cc33 feat: modal infrastructure + 11 modals (links, team, settings, workspace)
wire-elements/modal v3 integration:
- Replace custom Alpine modal-container with <livewire:livewire-ui-modal />
- Customize vendor modal view with design tokens (bg-s1, dark overlay, smooth transitions)
- All modals extend ModalComponent, use $this->closeModal()

Modals built (PHP + Blade view + tests):
- ConfirmAction — reusable danger confirm with custom event dispatch
- CreateLink, EditLink, DeleteLink — full CRUD with workspace isolation
- InviteMember, EditMemberRole, RemoveMember — team management
- CreateApiToken, RevokeApiToken — Sanctum token lifecycle
- CreateWebhook, DeleteWebhook — webhook CRUD
- CreateWorkspace (uses CreateWorkspaceAction), DeleteWorkspace (confirmation input)

Fixes:
- Remove SoftDeletes from Webhook model (table has no deleted_at column)
- Auto-generate webhook secret when user leaves field empty
- Use correct role enum: owner/admin/editor/viewer (not member)
- Protected Livewire model properties replaced with re-query pattern
- Sidebar "New Workspace" button now dispatches openModal

Tests: 105/105 passing, 211 assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 12:41:14 +02:00
boban 00673f3119 fix: Alpine double-load, FOUC, purple accent, logo/favicon
- Remove alpinejs devDep; Livewire v3 owns Alpine lifecycle
- Register sidebar + theme stores via alpine:init event
- @persist sidebar across wire:navigate to prevent re-render
- Inline critical CSS for FOUC prevention on initial load
- Purple #7c3aed accent + pink #ec4899 gradient in both themes
- Light-mode border visibility fix (solid color, not opacity)
- bg-accent-gradient / text-accent-gradient / border-accent-gradient utilities
- Logo mark SVG + full wordmark variants (dark/light/neutral)
- favicon.svg, favicon PNGs, apple-touch-icon, site.webmanifest
- Sidebar uses logo-mark.svg + text-accent-gradient wordmark

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 12:26:14 +02:00
boban d0365fd34f fix: tests green + logout route + smoke test
- Add POST /logout route (was missing from auth.php)
- Add workspaceId property to Analytics component (fixes #[On] binding)
- tests/bootstrap.php: force APP_ENV=testing before Docker OS env takes over
- phpunit.xml: use tests/bootstrap.php + force=true on APP_ENV
- Update AuthenticationTest, ProfileTest, LoginRateLimitTest to match current app structure
- Add tests/Feature/Livewire/WorkspacePagesTest.php: 11 workspace route tests
- scripts/smoke-test.sh: HTTP smoke test for all routes
- Result: 75/75 tests passing, all smoke routes 302→login

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:57:32 +02:00
boban ef0105098b fix: workspace membership + middleware owner fallback
- DemoDataSeeder now creates workspace_members record for owner + sets default_workspace_id
- ResolveWorkspace middleware accepts workspace owners even without explicit member record
- Applied membership fix for nexxo@nimuli.com via PHP (existing installs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:17:54 +02:00
boban 52d52824bc feat: add profile, team, settings subpages
- Profile pages: PersonalInfo, Preferences, Security
- Team: Members list with role management stub
- Settings: ApiTokens (Sanctum, one-time display), Webhooks stub
- Settings/Index: workspace name/slug update
- Sidebar: Team nav item + SVG icon
- Topbar: profile link → profile.personal
- Routes: profile.personal, profile.preferences, profile.security, w.team.index, w.settings.api-tokens, w.settings.webhooks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:07:31 +02:00
boban b86bdd772a fix: Tailwind v4 CSS-Loading unter Vite dev mode
Problem: Vite 7 serviert CSS-Entry-Points als JavaScript-Module
(Content-Type: text/javascript). Browser ignoriert JS-Response bei
<link rel="stylesheet"> — CSS wurde nie angewandt.

Lösung:
- CSS aus @vite()-Aufrufen entfernt (kein <link>-Tag mehr)
- CSS aus vite.config.js input entfernt
- import '../css/app.css' zu resources/js/app.js hinzugefügt
- Vite injiziert CSS via JS-Modul → <style>-Tags im Browser

In Prod: CSS wird aus JS-Import extrahiert, Manifest linkt sie korrekt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:00:17 +02:00
boban 30989523cd feat: vollständiges Nimuli-Frontend Phase 1
Layouts:
- nimuli-app: Sidebar + Topbar + Mobile-Drawer (Alpine.js stores)
- guest: Nimuli-Brand, bg-bg, Inter/JetBrains Mono
- sidebar: Workspace-Switcher, Nav mit SVG-Icons, Plan-Usage-Box
- topbar: Theme-Toggle, Notifications-Bell, User-Dropdown

Auth (Breeze-Restyle mit Design-Tokens):
- Login: "Sign in" + dark-theme-native (text-t1, bg-s2, text-blue)
- Register: "Create account"
- Forgot Password: restyled
- Alle Input-Komponenten (text-input, label, button, errors) auf Tokens

Dashboard:
- Hero-Metrics (Today/7d/30d/Total Links)
- 30-Tage Chart.js Line-Chart
- Top-5-Links-Tabelle
- Quick-Actions

Links:
- Tabelle: nimu.li/-Prefix, font-mono slug, Tag-Badges, Status-Badges
- Create-Link Modal (Alpine.js + Livewire, auto-slug)
- Modal-Container-System

Stub-Pages (alle mit nimuli-app layout):
- QR Codes: Empty-State + CTA
- Link-in-Bio: Empty-State + CTA
- Analytics: Metrics + Top-Countries + By-Device
- Domains: Empty-State + CTA
- Settings: Workspace-Name + Danger-Zone
- Billing: Current-Plan + 3-Tier-Pricing-Cards

Demo-Daten:
- DemoDataSeeder: 20 Links + 957 Clicks (30 Tage)
- Workspace "Demo Agency" für nexxo@nimuli.com

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:52:52 +02:00
boban 5ac6a198b4 fix: tailwind v4 plugin order + opcache revalidate_freq=0 + npm scripts
- vite.config.js: tailwindcss() vor laravel() Plugin-Reihenfolge (Tailwind v4)
- vite.config.js: refresh-Globs für Livewire + Blade + Lang + CSS
- docker/app/php.ini: opcache.revalidate_freq=0, jit=tracing, memory 512M
- docker/app/Dockerfile: COPY php.ini zu zz-app.ini
- package.json: build:watch + preview + lint scripts ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:26:15 +02:00
boban e551fb62ac fix: Vite-HMR-Websocket via /vite-hmr path + DevUserSeeder
- Add hmr.path '/vite-hmr' to vite.config.js for WSS routing
- Improve nginx /vite-hmr location with full proxy headers
- Add DevUserSeeder (nexxo@nimuli.com) for local dev login
- Register DevUserSeeder in DatabaseSeeder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 09:45:40 +02:00
boban c0f9ee4427 fix: trust proxies + tailwind v4 + debugbar off
- bootstrap/app.php: trustProxies(at: '*') — nginx/NPM forward HTTPS,
  app must trust X-Forwarded-Proto to generate correct https:// URLs
- package.json: tailwindcss v3 → v4, removed @tailwindcss/forms (v3-only)
- .env: DEBUGBAR_ENABLED=false, VITE_DEV_SERVER_URL corrected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 09:08:03 +02:00
boban fc5ab16f02 fix(routes): move auth.php require before slug catch-all
The /{slug} redirect route matched /login, /register etc. because
auth.php was required after it. Auth routes must be registered first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:57:12 +02:00
boban 2924ec4bf6 chore(vite): remove PostCSS, route HMR through app.nimuli.com
Tailwind v4 uses its own Vite plugin — PostCSS/autoprefixer not needed.
Nginx now proxies Vite dev paths to host:5173 so HMR works without a
separate vite.nimuli.com proxy host.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:47:25 +02:00
boban ea9274be49 fix(quality): PHPStan level 6 compliance, Pint formatting
- Add phpstan.neon with level 6 config and larastan extension
- Fix all 92 PHPStan errors: Eloquent relation generics, iterable
  value types, HasFactory generic annotations, missing return types
- Fix logic bugs: always-true Safari/Chrome comparison, nullsafe on
  non-nullable Carbon, unused $renderer suppressed with ignore rule
- Implement MustVerifyEmail on User model (was commented out in Breeze
  scaffold) to satisfy VerifyEmailController type contract
- Remove broken QrCode::newFactory() referencing non-existent factory
- Add @var docblocks for app('current_workspace') mixed resolution
- Apply Pint formatting: 53 style issues fixed across 168 files
- Test suite unchanged: 9 pre-existing assertSeeLivewire failures,
  55 tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:28:10 +02:00
boban 96ec518200 feat(domains): Domain model, DNS TXT verification action, pending verifier job 2026-05-16 08:14:33 +02:00
boban a394e0ff18 fix(bio): add BioPageFactory, newFactory() override on BioPage 2026-05-16 08:12:54 +02:00
boban e9623b9b31 feat(bio): Link-in-Bio pages with translatable fields, public render 2026-05-16 08:10:59 +02:00
boban d7ca84dd2e feat(auth): TOTP 2FA enable/disable with recovery codes 2026-05-16 08:08:31 +02:00
boban c193e482ac feat(ai): AiService (Anthropic adapter), GenerateSlug action
Implement AI domain with Anthropic API integration for slug generation.
Adds GenerateSlug action for URL-to-slug transformation and GenerateAbVariants
for A/B test variants. Includes test coverage for GenerateSlug action with
mocked AiService.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:06:11 +02:00
boban 79da7502cd feat(analytics): live dashboard with Reverb WebSocket, click event broadcasting
Overwrites the Analytics Index stub with a full Livewire component that
loads today's click count from the clicks table (workspace_id is a direct
column) and listens for live updates via Echo/Reverb using the
#[On('echo:workspace.{workspaceId}.analytics,click.recorded')] attribute.
Creates the corresponding blade view with a today-counter card and a
live recent-clicks feed (last 10 entries).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:04:08 +02:00
boban 61718fa867 feat(ui): links index page with search, filter, delete; workspace route group
Add Links Livewire page (Index component + blade view) with live search,
status filter, paginated table, and delete action using DeleteLink for
cache invalidation. Register workspace-scoped route group under
w/{workspace} with ResolveWorkspace middleware. Add stub Livewire pages
for QrCodes, Bio, Analytics, Domains, Settings, and Billing with a shared
coming-soon view.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 07:59:42 +02:00
boban 3929c48ea1 feat(ui): app layout with sidebar and topbar
Implement Nimuli-specific authenticated layout with:
- Sidebar navigation with 7 main menu items (dashboard, links, qr codes, link-in-bio, analytics, domains, settings)
- Topbar header with user email and logout button
- Safe route checking using Route::has() for undefined routes
- Workspace display in sidebar when available
- Livewire AppLayout base class for future Livewire components
2026-05-16 07:54:31 +02:00
boban a3930b5d25 test(security): assert Permissions-Policy header 2026-05-16 07:52:20 +02:00
boban 3f6b281c62 feat(security): add HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy headers 2026-05-16 07:51:33 +02:00
boban e33a1fafde fix(webhooks): HMAC integrity fix (send raw body), IPv6 SSRF protection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 07:49:25 +02:00
boban c8a6325a77 feat(webhooks): outgoing webhooks with HMAC signature, SSRF protection, retry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 07:46:57 +02:00
boban 69ca1ab058 fix(link): evict cache after delete, not before 2026-05-16 07:44:21 +02:00
boban 3568e67ca0 fix(api): use DeleteLink action for cache invalidation, fix validation, add delete tests 2026-05-16 07:42:56 +02:00
boban 1375519cdb feat(api): REST API v1 — links CRUD with Sanctum auth
Adds /api/v1/workspaces/{ulid}/links endpoints (index, store, destroy)
protected by Sanctum token auth; adds HasApiTokens to User model and
AuthorizesRequests to base Controller; wires routes/api.php into bootstrap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 07:38:45 +02:00
boban 8f1398380f fix(billing): pm_type/pm_last_four migration, narrow CSRF exclusion, EU paper size
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:46:54 +02:00
boban 06dbb1dffb feat(billing): Cashier Billable on Workspace, Stripe webhook route
- Add Laravel\Cashier\Billable trait to Workspace model
- Register Workspace as Cashier customer model in AppServiceProvider
- Publish cashier config with EUR currency and de_DE locale defaults
- Add CSRF exclusion for stripe/* in bootstrap/app.php
- Add StripeWebhookTest verifying endpoint exists and rejects bad signatures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:35:11 +02:00
boban 5c5ddbbe83 fix(i18n): remove stale redis del pattern, drop dead singleton 2026-05-16 01:29:45 +02:00
boban b121520ed6 fix(i18n): include namespace in cache key/query, clean up singleton, test isolation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:27:10 +02:00
boban ce9e53d206 feat(i18n): database-backed translation loader with Redis cache
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:20:48 +02:00
boban 15f4b2c0d8 fix(qr): wifi/vcard injection protection, remove dead code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:16:47 +02:00
boban d3b777d8e8 feat(qr): QR code renderer (SVG + PNG), model, generate action
Implements QrRenderer using chillerlan/php-qrcode v6 API (outputInterface
instead of deprecated outputType constants), QrCode Eloquent model with
ULID auto-generation and soft deletes, and GenerateQrCode action.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:13:41 +02:00
boban dd89ee395c feat(billing): plan limits service, plan factory, seeder (free/pro/business/agency) 2026-05-16 01:06:01 +02:00
boban f757f43926 feat(analytics): hourly/daily rollups, 90-day raw click prune, schedule 2026-05-16 01:03:49 +02:00
boban 7d47222591 fix(routing): defensive fallback, between bounds check, condition key guard 2026-05-16 01:02:37 +02:00
boban e06db03320 feat(routing): smart routing engine with condition evaluator (device, country, UTM) 2026-05-16 01:01:47 +02:00