admin: Reports ▸ Wachstum mit den Abbruchbedingungen, Umami nur lesend angebunden
Roadmap 6.1/6.16: Block 1 bis 3 mit Ist, zeitanteiligem Soll, Schwelle und Status, Trichter seit 17.09., Transparenzseiten aus Umami. Neue Befehle growth:report und umami:report. Zugang über UMAMI_API_* (View-only-Benutzer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e8cf684f02
commit
b4870f98bb
12 changed files with 1098 additions and 0 deletions
74
app/Console/Commands/ReportGrowth.php
Normal file
74
app/Console/Commands/ReportGrowth.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Growth\GrowthReport;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Stand der Abbruchbedingungen für Block 1 bis 3 (Roadmap 6.1) — dieselben
|
||||
* Zahlen wie Admin ▸ Reports ▸ Wachstum.
|
||||
*/
|
||||
class ReportGrowth extends Command
|
||||
{
|
||||
protected $signature = 'growth:report
|
||||
{--fresh : Umami-Werte neu abfragen statt aus dem Zwischenspeicher}
|
||||
{--json : Ausgabe als JSON statt als Tabelle}';
|
||||
|
||||
protected $description = 'Abbruchbedingungen der Wachstumsblöcke: Ist, Schwelle, Status.';
|
||||
|
||||
public function handle(GrowthReport $report): int
|
||||
{
|
||||
$data = $report->build((bool) $this->option('fresh'));
|
||||
|
||||
if ($this->option('json')) {
|
||||
$this->line((string) json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$period = $data['period'];
|
||||
$this->info(sprintf(
|
||||
'Wachstum — Zeitraum %s bis %s, Zwischenblick %s, %d %% der Zeit vergangen',
|
||||
$period['start']->format('d.m.Y'),
|
||||
$period['end']->format('d.m.Y'),
|
||||
$period['review']->format('d.m.Y'),
|
||||
(int) round($period['elapsed_share'] * 100),
|
||||
));
|
||||
|
||||
foreach ($data['blocks'] as $block) {
|
||||
$this->newLine();
|
||||
$this->line("<comment>{$block['title']}</comment>");
|
||||
|
||||
$this->table(
|
||||
['Bedingung', 'Ist', 'Soll heute', 'Schwelle', 'Status'],
|
||||
array_map(static fn (array $criterion): array => [
|
||||
$criterion['label'],
|
||||
$criterion['actual'] ?? '–',
|
||||
$criterion['expected'] ?? '–',
|
||||
$criterion['threshold'],
|
||||
GrowthReport::statusLabel($criterion['status']),
|
||||
], $block['criteria']),
|
||||
);
|
||||
|
||||
foreach ($block['criteria'] as $criterion) {
|
||||
if ($criterion['note'] !== null) {
|
||||
$this->line(' '.$criterion['note']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->line(' '.$block['consequence']);
|
||||
}
|
||||
|
||||
if ($data['pages'] !== []) {
|
||||
$this->newLine();
|
||||
$this->line('<comment>Transparenzseiten, letzte 30 Tage (Umami)</comment>');
|
||||
$this->table(
|
||||
['Portal', 'Seite', 'Aufrufe', 'aus der Suche'],
|
||||
array_map(static fn (array $row): array => [$row['brand'], $row['page'], $row['views'], $row['search_views']], $data['pages']),
|
||||
);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
103
app/Console/Commands/ReportUmami.php
Normal file
103
app/Console/Commands/ReportUmami.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Analytics\UmamiClient;
|
||||
use App\Services\Analytics\UmamiUnavailable;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* Nur lesende Abfrage der Umami-Webanalyse (Roadmap 6.16): Kennzahlen je
|
||||
* Website, dazu eine Rangliste (Seiten, Herkunft, Kanäle …), optional auf
|
||||
* Pfade gefiltert. Zugangsdaten aus `UMAMI_API_*`, werden nie ausgegeben.
|
||||
*/
|
||||
class ReportUmami extends Command
|
||||
{
|
||||
protected $signature = 'umami:report
|
||||
{--from= : Startdatum (Standard: vor 30 Tagen)}
|
||||
{--to= : Enddatum (Standard: jetzt)}
|
||||
{--site=* : pressekonto, presseecho, businessportal24 (Standard: alle)}
|
||||
{--path= : nur Pfade, die diesen Text enthalten, z. B. gute-pressemitteilung}
|
||||
{--metric=channel : Rangliste nach path, referrer, channel, country, device, browser, event}
|
||||
{--limit=10 : Zeilen der Rangliste}
|
||||
{--json : Ausgabe als JSON statt als Tabelle}';
|
||||
|
||||
protected $description = 'Umami-Kennzahlen je Website (nur lesend).';
|
||||
|
||||
private const array METRICS = ['path', 'referrer', 'channel', 'country', 'device', 'browser', 'os', 'event', 'title', 'entry', 'exit'];
|
||||
|
||||
public function handle(UmamiClient $umami): int
|
||||
{
|
||||
if (! $umami->isConfigured()) {
|
||||
$this->error('Umami-API nicht konfiguriert: UMAMI_API_URL, UMAMI_API_USERNAME und UMAMI_API_PASSWORD setzen.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$metric = (string) $this->option('metric');
|
||||
|
||||
if (! in_array($metric, self::METRICS, true)) {
|
||||
$this->error('Unbekannte Rangliste. Möglich: '.implode(', ', self::METRICS).'.');
|
||||
|
||||
return self::INVALID;
|
||||
}
|
||||
|
||||
$to = $this->option('to') !== null ? Carbon::parse((string) $this->option('to'))->endOfDay() : Carbon::now();
|
||||
$from = $this->option('from') !== null ? Carbon::parse((string) $this->option('from'))->startOfDay() : $to->copy()->subDays(30);
|
||||
$sites = (array) $this->option('site') ?: UmamiClient::BRANDS;
|
||||
$filters = filled($this->option('path')) ? ['path' => 'c.'.$this->option('path')] : [];
|
||||
$limit = max(1, min(500, (int) $this->option('limit')));
|
||||
|
||||
$result = [];
|
||||
|
||||
try {
|
||||
foreach ($sites as $site) {
|
||||
$websiteId = $umami->websiteId((string) $site);
|
||||
|
||||
if ($websiteId === null) {
|
||||
$this->warn("Keine Umami-Website für „{$site}“ in config/domains.php.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$site] = [
|
||||
'stats' => $umami->stats($websiteId, $from, $to, $filters),
|
||||
$metric => $umami->metrics($websiteId, $metric, $from, $to, $filters, $limit),
|
||||
];
|
||||
}
|
||||
} catch (UmamiUnavailable $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($this->option('json')) {
|
||||
$this->line((string) json_encode([
|
||||
'from' => $from->toIso8601String(),
|
||||
'to' => $to->toIso8601String(),
|
||||
'filters' => $filters,
|
||||
'sites' => $result,
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(sprintf('Umami — %s bis %s%s', $from->format('d.m.Y'), $to->format('d.m.Y'), $filters === [] ? '' : ', Pfad enthält „'.$this->option('path').'“'));
|
||||
|
||||
foreach ($result as $site => $data) {
|
||||
$this->newLine();
|
||||
$this->line("<comment>{$site}</comment>");
|
||||
$this->table(
|
||||
['Aufrufe', 'Besucher', 'Besuche', 'Absprünge'],
|
||||
[[$data['stats']['pageviews'], $data['stats']['visitors'], $data['stats']['visits'], $data['stats']['bounces']]],
|
||||
);
|
||||
$this->table(
|
||||
[ucfirst($metric), 'Anzahl'],
|
||||
array_map(static fn (array $row): array => [$row['x'], $row['y']], $data[$metric]),
|
||||
);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue