Spoke-Frontend-Audit: Fake-Inhalte raus, Newsletter-Rueckbau, echte Boersendaten, EN-Lokalisierung
Frontend-Umbau (Kevin, 06.07.2026), Ergebnis des businessportal24-Audits: - Fake-Anzeigen und Mock-/Fallback-Daten entfernt (focus-ad, feed-ad, highlight-card, press-release-card, featured-releases, press-releases-grid); Sektionen rendern nur noch mit echten Daten oder gar nicht - Newsletter-Strip und Newsletter-Box zurueckgebaut (Feature nachgelagert, vor Reaktivierung DOI-Flow noetig) - Echte Boersendaten: MarketDataService (Yahoo-Finance-Quotes, config/market.php, Cache) + market:refresh alle 15 Min. im Scheduler; live-ticker blendet sich ohne Daten aus - EN-Lokalisierung der Kern-Seiten via lang/de/web.php + lang/en/web.php; Carbon-Locale folgt app()->getLocale() - Web-Tests auf den neuen Frontend-Stand nachgezogen (Suite: 993 gruen) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3320aaaa85
commit
bfad13af7d
67 changed files with 1735 additions and 1521 deletions
36
app/Console/Commands/RefreshMarketData.php
Normal file
36
app/Console/Commands/RefreshMarketData.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Web\MarketDataService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RefreshMarketData extends Command
|
||||
{
|
||||
protected $signature = 'market:refresh';
|
||||
|
||||
protected $description = 'Kursdaten (DAX, EUR/USD, Brent) für den Web-Ticker abrufen und cachen';
|
||||
|
||||
public function handle(MarketDataService $marketData): int
|
||||
{
|
||||
if (! config('market.enabled')) {
|
||||
$this->info('Marktdaten sind deaktiviert (MARKET_DATA_ENABLED=false).');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$slides = $marketData->refresh();
|
||||
|
||||
if ($slides === []) {
|
||||
$this->warn('Keine Kursdaten erhalten — Cache unverändert.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
foreach ($slides as $slide) {
|
||||
$this->line(sprintf('%s: %s (%s)', $slide['label'], $slide['value'], $slide['delta']));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,11 @@ use App\Models\PressRelease;
|
|||
use App\Support\EditorialClock;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Datenaufbereitung der öffentlichen Startseite einer Ausgabe: Lead-/Seiten-/
|
||||
* meistgelesene Meldungen, aktive Newsrooms, Branchen-Index, „Heute im Fokus"
|
||||
* (Spotlight), Ticker und Kennzahlen.
|
||||
* (Spotlight) und Kennzahlen. Der Ad-Hoc-Ticker kommt zentral aus TickerData.
|
||||
*/
|
||||
class HomePageData
|
||||
{
|
||||
|
|
@ -207,26 +206,15 @@ class HomePageData
|
|||
$spotlight = [
|
||||
'industry' => $spotlightTranslation->name,
|
||||
'stats' => [
|
||||
['label' => 'Meldungen (7 Tage)', 'value' => number_format($windowCount, 0, ',', '.'), 'sub' => 'Stand '.$referenceNow->translatedFormat('j. M Y')],
|
||||
['label' => 'Aktive Unternehmen', 'value' => number_format($activeCompanies, 0, ',', '.'), 'sub' => 'in dieser Branche'],
|
||||
['label' => 'Im Archiv', 'value' => number_format($archiveCount, 0, ',', '.'), 'sub' => 'Meldungen gesamt'],
|
||||
['label' => __('web.spotlight.stat_window'), 'value' => number_format($windowCount, 0, ',', '.'), 'sub' => __('web.spotlight.stat_window_sub', ['date' => $referenceNow->translatedFormat('j. M Y')])],
|
||||
['label' => __('web.spotlight.stat_companies'), 'value' => number_format($activeCompanies, 0, ',', '.'), 'sub' => __('web.spotlight.stat_companies_sub')],
|
||||
['label' => __('web.spotlight.stat_archive'), 'value' => number_format($archiveCount, 0, ',', '.'), 'sub' => __('web.spotlight.stat_archive_sub')],
|
||||
],
|
||||
'releases' => $spotlightReleases->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Ad-Hoc-Ticker aus den jüngsten echten Pressemitteilungen.
|
||||
$tickerItems = $publishedQuery()
|
||||
->orderByDesc('published_at')
|
||||
->limit(8)
|
||||
->get(['id', 'title', 'published_at'])
|
||||
->map(static fn (PressRelease $release): array => [
|
||||
'time' => $release->published_at?->format('H:i') ?? '',
|
||||
'text' => Str::limit((string) $release->title, 70),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'leadRelease' => $leadRelease,
|
||||
'sideReleases' => $sideReleases,
|
||||
|
|
@ -234,7 +222,6 @@ class HomePageData
|
|||
'activeNewsrooms' => $activeNewsrooms,
|
||||
'industryIndex' => $industryIndex,
|
||||
'spotlight' => $spotlight,
|
||||
'tickerItems' => $tickerItems,
|
||||
'homeStats' => [
|
||||
'publishedCount' => $publishedQuery()->count(),
|
||||
'publishedToday' => $publishedQuery()->whereDate('published_at', $referenceNow->toDateString())->count(),
|
||||
|
|
|
|||
109
app/Services/Web/MarketDataService.php
Normal file
109
app/Services/Web/MarketDataService.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Web;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Kursdaten für den Ad-Hoc-Ticker der Web-Frontends.
|
||||
*
|
||||
* refresh() fragt die konfigurierten Symbole (config/market.php) über die
|
||||
* Yahoo-Finance-Chart-API ab und legt sie in den Cache; die Views lesen nur
|
||||
* aus dem Cache (slides()). Schlägt der Abruf für alle Symbole fehl, bleibt
|
||||
* der bestehende Cache-Stand erhalten, damit ein API-Ausfall die Anzeige
|
||||
* nicht sofort leert.
|
||||
*/
|
||||
class MarketDataService
|
||||
{
|
||||
/**
|
||||
* @return list<array{label: string, value: string, delta: string, positive: bool}>
|
||||
*/
|
||||
public function slides(): array
|
||||
{
|
||||
if (! config('market.enabled')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var list<array{label: string, value: string, delta: string, positive: bool}> $slides */
|
||||
$slides = Cache::get((string) config('market.cache_key'), []);
|
||||
|
||||
return is_array($slides) ? $slides : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{label: string, value: string, delta: string, positive: bool}>
|
||||
*/
|
||||
public function refresh(): array
|
||||
{
|
||||
$slides = [];
|
||||
|
||||
foreach ((array) config('market.symbols', []) as $config) {
|
||||
$quote = $this->fetchQuote((string) $config['symbol']);
|
||||
|
||||
if ($quote === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$price, $previousClose] = $quote;
|
||||
|
||||
$deltaPercent = $previousClose > 0.0
|
||||
? ($price - $previousClose) / $previousClose * 100
|
||||
: 0.0;
|
||||
|
||||
$slides[] = [
|
||||
'label' => (string) $config['label'],
|
||||
'value' => number_format($price, (int) ($config['precision'] ?? 2), ',', '.').($config['suffix'] ?? ''),
|
||||
'delta' => ($deltaPercent >= 0 ? '+' : '').number_format($deltaPercent, 1, ',', '.').'%',
|
||||
'positive' => $deltaPercent >= 0,
|
||||
];
|
||||
}
|
||||
|
||||
if ($slides === []) {
|
||||
Log::warning('market:refresh lieferte keine Kursdaten — bestehender Cache-Stand bleibt erhalten.');
|
||||
|
||||
return $this->slides();
|
||||
}
|
||||
|
||||
Cache::put((string) config('market.cache_key'), $slides, (int) config('market.cache_ttl', 86400));
|
||||
|
||||
return $slides;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: float, 1: float}|null [Kurs, Vortagesschluss]
|
||||
*/
|
||||
private function fetchQuote(string $symbol): ?array
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
->withHeaders(['User-Agent' => 'Mozilla/5.0 (compatible; PressekontoTicker/1.0)'])
|
||||
->get(config('market.endpoint').rawurlencode($symbol), [
|
||||
'range' => '1d',
|
||||
'interval' => '1d',
|
||||
]);
|
||||
} catch (\Throwable $exception) {
|
||||
Log::warning('Kursabruf fehlgeschlagen.', ['symbol' => $symbol, 'error' => $exception->getMessage()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::warning('Kursabruf fehlgeschlagen.', ['symbol' => $symbol, 'status' => $response->status()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$meta = $response->json('chart.result.0.meta');
|
||||
|
||||
$price = $meta['regularMarketPrice'] ?? null;
|
||||
$previousClose = $meta['chartPreviousClose'] ?? $meta['previousClose'] ?? null;
|
||||
|
||||
if (! is_numeric($price) || ! is_numeric($previousClose)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [(float) $price, (float) $previousClose];
|
||||
}
|
||||
}
|
||||
42
app/Services/Web/TickerData.php
Normal file
42
app/Services/Web/TickerData.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Web;
|
||||
|
||||
use App\Enums\Portal;
|
||||
use App\Models\PressRelease;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Ad-Hoc-Ticker-Meldungen der Web-Frontends: die jüngsten veröffentlichten
|
||||
* Pressemitteilungen des Portals. Zentral hier statt je Seite, damit der
|
||||
* Ticker auf allen Unterseiten identisch gefüllt ist; kurz gecacht, weil
|
||||
* der Header auf jeder Seite rendert.
|
||||
*/
|
||||
class TickerData
|
||||
{
|
||||
private const CACHE_TTL_SECONDS = 300;
|
||||
|
||||
/**
|
||||
* @return list<array{time: string, text: string}>
|
||||
*/
|
||||
public function items(Portal $portal, string $language): array
|
||||
{
|
||||
$cacheKey = sprintf('web.ticker.%s.%s', $portal->value, $language);
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL_SECONDS, function () use ($portal, $language): array {
|
||||
return PressRelease::query()
|
||||
->forPortalValues([$portal->value, Portal::Both->value])
|
||||
->forLanguage($language)
|
||||
->visibleInArchive()
|
||||
->orderByDesc('published_at')
|
||||
->limit(8)
|
||||
->get(['id', 'title', 'published_at'])
|
||||
->map(static fn (PressRelease $release): array => [
|
||||
'time' => $release->published_at?->format('H:i') ?? '',
|
||||
'text' => Str::limit((string) $release->title, 70),
|
||||
])
|
||||
->all();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -23,13 +23,21 @@ class WebPortalContext
|
|||
public readonly array $domainConfig,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Portal-Ableitung ohne Seiteneffekte (kein Anwenden der Domain-Config) —
|
||||
* für Komponenten, die nur wissen müssen, auf welchem Portal sie rendern.
|
||||
*/
|
||||
public static function portalFromHost(): Portal
|
||||
{
|
||||
return str_contains(request()->getHost(), 'presseecho')
|
||||
? Portal::Presseecho
|
||||
: Portal::Businessportal24;
|
||||
}
|
||||
|
||||
public static function fromRequest(): self
|
||||
{
|
||||
$host = request()->getHost();
|
||||
|
||||
[$domainKey, $portal] = str_contains($host, 'presseecho')
|
||||
? ['presseecho', Portal::Presseecho]
|
||||
: ['businessportal24', Portal::Businessportal24];
|
||||
$portal = self::portalFromHost();
|
||||
$domainKey = $portal === Portal::Presseecho ? 'presseecho' : 'businessportal24';
|
||||
|
||||
return new self(
|
||||
domainKey: $domainKey,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue