Skip to content

v2: teal redesign, owned QR engine, MCP server, scheduled reporting - #62

Merged
DGINXREAL merged 135 commits into
mainfrom
v2
Aug 9, 2026
Merged

v2: teal redesign, owned QR engine, MCP server, scheduled reporting#62
DGINXREAL merged 135 commits into
mainfrom
v2

Conversation

@DGINXREAL

Copy link
Copy Markdown
Member

Overview

v2 brings a full UI refresh plus three substantial new capabilities. All work landed via subagent-driven development with per-task and whole-branch review.

Highlights

Design system + localization

  • Migrated the entire app from the raw Tailwind palette to a teal design-token system + shared @/Components/ui kit (Button/LinkButton, Input/Select/Checkbox/Field, PageHeader, TableCard, Flash, Pagination, …).
  • Full de / en / fr / nl localization across every area (shell, dashboard, index/CRUD pages, QR editor, statistics, admin, analytics, auth/profile/team).
  • Accessibility pass (Field aria-invalid/aria-describedby, Button aria-busy, focus rings).

Owned QR styling engine

  • Replaced the qr-code-styling dependency with an owned SVG renderer (resources/js/lib/qr/ — matrix → shapes → render → export + scannability), using qrcode-generator for the matrix.
  • Parametric rounding (neighbour-aware) for modules & eyes, gradients, independent eye colors, "Scan me" frames, a live scannability meter, project brand templates, and PNG/SVG/PDF export.
  • One-time migration of stored QR styles to the v2 schema.

MCP server + API tokens

  • Built-in laravel/mcp server at POST /mcp/marketix (Sanctum personal-access-token auth + throttle).
  • 7 tenant-scoped tools (read: projects/links/domains/qr-codes/link-stats; create: link/qr-code via shared CreateLink/CreateQrCode actions).
  • Token management UI in Profile (create/copy-once/revoke) + connection help.

Scheduled reporting

  • Per-project recurring email reports (project-summary / single-link / site-analytics) with optional CSV/PDF attachments and a "send now" test.
  • Pluggable ReportType registry reusing the existing ReportDataService + spatie/laravel-pdf; hourly reports:dispatch-due scheduler; pure NextRunCalculator/PeriodResolver.

Verification

  • ddev npm run build green (tsc + vite).
  • ddev php artisan test571 passed (2142 assertions).

Notes / follow-ups

  • Pre-existing (not introduced here): Project::forceDelete() FK violation via LogsActivity — ticket before any hard-delete feature.
  • Scheduled-report PDF path relies on Browsershot/headless Chrome and is not covered by an automated test.

🤖 Generated with Claude Code

DGINXREAL and others added 30 commits August 4, 2026 19:24
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…av, consent contract, retention field, config cache, cookie Secure)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the AppLayout inner-content padding convention used by the other pages
(e.g. Pixels/Index); the new pages rendered flush against the sidebar/top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The snippet has no own-banner branch yet, so own_banner must not be
selectable. Add ConsentMode::selectableCases/Options/Values (excluding
OwnBanner), use them in the Site create/edit forms and SiteRequest
validation. The enum keeps the case for v1.1. Adds tests asserting the
option is absent from the form and rejected by validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CSRF)

The snippet used navigator.sendBeacon, which always sends in credentials
'include' mode; the CORS spec forbids a wildcard Access-Control-Allow-Origin
for credentialed requests, so cross-origin beacons were blocked at preflight.

- mx.js: send via fetch with credentials 'omit' + Content-Type text/plain
  (a CORS simple request → no preflight, and '*' is valid for any domain).
- Controller: decode the raw text/plain JSON body before validating.
- CSRF: exempt 'a/*' — customer sites cannot carry this app's token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cache-busting a snippet embedded on third-party sites can't use hashed
URLs or ?v= — the embed is fixed. Follow the vendor pattern (GA/Plausible/
Fathom): a stable URL whose updates propagate via cache headers, not URL
changes. Move mx.js out of public/ and serve it from GET /mx.js with
Cache-Control: public, max-age=3600, stale-while-revalidate=86400 and an
ETag, so expiry is a cheap 304 revalidation and edits roll out within the
hour without touching any customer's embed code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h/per-view storage)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DGINXREAL and others added 25 commits August 6, 2026 22:10
…vice metric

Replace the hand-rolled (browser, os) DB::raw aggregate in GetLinkStatsTool
with separate by_browser/by_os breakdowns via StatisticsAggregator::breakdown(),
matching the single-column breakdowns already used by StatisticsController and
UrlController (topBrowsers/topOs). No combined-tuple metric exists elsewhere
in the app, so by_device is dropped in favor of by_browser + by_os.
Field wasn't receiving errors.name from useForm, so a server-side
validation failure round-tripped silently with no message and no
aria-invalid/aria-describedby linkage.
…/regenerate actions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tokens section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add lang/{en,de,fr,nl}/reports.php catalogs (index/form/types/frequencies/email/report
sub-arrays) for the upcoming scheduled reporting feature, and a "reports" key in the
common.php nav group used by the sidebar. Pure i18n — no code changes.
Adds the scheduled_reports table (ULID pk, project/creator FKs,
frequency/period/format/recipient columns, next_run_at index),
the ScheduledReport model with array/bool/datetime casts and
project()/creator() relations, config/reports.php send_hour
setting, and Project::scheduledReports().
Deterministic scheduling-math for scheduled reporting (Task 3 of the plan):
both classes take the reference instant as a parameter instead of calling
now(), so results are fully reproducible in tests.

- PeriodResolver::resolve() maps last_7_days/last_30_days/last_90_days to a
  rolling [now-N, now] window, and previous_month to the full prior
  calendar month; throws InvalidArgumentException on unknown periods.
- NextRunCalculator::next() computes the next send instant strictly after a
  given time, in config('app.timezone'), for daily/weekly/monthly
  frequencies, clamping day_of_month to 28 to avoid month-length issues.
$now->subMonth() from a day-29/30/31 date uses Carbon's overflowing month
arithmetic and can land back in the *same* month (e.g. 2026-03-31 minus one
month -> 2026-03-03), so previous_month resolved to the current, still-
accumulating month instead of the completed prior one on those days.

Fix: subtract the month from startOfMonth() instead, where subtracting a
month is always safe regardless of day-of-month. Adds regression tests for
day-31 in both a 31-day month (May -> April) and around the Feb boundary
(March -> February).
Adds the report-type abstraction for scheduled reporting: ReportData
(struct), ReportType (contract), ReportTypeRegistry (container-resolved
singleton, throws InvalidArgumentException on unknown key), and the 3
implementations — ProjectSummaryReport, LinkReport, SiteAnalyticsReport —
each gathering data via the existing StatisticsAggregator /
AnalyticsAggregator / GoalAggregator.

Renames the pre-existing, unrelated on-demand PDF report struct
App\Reports\ReportData (used by ReportController's /reports/download
routes) to App\Reports\LegacyReportData to free up the ReportData name
for the new scheduled-reporting struct, per the plan's shared reference.
No behavior change to the legacy download feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t parallel stack)

Reverts the parallel report-data stack from 07f539d: deletes the new
title/periodLabel/kpis/breakdowns/series ReportData struct, restores
LegacyReportData back to App\Reports\ReportData (original
scope/title/subtitle/rangeLabel/generatedAt/totalClicks/uniqueClicks/
timeSeries/breakdowns/topLinks/recentClicks shape + toArray()), and
re-points ReportDataService + the 2 template/branding tests at it — the
on-demand /reports/download PDF feature is byte-for-byte back to its
pre-07f539d behavior (ReportTemplateRenderTest, Admin/BrandingTemplatesTest
pass unchanged).

Rebuilds ReportType/ReportTypeRegistry per the revised interface
(validateSubject/subjectLabel/title/viewData/pdfView/emailView/csv) as thin
adapters over the existing subsystem instead of a second data-gathering
layer: ProjectSummaryReport and LinkReport call
ReportDataService::forProject()/forUrl()->toArray() directly;
SiteAnalyticsReport keeps its own AnalyticsAggregator+GoalAggregator-backed
array shape (visits/page_views/goals/top_events/series), converting the
ReportDateRange to a day count since those aggregators only support
trailing-N-day windows. A small WritesCsv trait shares CSV-escaping across
the three csv() implementations.

Rewrites tests/Feature/Reports/ReportTypesTest.php for the new interface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds resources/views/reports/site.blade.php (new PDF Blade extending
reports.layout, matching project/link style: KPI grid, page-views trend
table, goals table, top-events table) so SiteAnalyticsReport has a PDF
view. reports.project/reports.link/layout/partials are unchanged.

Adds resources/views/reports/email/{layout,project,link,site}.blade.php
as a fully separate, email-client-safe rendering path: table-based
layout, inline styles only, no <canvas>/JS/Tailwind. Section labels are
localized via the existing reports.report.* catalog (Task 1).

Adds tests/Feature/Reports/ReportViewsTest.php as a render-only smoke
gate (no Browsershot) covering all 6 views plus an empty-data case.
…PDF + CSV)

Adds the queued send path for scheduled reports: SendScheduledReport
resolves the report's ReportType, validates the subject still exists,
resolves the period into a ReportDateRange, builds viewData, optionally
renders CSV/PDF per report.formats, and mails ScheduledReportMail to the
recipients (or an override list for send-now). Scheduling bookkeeping
(last_sent_at/next_run_at) is intentionally left untouched here.
each() delegates to chunk(), which re-runs the same WHERE per page with
an offset; since the callback advances next_run_at, processed rows drop
out of the WHERE and >1000 due reports in one run would cause page 2 to
overshoot and skip still-due rows. Load the due set once via get() and
iterate the in-memory collection instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Reports/{Index,Create,Edit} Inertia pages and a shared
ReportForm partial (mirrors Links/partials/LinkForm) that wires up
type/subject/frequency/period/formats/recipients/active against the
Task-8 ScheduledReportController routes, plus a "Reports" sidebar nav
entry. Fully tokenized and localized via existing reports.* keys.
…-summary key

- Add reports.index.active/inactive to all 4 locales so the Index
  active-toggle pill shows real text ("Active"/"Inactive") instead of
  always "Active" with only color signaling state.
- Add reports.form.save_error_title (reusing each locale's existing
  links.form.save_error_title wording) so ReportForm no longer borrows
  a cross-domain i18n key for its ErrorSummary title.
- Give the Index active-toggle button an aria-label/title.
@DGINXREAL
DGINXREAL requested a review from noidee-dev as a code owner August 9, 2026 05:30
DGINXREAL and others added 2 commits August 9, 2026 07:36
…ge frankenphp CVEs

- guzzlehttp/guzzle 7.15.1 -> 7.15.3 (CVE-2026-69246)
- league/commonmark 2.8.2 -> 2.9.0 (CVE-2026-71488 + 3 GHSA DoS advisories)
- .trivyignore: kin-openapi (CRITICAL) + grpc (HIGH) live in the FrankenPHP
  base-image binary, not app deps; documented, temporary, base-image-fixable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DGINXREAL
DGINXREAL merged commit f4390e1 into main Aug 9, 2026
3 checks passed
@DGINXREAL
DGINXREAL deleted the v2 branch August 9, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant