Commit Graph

95 Commits (2412abcde771a9715843c2740f336f780490eafd)

Author SHA1 Message Date
boban 2412abcde7 fix(ui): blur label during loading so only spinner visible
wire:loading.class adds opacity-40 + blur-sm to label span,
transition-[filter,opacity] smooths the blur in/out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:17:10 +02:00
boban be8fdc8fe4 fix(ui): center spinner via top-1/2 left-1/2 translate instead of inset-0 flex
inset-0 flex relies on display:flex being restored after Livewire removes
display:none — display type ambiguous in that moment. Explicit 50%/50%
translate centering is display-independent and always correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:15:52 +02:00
boban 695727e321 fix(ui): spinner full opacity — remove disabled:opacity-60 from button
CSS opacity on parent compounds to all children, spinner can't escape it.
disabled:opacity-60 → disabled:opacity-100 (keeps button at full color
while loading, cursor:wait signals busy state). Text fades via
wire:loading.class="opacity-50" on label span only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:13:55 +02:00
boban 36b335819a fix(ui): button spinner overlay — text fades to 50%, no layout shift
Keep label in DOM with opacity-50 during loading, spinner sits as
absolute inset-0 overlay. Button width stays constant, no jump.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:11:56 +02:00
boban add7957430 fix(ui): proper 3/4-arc spinner centered in button, honest AI prompt
- Button: wire:loading.remove on label (DOM removal, no z-index fight)
  spinner span is normal flex child — button's own flex centers it
  SVG: track circle opacity 25% + 3/4 arc via stroke-dasharray 42.4/56.6,
  stroke-linecap round, rotated -90deg so arc starts at top
- AI prompt: explicitly flags sparse data (<3 links or <10 clicks),
  requires every claim reference a specific slug, forbids generic padding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:05:31 +02:00
boban 8cd46398c0 fix(ai): language-aware prompt, better insights, larger spinner
- Prompt respects user locale (de/en/fr/es/it/pt) — responds in user's language
- Rewrite prompt: domain pattern analysis, low-click diagnosis, no generic advice
- Include total clicks + link count for context
- max_tokens 512 → 800 for richer output
- Button spinner: h-5 w-5 (was h-4 w-4), bg-inherit to cover label text cleanly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 14:44:18 +02:00
boban d9f665f749 fix(ai): switch to OpenAI, add error handling in Insights
- AiService: use OpenAI chat completions API (gpt-4o-mini) instead of Anthropic
- Add services.openai.api_key config entry
- Insights component: check for API key, handle empty links, catch HTTP errors
- View: show error banner, hide placeholder when error shown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 14:40:11 +02:00
boban 3eada67b9a fix(sidebar): remove collapse, fix mobile overlay and blur effect
- Remove desktop collapse feature — sidebar always open at 264px
- Simplify Alpine sidebar store to mobileOpen only
- Fix mobile overlay: add x-data to #nim-app so Alpine processes x-show/x-transition
- Mobile overlay uses direct backdrop-filter: blur(4px) bypassing Tailwind v4 CSS variable chain
- Fix close/workspace-trigger overlap: restructure header as flex row with close as sibling
- Clicking overlay (z-30) closes sidebar; sidebar stays at z-40

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 14:34:33 +02:00
boban e36ecb1dba fix(BUG-2): sidebar pure Tailwind, revert logo, fix mobile translate
- Replace all inline style= with Tailwind classes (except progress bar width)
- Remove onmouseenter/onmouseleave JS hover handlers → hover: Tailwind classes
- Revert ws-logo to /brand/logo-mark.svg (user requirement)
- Fix mobile: 'transform: translateX(0)' → 'translate: 0' (Tailwind v4 uses
  CSS translate property, not transform, for -translate-x-full)
- Move sidebar transition to .nim-sidebar critical CSS rule
- Switch --nim-* CSS vars to --color-* tokens already in app.css
- nav-item hover uses :not(.nim-nav-active) to avoid conflict with active state
- Workspace/Account section labels, icon partial, AI badge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 13:47:50 +02:00
boban d2d43e1097 feat(BUG-2): redesign sidebar from template — Geist font, section nav, CSS-driven collapse
Implement user-provided template design:
- Split nav into Workspace / Account sections with labels
- New CSS design tokens (--nim-*) for sidebar without breaking app vars
- Active indicator: left glow bar via ::before, CSS class nim-nav-active
- Collapsed tooltip via CSS ::after content: attr(data-label)
- Geist font for sidebar via Google Fonts
- 264px expanded / 72px collapsed sidebar widths
- All collapse state driven by html[data-sidebar-collapsed] critical CSS
  (no Alpine reactivity needed, works across @persist boundary)
- Mobile: always full expanded sidebar regardless of collapse state
- Icon partial at partials/sidebar-icon with 24px viewBox icons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 13:40:16 +02:00
boban b241df93a8 fix(BUG-2): scope sidebar collapse CSS to desktop media query
nim-sb-expanded/nim-sb-collapsed/sidebar-nav-link collapse rules were
applied globally. On mobile, switching from a desktop-collapsed state
hid all nav labels because data-sidebar-collapsed was still set.
Wrap the rules in @media (min-width: 768px) so mobile always shows
the full expanded sidebar regardless of collapse state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 13:14:47 +02:00
boban 3c24c19b19 fix: remove @persist from CSS comment to prevent Blade directive parse error
Blade processes @-directives everywhere in templates including inside <style>
blocks and HTML comments. The word "@persist" in a CSS comment was being
interpreted as Livewire's @persist directive, compiling to e() with no argument.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 13:09:37 +02:00
boban e71bd33e63 fix(BUG-2): drive sidebar collapse/expand via CSS attribute, not Alpine reactivity
x-show and :class bindings inside @persist blocks do not re-evaluate when
Alpine store changes. Replace all collapse-dependent bindings with CSS utility
classes (nim-sb-expanded, nim-sb-collapsed, sidebar-nav-link) driven by
html[data-sidebar-collapsed] in the critical inline style block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 13:05:33 +02:00
boban 3897d54ba3 fix(bug-2): sidebar-aside base width outside media query + auto-close on mobile
- Move .sidebar-aside{width:15rem} outside @media min-width:768px so mobile
  gets explicit width — without it -translate-x-full translates 0px and
  sidebar content bleeds visible (Collapse text, logout icon)
- Resize handler: auto-close mobileOpen when crossing to mobile viewport

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:56:41 +02:00
boban 0a08d0f268 fix(bug-2): sidebar width via CSS attribute — bypasses @persist Alpine reactivity issue
@persist disconnects Alpine :class bindings from store reactivity on re-render.
Solution: drive sidebar width via html[data-sidebar-collapsed] CSS attribute
(same mechanism as skeleton) instead of Alpine :class.

- _syncDataset() now uses isCollapsed (respects mobile: never narrow on <768px)
- Blocking script checks window.innerWidth >= 768 before setting attribute
- Resize handler calls _syncDataset() to update attribute on viewport change
- aside gets sidebar-aside class; CSS sets 15rem/4rem at md+ via attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:52:57 +02:00
boban fe257ef75b fix(bug-2): isCollapsed as plain reactive property — getters unreliable in Alpine stores 2026-05-17 12:50:11 +02:00
boban d3ad4b4740 fix(bug-2): isCollapsed getter — sidebar auto-expands on mobile viewport
Add isMobile reactive property + resize listener to sidebar store.
isCollapsed getter returns collapsed && !isMobile, so:
- Desktop: collapse works as normal
- Mobile: isCollapsed always false → sidebar always shows expanded state
  regardless of desktop-set collapsed value

Replace all visual $store.sidebar.collapsed → isCollapsed in sidebar + layout.
toggle() / _syncDataset() remain on raw `collapsed` (persisted to localStorage).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:47:38 +02:00
boban e0624b5d7c fix(bug-2): collapse toggle hidden via max-md:hidden not hidden md:block 2026-05-17 12:43:58 +02:00
boban fbb57eec81 fix(bug-2): mobile sidebar close button + desktop-only collapse toggle
- Add mobile-only X button in sidebar header to call closeMobile()
  (splits workspace-switcher <button> into flex row with close sibling)
- Hide collapse toggle with hidden md:block — mobile only needs open/close,
  desktop uses collapse to go narrow/wide

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 19:07:02 +02:00
boban 90bf5a0c53 fix(bug-2): mobile hamburger — use CSS translate property to override Tailwind v4
Tailwind v4 compiles -translate-x-full to `translate: -100%` (CSS translate
shorthand), not `transform`. The old :style set `transform: translateX(0)` —
a different property — so the sidebar stayed hidden regardless of mobileOpen.

Fix: use `translate: 0` in inline style so it overrides the same property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 19:01:26 +02:00
boban b077c8652b fix(bug-1): adjust nim-content margins to pixel-exact values (210px / 55px) 2026-05-16 18:58:55 +02:00
boban 326caa51ce fix(bug-1): eliminate content-area overlap on hard-reload with collapsed sidebar
Add .nim-content class to main content div and pre-Alpine CSS rules:
- @media min-width:768px .nim-content { margin-left:240px }
- html[data-sidebar-collapsed] .nim-content { margin-left:64px }

The blocking script (FIX-13) already sets data-sidebar-collapsed before first
paint; the new CSS applies the matching margin before Alpine initializes.
Also fix skel-topbar left position for collapsed state (was always 240px).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:55:26 +02:00
boban 436d80a097 fix(FIX-13): 8 bugs — sidebar flash, mobile hamburger, AI insights, button overlay, QR modal collapse, dashboard always-active, bio themes
- BUG 1: blocking script in <head> reads localStorage before first paint; CSS rule html[data-sidebar-collapsed] .skel-sidebar{width:64px} eliminates width flash
- BUG 2: sidebar store gains mobileOpen/toggleMobile()/closeMobile(); hamburger calls toggleMobile(); backdrop and sidebar :style use mobileOpen; SPA navigated handler calls closeMobile()
- BUG 3: text-purple → text-accent in AI Insights view (text-purple is undefined)
- BUG 4: ui/button.blade.php — spinner is now absolute inset-0 overlay; content fades to opacity-40 via wire:loading.class instead of hiding
- BUG 5: insights generate button and QR modal save/cancel migrated to x-ui.button component
- BUG 6: QR modal Styling/Frame/Advanced sections replaced from <details> (destroyed by Livewire re-render) to Alpine x-data{open:false} divs with x-show — state survives wire:model.live updates
- BUG 7: updateActiveNav() now checks el.dataset.navExact; Dashboard <a> gets data-nav-exact="1" so it only activates on exact pathname match
- BUG 8: bio_pages.theme JSON applied in public show.blade.php via inline CSS vars (4 presets: dark/light/purple/blue); EditBioPage gains theme property with preset selector in modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:28:27 +02:00
boban e211770312 fix(FIX-12): 10 bugs — AI workspace context, Alpine sidebar/theme, analytics clearFilters, link badge layout, slug suggest
BUG 1: Insights/Anomalies called require_workspace() in action methods — add mount() cache
BUG 2: Analytics clear-filter used multiple $set() calls — replace with clearFilters() action
BUG 3: Static md:ml-60 on main div blocked Alpine :class md:ml-16 override — remove static
BUG 4: updateActiveNav missed icon <span> colors on @persist sidebar — update icon span too
BUG 5: Topbar <header> lacked x-data — Alpine x-show on theme SVGs not reactive
BUG 6: --color-t2 #8888a0 too dark in dark mode — raise to #b8b8d0
BUG 7: UtmBuilder::createLink() used current_workspace() in AJAX — pass workspaceUlid via mount
BUG 8: Clicks badge in separate column, not rightmost — merge into actions cell
BUG 9: Sidebar nav icons w-4 h-4 — increase to w-5 h-5, justify-center when collapsed
BUG 10: suggestSlugs() no try-catch; text-purple undefined — add guard + use text-accent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:02:23 +02:00
boban 28fbcba227 fix: bugs 1,4,5,7,8,9 — sidebar icon, theme button, copy-link, UTM computed, workspace context, theme sync
Bug 1: sidebar toggle — single rotating SVG → two x-show SVGs (chevron-left / chevron-right)
Bug 4: theme button — template x-if → svg x-show x-cloak; apply() now resolves 'system'
  to dark/light before setting data-theme (was setting data-theme="system" → CSS ignored it)
Bug 5: x-ui.copy-button added to links, bio, qr-codes rows and api-tokens new-token display
Bug 7: UtmBuilder::getFullUrlProperty() (Livewire v2 magic) → #[Computed] fullUrl()
  (Livewire v3 dropped getXProperty magic — $fullUrl was undefined in blade)
Bug 8: require_workspace() moved from render() to mount() in Overview, Links/Index, Settings/Index
  (Livewire update requests hit /livewire/update — ResolveWorkspace middleware never ran → threw)
Bug 9: Preferences::save() dispatches theme-changed-server → layout listener calls
  Alpine.store('theme').apply(theme) so DOM updates immediately without reload

Bugs 2, 3, 6 already correct in codebase (layout had transition/ml-60 binding,
mobile sidebar used $store.sidebar.open, routes used ULID via model route key).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 17:30:44 +02:00
boban 43ff6d6407 fix(FIX-10): 6 bugs — route ULID, QR modal, bio unique, nav active, copy btn, analytics context
- Workspace.getRouteKeyName()='ulid' so route() generates ULID URLs not ID
- QR modal 4xl→5xl; import QRCode in @script→window.QRCode (no ESM in @script)
- Bio slug unique rule scoped to (workspace_id, domain_id) not global
- updateActiveNav uses URL.pathname not raw getAttribute('href') (absolute URL fix)
- Add resources/views/components/ui/copy-button.blade.php Alpine component
- Analytics Index.render/baseQuery use $workspaceId not require_workspace() — prevents RuntimeException on Livewire action calls without middleware

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 17:01:16 +02:00
boban 2d231fe6b8 fix: bio public /b/{slug} route + QR types + code cleanup
- Add GET /b/{slug} route alias (nimu.li shorthand for bio pages)
- QR wizard: extend type enum to include email, phone, sms, location
- Modals: replace FQCN references with imported class aliases (Pint clean)
- Analytics: add missing Carbon/Builder imports, improve return types
- Octane config: add ResetCurrentWorkspace import
- scripts/crawl-all.sh: curl+PHPUnit based crawler (replaces Playwright)

All 158 tests pass, PHPStan clean, Pint clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:41:05 +02:00
boban 5d90c375f7 fix(sidebar+scripts): correct CSS specificity, externalize test credentials
- sidebar: revert translate to static class (-translate-x-full md:translate-x-0)
  + inline :style for mobile-open (highest specificity, beats Tailwind classes)
  Keeps transition-[width,transform] for smooth mobile slide animation
- crawl-all-routes.mjs, console-check.mjs: read TEST_EMAIL / TEST_PASSWORD
  from process.env; DB creds read from .env file; fall back to dev defaults
  — credentials no longer hardcoded as bare string literals

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:50:07 +02:00
boban 248c5100d1 fix(sidebar+types): mobile slide animation, close-on-navigate, PHPStan clean
- sidebar: transition-[width,transform] so mobile open/close animates
- sidebar: pure :class binding replaces mixed class+:style for translate
- layout: livewire:navigated closes mobile sidebar via Alpine store
- tests: RouteSmokeTest covers all 17 workspace/profile/AI routes
- scripts: crawl-all-routes.mjs + console-check.mjs (Playwright)
- phpstan: add @property-read, @var, @return generics across 15 modal/component files → 0 errors
- pint: auto-fix style violations in 9 files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:47:17 +02:00
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