switch to a new logviewer and upgraded php libraries
This commit is contained in:
@@ -1,157 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\SuperAdmin\Pages;
|
||||
|
||||
use BackedEnum;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class LogViewerPage extends Page
|
||||
{
|
||||
protected static ?string $navigationLabel = 'Logs';
|
||||
|
||||
protected static ?string $title = 'Application Logs';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
protected string $view = 'filament.super-admin.pages.log-viewer';
|
||||
|
||||
public array $logFiles = [];
|
||||
|
||||
public string $selectedFile = '';
|
||||
|
||||
public string $filter = '';
|
||||
|
||||
public int $limit = 200;
|
||||
|
||||
public array $entries = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->logFiles = $this->discoverLogFiles();
|
||||
$this->selectedFile = $this->logFiles[0]['name'] ?? '';
|
||||
$this->refreshLines();
|
||||
}
|
||||
|
||||
public function updatedSelectedFile(): void
|
||||
{
|
||||
$this->refreshLines();
|
||||
}
|
||||
|
||||
public function updatedFilter(): void
|
||||
{
|
||||
$this->refreshLines();
|
||||
}
|
||||
|
||||
public function updatedLimit(): void
|
||||
{
|
||||
$this->limit = max(50, min(1000, $this->limit));
|
||||
$this->refreshLines();
|
||||
}
|
||||
|
||||
protected function discoverLogFiles(): array
|
||||
{
|
||||
$paths = File::glob(storage_path('logs/*.log')) ?: [];
|
||||
|
||||
$files = collect($paths)
|
||||
->map(fn (string $path) => [
|
||||
'name' => basename($path),
|
||||
'path' => $path,
|
||||
'modified' => File::lastModified($path) ?: 0,
|
||||
'size' => File::size($path) ?: 0,
|
||||
])
|
||||
->sortByDesc('modified')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
protected function refreshLines(): void
|
||||
{
|
||||
$path = storage_path('logs/'.$this->selectedFile);
|
||||
|
||||
if (! File::exists($path)) {
|
||||
$this->entries = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$rawLines = $this->tailFile($path, $this->limit * 2);
|
||||
|
||||
if ($this->filter !== '') {
|
||||
$needle = mb_strtolower($this->filter);
|
||||
$rawLines = array_values(array_filter(
|
||||
$rawLines,
|
||||
fn (string $line) => str_contains(mb_strtolower($line), $needle)
|
||||
));
|
||||
}
|
||||
|
||||
$rawLines = array_slice($rawLines, -$this->limit * 2);
|
||||
$rawLines = $this->mergeStackTraces($rawLines);
|
||||
$rawLines = array_slice(array_reverse($rawLines), 0, $this->limit);
|
||||
|
||||
$this->entries = array_map([$this, 'formatLine'], $rawLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return last $lines lines of file. Simpler approach is fine for small logs in admin view.
|
||||
*/
|
||||
protected function tailFile(string $path, int $lines = 200): array
|
||||
{
|
||||
try {
|
||||
$contents = File::get($path);
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$all = preg_split('/\r\n|\r|\n/', (string) $contents) ?: [];
|
||||
|
||||
return array_slice($all, -$lines);
|
||||
}
|
||||
|
||||
protected function mergeStackTraces(array $lines): array
|
||||
{
|
||||
$merged = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = ltrim($line);
|
||||
$isStackLine = str_starts_with($trimmed, '#')
|
||||
|| str_starts_with($trimmed, 'Stack trace:')
|
||||
|| str_starts_with($line, ' ');
|
||||
|
||||
if ($isStackLine && ! empty($merged)) {
|
||||
$merged[array_key_last($merged)] .= PHP_EOL.$line;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$merged[] = $line;
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
protected function formatLine(string $line): array
|
||||
{
|
||||
$level = 'info';
|
||||
$timestamp = null;
|
||||
|
||||
if (preg_match('/\\[(?<ts>[^\\]]+)\\]/', $line, $matches)) {
|
||||
$timestamp = $matches['ts'] ?? null;
|
||||
}
|
||||
|
||||
if (preg_match('/\\.(EMERGENCY|ALERT|CRITICAL|ERROR|WARNING|NOTICE|INFO|DEBUG)/i', $line, $matches)) {
|
||||
$level = strtolower($matches[1]);
|
||||
}
|
||||
|
||||
return [
|
||||
'text' => $line,
|
||||
'level' => $level,
|
||||
'timestamp' => $timestamp,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ use App\Filament\Widgets\PlatformStatsWidget;
|
||||
use App\Filament\Widgets\RevenueTrendWidget;
|
||||
use App\Filament\Widgets\TopTenantsByRevenue;
|
||||
use App\Filament\Widgets\TopTenantsByUploads;
|
||||
use Boquizo\FilamentLogViewer\FilamentLogViewerPlugin;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Pages;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Widgets;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
@@ -39,6 +41,13 @@ class SuperAdminPanelProvider extends PanelProvider
|
||||
->colors([
|
||||
'primary' => Color::Pink,
|
||||
])
|
||||
->plugins([
|
||||
FilamentLogViewerPlugin::make()
|
||||
->navigationGroup('Platform')
|
||||
->navigationLabel('Log Viewer')
|
||||
->navigationIcon(Heroicon::OutlinedDocumentText)
|
||||
->navigationSort(20),
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||
->discoverPages(in: app_path('Filament/SuperAdmin/Pages'), for: 'App\\Filament\\SuperAdmin\\Pages')
|
||||
->pages([
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dompdf/dompdf": "2.0",
|
||||
"filament/filament": "~4.0",
|
||||
"firebase/php-jwt": "^6.11",
|
||||
"gboquizosanchez/filament-log-viewer": "*",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/horizon": "^5.37",
|
||||
|
||||
1290
composer.lock
generated
1290
composer.lock
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))}}}export{c as default};
|
||||
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||i.checked!==e&&(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))}}}export{c as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default};
|
||||
function r({initialHeight:i,shouldAutosize:s,state:h}){return{state:h,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),s?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=i+"rem")},resize(){if(this.$el.scrollHeight<=0)return;let e=this.$el.style.height;this.$el.style.height="0px";let t=this.$el.scrollHeight+"px";this.$el.style.height=e,this.wrapperEl.style.height!==t&&(this.wrapperEl.style.height=t)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default};
|
||||
|
||||
@@ -1 +1 @@
|
||||
function u({activeTab:a,isTabPersistedInQueryString:e,livewireId:h,tab:o,tabQueryStringKey:s}){return{tab:o,init(){let t=this.getTabs(),i=new URLSearchParams(window.location.search);e&&i.has(s)&&t.includes(i.get(s))&&(this.tab=i.get(s)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[a-1]),Livewire.hook("commit",({component:r,commit:f,succeed:c,fail:l,respond:b})=>{c(({snapshot:d,effect:m})=>{this.$nextTick(()=>{if(r.id!==h)return;let n=this.getTabs();n.includes(this.tab)||(this.tab=n[a-1]??this.tab)})})})},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!e)return;let t=new URL(window.location.href);t.searchParams.set(s,this.tab),history.replaceState(null,document.title,t.toString())}}}export{u as default};
|
||||
function I({activeTab:w,isScrollable:f,isTabPersistedInQueryString:m,livewireId:g,tab:T,tabQueryStringKey:c}){return{boundResizeHandler:null,isScrollable:f,resizeDebounceTimer:null,tab:T,withinDropdownIndex:null,withinDropdownMounted:!1,init(){let t=this.getTabs(),e=new URLSearchParams(window.location.search);m&&e.has(c)&&t.includes(e.get(c))&&(this.tab=e.get(c)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[w-1]),Livewire.hook("commit",({component:n,commit:d,succeed:r,fail:h,respond:u})=>{r(({snapshot:p,effect:i})=>{this.$nextTick(()=>{if(n.id!==g)return;let s=this.getTabs();s.includes(this.tab)||(this.tab=s[w-1]??this.tab)})})}),f||(this.boundResizeHandler=this.debouncedUpdateTabsWithinDropdown.bind(this),window.addEventListener("resize",this.boundResizeHandler),this.updateTabsWithinDropdown())},calculateAvailableWidth(t){let e=window.getComputedStyle(t);return Math.floor(t.clientWidth)-Math.ceil(parseFloat(e.paddingLeft))*2},calculateContainerGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap))},calculateDropdownIconWidth(t){let e=t.querySelector(".fi-icon");return Math.ceil(e.clientWidth)},calculateTabItemGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap)||8)},calculateTabItemPadding(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.paddingLeft))+Math.ceil(parseFloat(e.paddingRight))},findOverflowIndex(t,e,n,d,r,h){let u=t.map(i=>Math.ceil(i.clientWidth)),p=t.map(i=>{let s=i.querySelector(".fi-tabs-item-label"),a=i.querySelector(".fi-badge"),o=Math.ceil(s.clientWidth),l=a?Math.ceil(a.clientWidth):0;return{label:o,badge:l,total:o+(l>0?d+l:0)}});for(let i=0;i<t.length;i++){let s=u.slice(0,i+1).reduce((b,y)=>b+y,0),a=i*n,o=p.slice(i+1),l=o.length>0,W=l?Math.max(...o.map(b=>b.total)):0,D=l?r+W+d+h+n:0;if(s+a+D>e)return i}return-1},get isDropdownButtonVisible(){return this.withinDropdownMounted?this.withinDropdownIndex===null?!1:this.getTabs().findIndex(e=>e===this.tab)<this.withinDropdownIndex:!0},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!m)return;let t=new URL(window.location.href);t.searchParams.set(c,this.tab),history.replaceState(null,document.title,t.toString())},debouncedUpdateTabsWithinDropdown(){clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=setTimeout(()=>this.updateTabsWithinDropdown(),150)},async updateTabsWithinDropdown(){this.withinDropdownIndex=null,this.withinDropdownMounted=!1,await this.$nextTick();let t=this.$el.querySelector(".fi-tabs"),e=t.querySelector(".fi-tabs-item:last-child"),n=Array.from(t.children).slice(0,-1),d=n.map(a=>a.style.display);n.forEach(a=>a.style.display=""),t.offsetHeight;let r=this.calculateAvailableWidth(t),h=this.calculateContainerGap(t),u=this.calculateDropdownIconWidth(e),p=this.calculateTabItemGap(n[0]),i=this.calculateTabItemPadding(n[0]),s=this.findOverflowIndex(n,r,h,p,i,u);n.forEach((a,o)=>a.style.display=d[o]),s!==-1&&(this.withinDropdownIndex=s),this.withinDropdownMounted=!0},destroy(){this.boundResizeHandler&&window.removeEventListener("resize",this.boundResizeHandler),clearTimeout(this.resizeDebounceTimer)}}}export{I as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,63 +0,0 @@
|
||||
<x-filament::page>
|
||||
@php($selectedMeta = collect($logFiles)->firstWhere('name', $selectedFile))
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-semibold uppercase text-slate-500">Logdatei</label>
|
||||
<select wire:model="selectedFile" class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm focus:border-rose-300 focus:ring-1 focus:ring-rose-200">
|
||||
@foreach($logFiles as $file)
|
||||
<option value="{{ $file['name'] }}">
|
||||
{{ $file['name'] }} ({{ number_format($file['size'] / 1024, 1) }} KB)
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p class="text-xs text-slate-500">Zuletzt aktualisiert: {{ $selectedMeta ? \Carbon\Carbon::createFromTimestamp($selectedMeta['modified'])->diffForHumans() : '—' }}</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-semibold uppercase text-slate-500">Filter (Level/Text)</label>
|
||||
<input type="text" wire:model.debounce.400ms="filter" placeholder="error|warning|payment" class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm focus:border-rose-300 focus:ring-1 focus:ring-rose-200" />
|
||||
<p class="text-xs text-slate-500">Einfache Teilstring-Suche. Kein Regex.</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-semibold uppercase text-slate-500">Zeilenlimit</label>
|
||||
<input type="number" min="50" max="1000" step="50" wire:model.lazy="limit" class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm focus:border-rose-300 focus:ring-1 focus:ring-rose-200" />
|
||||
<p class="text-xs text-slate-500">Letzte Zeilen (nach Filter).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div class="flex items-center justify-between border-b border-slate-200 px-4 py-2 text-xs uppercase tracking-wide text-slate-500 dark:border-slate-800 dark:text-slate-400">
|
||||
<span>{{ $selectedFile ?: 'Keine Datei' }}</span>
|
||||
<span>{{ $filter ? 'Gefiltert' : 'Ungefiltert' }} · {{ count($entries) }} Zeilen</span>
|
||||
</div>
|
||||
<div class="max-h-[70vh] overflow-y-auto">
|
||||
<ul class="divide-y divide-slate-100 text-xs dark:divide-slate-800">
|
||||
@forelse($entries as $entry)
|
||||
<li class="px-4 py-2">
|
||||
<div class="flex items-start gap-3">
|
||||
<span @class([
|
||||
'inline-flex min-w-[64px] items-center justify-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
||||
'bg-red-100 text-red-800 dark:bg-red-500/20 dark:text-red-200' => $entry['level'] === 'error' || $entry['level'] === 'critical',
|
||||
'bg-amber-100 text-amber-900 dark:bg-amber-500/20 dark:text-amber-100' => $entry['level'] === 'warning',
|
||||
'bg-blue-100 text-blue-800 dark:bg-blue-500/20 dark:text-blue-100' => $entry['level'] === 'info',
|
||||
'bg-slate-200 text-slate-800 dark:bg-slate-700 dark:text-slate-200' => ! in_array($entry['level'], ['error','critical','warning','info']),
|
||||
])>
|
||||
{{ strtoupper($entry['level']) }}
|
||||
</span>
|
||||
<div class="flex-1 space-y-1">
|
||||
@if($entry['timestamp'])
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400">{{ $entry['timestamp'] }}</p>
|
||||
@endif
|
||||
<p class="whitespace-pre-wrap leading-relaxed text-slate-800 dark:text-slate-100">{{ $entry['text'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="px-4 py-3 text-slate-500 dark:text-slate-400">Keine Logeinträge gefunden.</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::page>
|
||||
Reference in New Issue
Block a user