Feat/branding and bot enhancements#301
Conversation
📝 WalkthroughWalkthroughThis PR introduces avatar field propagation through message activity tracking, adds admin panel provider scoping via middleware and query filtering, delivers a new console command for syncing Discord message history, and comprehensively redesigns the login pages and admin branding. The avatar flows from Discord events through DTOs, persistence, and into external identity metadata. A new SetProviderScope middleware enables session-based filtering by identity provider, integrated into the admin panel via a custom topbar with tenant and provider pickers. The app gains a SyncDiscordHistoryCommand to import historical channel messages. Both admin and app login experiences are rebuilt with custom styling, form configuration, and Livewire event handling, supported by new branding components and theme updates. Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (2)
app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php (1)
52-61: 💤 Low valueConsider removing the redundant timezone fallback.
The
config('app.display_timezone')already defaults to'America/Sao_Paulo'inconfig/app.php, so the explicit fallback on line 55 is unnecessary.♻️ Simplify timezone retrieval
- $tz = config('app.display_timezone', 'America/Sao_Paulo'); + $tz = config('app.display_timezone');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php` around lines 52 - 61, Remove the redundant fallback in the mount() method: instead of calling config('app.display_timezone', 'America/Sao_Paulo') use config('app.display_timezone') so the default defined in config/app.php is honored; update the mount() function references to $tz = config('app.display_timezone') and keep the existing assignments to $this->startDate, $this->endDate, and $this->channelId unchanged (function: mount, properties: startDate, endDate, channelId).app-modules/he4rt/resources/views/components/base/logo.blade.php (1)
1-7: ⚡ Quick winComponent should merge attributes for reusability.
The logo component doesn't merge attributes onto its root element. In
brand.blade.php(line 2), the component is used withclass="h-8 w-auto", but this attribute will be silently ignored. Add attribute merging to make the component customizable.♻️ Proposed fix
-<div class="flex items-center gap-3 py-1"> +<div {{ $attributes->merge(['class' => 'flex items-center gap-3 py-1']) }}> <img src="{{ asset('images/logo.svg') }}" alt="He4rt Developers" class="h-8 w-auto dark:invert" /> <div class="flex flex-col leading-none"> <span class="text-sm font-black tracking-tight text-zinc-950 dark:text-white uppercase">He4rt</span> <span class="text-[10px] font-bold text-purple-600 uppercase tracking-widest">Developers</span> </div> </div>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/he4rt/resources/views/components/base/logo.blade.php` around lines 1 - 7, The root element in the logo Blade component doesn't merge incoming attributes so caller classes (e.g., class="h-8 w-auto") are ignored; update the root div to merge $attributes by replacing the hardcoded class with attribute merging like $attributes->merge(['class' => 'flex items-center gap-3 py-1']) on the root <div> in logo.blade.php so passed-in classes and other attributes are applied; keep the existing structure (img and inner div) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app-modules/bot-discord/src/Events/MessageReceivedEvent.php`:
- Line 53: MessageReceivedEvent uses the magic property avatar on the Discord
User which PHPStan flags; replace the property access with the explicit accessor
by calling getAvatarAttribute() on the author object (i.e., change occurrences
of $message->author->avatar to $message->author->getAvatarAttribute()) in the
MessageReceivedEvent class so static analysis recognizes the getter.
In
`@app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php`:
- Around line 37-40: The current code always calls $identity->update when
$metadata is non-empty which causes no-op DB writes; compute $merged =
array_merge($identity->metadata ?? [], $metadata) and compare it to
$identity->metadata (strict equality) and only call
$identity->update(['metadata' => $merged]) if $merged !== ($identity->metadata
?? []); this ensures ResolveExternalIdentity only updates the DB when the
metadata actually changes.
In
`@app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php`:
- Around line 69-84: The success counter is incremented regardless because
NewMessageAction::persist() swallows exceptions; update the call site in
SyncDiscordHistoryCommand (the block creating NewMessageDTO and calling
$newMessageAction->persist(...)) to only increment $count when persist() returns
a success flag, and change NewMessageAction::persist() (in
app-modules/activity/src/Message/Actions/NewMessage.php) to return a boolean
(true on successful persistence, false or throw on failure) or rethrow failures
instead of swallowing them; then modify SyncDiscordHistoryCommand to check the
returned boolean (or catch rethrown exceptions) and only $count++ on
true/success so failed writes are not counted.
- Around line 29-30: SyncDiscordHistoryCommand currently reads environment
variables at runtime; change the two places where $botToken and $guildId are set
to use config() instead of env() so config caching works. Replace the fallback
env('HE4RT_DISCORD_BOT_KEY') with a config key (e.g. config('discord.token'))
when assigning $botToken, and replace env('HE4RT_DISCORD_GUILD') with a config
lookup (e.g. config('he4rt.discord.guild_id')) when assigning $guildId; update
any references in the class to use these config-backed variables.
In
`@app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/ExternalIdentityResource.php`:
- Around line 69-72: getEloquentQuery is being scoped by
session('active_provider') but getGlobalSearchEloquentQuery is not, causing
global search to bypass the provider filter; extract the provider filtering into
a shared helper (e.g., private static function applyProviderScope(Builder
$query): Builder or a protected method) that applies the
session('active_provider') when present and then call that helper from both
getEloquentQuery() and getGlobalSearchEloquentQuery() (also update the same
pattern used around the other builder code at the region noted for lines 79-84)
so both the table and global search use the identical provider scope.
In `@app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php`:
- Around line 91-96: The closure passed to map incorrectly type-hints its
parameter as Message ($stat) even though $messageStats contains query result
rows with only external_identity_id and total_messages; remove the Message type
hint (change to function ($stat) or function (object $stat)) so static analysis
sees the correct shape, keep using (string) $stat->external_identity_id and
(int) $stat->total_messages and call $this->extractDiscordData($identity, (int)
$stat->total_messages) as before.
In `@app-modules/panel-app/resources/views/auth/login.blade.php`:
- Around line 91-128: The OAuth links in login.blade.php are passing
request()->getHost() as the 'tenant' param to route('oauth.redirect'), causing
wrong tenant resolution; update each anchor href to use the current tenant's
route key instead of request()->getHost() (e.g., replace request()->getHost()
with the current tenant's route key retrieval such as tenant()->getRouteKey() or
currentTenant()->getRouteKey() depending on your tenant helper). Ensure all
three providers (Discord/GitHub/Twitch) that build the route in this file use
the tenant route key and keep panelId and provider unchanged.
- Around line 141-167: The login form is missing a surrounding <form> element
with wire:submit to handle authentication properly, so pressing Enter won't work
as expected. Wrap the relevant inputs and the submit button inside a <form> tag,
using wire:submit.prevent="authenticate" for submission. Remove the
wire:click="authenticate" from the button since the form submit event will
handle it. This will restore proper semantic form behavior and accessibility.
In `@app-modules/panel-app/src/Pages/LoginPage.php`:
- Around line 67-75: The phpstan failure is caused by calling ->label() on
methods that are typed to the base \Filament\Schemas\Components\Component;
update getEmailFormComponent(), getPasswordFormComponent(), and
getRememberFormComponent() to return concrete component classes (e.g.,
\Filament\Forms\Components\TextInput for email, a password-capable TextInput or
specific Password component for password, and
\Filament\Forms\Components\Checkbox for the remember field) and move the
->label('...') calls into those getter implementations (so each getter returns
the labeled concrete component), then leave LoginPage::form() to only assemble
the returned components without chaining ->label().
In `@resources/css/filament/admin/theme.css`:
- Around line 24-27: The stylesheet is failing stylelint's
declaration-empty-line-before rule; add a single blank line before the affected
declaration blocks so the rule is satisfied—specifically insert an empty line
before the declaration inside the .fi-topbar rule and likewise in the other
affected selector blocks referenced around the ranges 73-76 and 91-94; keep
existing properties and !important flags unchanged, just ensure a blank line
immediately before each offending declaration to satisfy
declaration-empty-line-before.
In `@resources/views/filament/admin/components/topbar-tenant-menu.blade.php`:
- Around line 6-8: The current fallback for $currentProvider uses $providers[1],
which is fragile; change the fallback to a concrete enum value (e.g.
\He4rt\Identity\ExternalIdentity\Enums\IdentityProvider::Discord) so that when
IdentityProvider::tryFrom($activeProviderValue) returns null you explicitly
default to IdentityProvider::Discord; update the assignment for $currentProvider
(the line referencing IdentityProvider::tryFrom and $providers[1]) to use the
explicit enum fallback and ensure session default (session('active_provider',
...)) remains consistent with that same enum.
---
Nitpick comments:
In `@app-modules/he4rt/resources/views/components/base/logo.blade.php`:
- Around line 1-7: The root element in the logo Blade component doesn't merge
incoming attributes so caller classes (e.g., class="h-8 w-auto") are ignored;
update the root div to merge $attributes by replacing the hardcoded class with
attribute merging like $attributes->merge(['class' => 'flex items-center gap-3
py-1']) on the root <div> in logo.blade.php so passed-in classes and other
attributes are applied; keep the existing structure (img and inner div)
unchanged.
In `@app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php`:
- Around line 52-61: Remove the redundant fallback in the mount() method:
instead of calling config('app.display_timezone', 'America/Sao_Paulo') use
config('app.display_timezone') so the default defined in config/app.php is
honored; update the mount() function references to $tz =
config('app.display_timezone') and keep the existing assignments to
$this->startDate, $this->endDate, and $this->channelId unchanged (function:
mount, properties: startDate, endDate, channelId).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Pro
Run ID: c18fcf6a-c91a-4610-b018-b96ba7ad79d6
📒 Files selected for processing (21)
app-modules/activity/src/Message/Actions/NewMessage.phpapp-modules/activity/src/Message/DTOs/NewMessageDTO.phpapp-modules/bot-discord/src/Events/MessageReceivedEvent.phpapp-modules/he4rt/resources/views/components/base/logo.blade.phpapp-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.phpapp-modules/integration-discord/src/IntegrationDiscordServiceProvider.phpapp-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.phpapp-modules/panel-admin/src/Filament/Resources/ExternalIdentities/ExternalIdentityResource.phpapp-modules/panel-admin/src/Http/Middleware/SetProviderScope.phpapp-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.phpapp-modules/panel-admin/src/PanelAdminServiceProvider.phpapp-modules/panel-app/resources/views/auth/login.blade.phpapp-modules/panel-app/src/Pages/LoginPage.phpapp/Filament/Pages/Login.phpapp/Providers/Filament/AdminPanelProvider.phpapp/Providers/Filament/AppPanelProvider.phpresources/css/filament/admin/theme.cssresources/css/filament/app/theme.cssresources/views/filament/admin/components/brand.blade.phpresources/views/filament/admin/components/topbar-content.blade.phpresources/views/filament/admin/components/topbar-tenant-menu.blade.php
| content: $message->content, | ||
| sentAt: $message->timestamp->toDateTimeImmutable() | ||
| sentAt: $message->timestamp->toDateTimeImmutable(), | ||
| avatar: $message->author->avatar, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and display the relevant section of the file.
FILE="app-modules/bot-discord/src/Events/MessageReceivedEvent.php"
if [ -f "$FILE" ]; then
echo "=== $FILE (around line 40-80) ==="
nl -ba "$FILE" | sed -n '35,80p'
else
echo "Missing file: $FILE"
fi
# Search for avatar getter usage patterns across the repo.
echo "=== Searching for avatar usage patterns ==="
rg -n --hidden --no-ignore-vcs "getAvatarAttribute\\(|\\$author->avatar\\b|->avatar\\b" .
# Search for DiscordPHP User avatar accessor patterns if present.
echo "=== Searching for 'Discord\\Parts\\User\\User' type usage ==="
rg -n --hidden --no-ignore-vcs "Discord\\\\Parts\\\\User\\\\User" .
# Search for any existing workaround/stubs for DiscordPHP magic properties.
echo "=== Searching for PHPStan stubs/workarounds ==="
rg -n --hidden --no-ignore-vcs "(phpstan|stub|ignoreErrors|magic|undefined property|User::\\$avatar)" .Repository: he4rt/heartdevs.com
Length of output: 212
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/bot-discord/src/Events/MessageReceivedEvent.php"
if [ -f "$FILE" ]; then
echo "=== $FILE (around lines 40-80) ==="
# show with line numbers via awk (nl isn't available)
awk 'NR>=35 && NR<=80 {printf "%6d %s\n", NR, $0}' "$FILE"
else
echo "Missing file: $FILE"
exit 1
fi
echo "=== Searching for avatar usage patterns ==="
rg -n --hidden --no-ignore-vcs "getAvatarAttribute\\(|\\$author->avatar\\b|->avatar\\b" app-modules/bot-discord src . || true
echo "=== Searching for DiscordPHP User avatar accessor patterns ==="
rg -n --hidden --no-ignore-vcs "getAvatarAttribute\\(" app-modules/bot-discord . || true
echo "=== Searching for 'Discord\\\\Parts\\\\User\\\\User' type usage ==="
rg -n --hidden --no-ignore-vcs "Discord\\\\Parts\\\\User\\\\User" . || true
echo "=== Searching for existing PHPStan/stub/ignore workarounds ==="
rg -n --hidden --no-ignore-vcs "(phpstan|stub|ignoreErrors|magic|undefined property|User::\\$avatar)" . || trueRepository: he4rt/heartdevs.com
Length of output: 2582
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Contents around $FILE ==="
FILE="app-modules/bot-discord/src/Events/MessageReceivedEvent.php"
awk 'NR>=45 && NR<=65 {printf "%6d %s\n", NR, $0}' "$FILE"
echo
echo "=== Searching for getAvatarAttribute usage ==="
rg -n --hidden --no-ignore-vcs "getAvatarAttribute\\(" app-modules . || true
echo
echo "=== Searching for direct avatar magic-property usage (${message->author->avatar}) ==="
rg -n --hidden --no-ignore-vcs -F "\$message->author->avatar" . || true
echo
echo "=== Searching for other '->avatar' property accesses ==="
rg -n --hidden --no-ignore-vcs "->avatar\\b" app-modules . || true
echo
echo "=== Searching for PHPStan ignore/stubs for undefined/magic properties ==="
rg -n --hidden --no-ignore-vcs "(ignoreErrors|`@phpstan-ignore`|undefined property|stub|magic property|User::\\$avatar)" . || true
echo
echo "=== Searching for any 'User' avatar accessor patterns ==="
rg -n --hidden --no-ignore-vcs "Avatar" app-modules/bot-discord/src . || trueRepository: he4rt/heartdevs.com
Length of output: 105
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/bot-discord/src/Events/MessageReceivedEvent.php"
echo "=== Contents around $FILE ==="
awk 'NR>=45 && NR<=65 {printf "%6d %s\n", NR, $0}' "$FILE"
echo
echo "=== Searching for getAvatarAttribute usage ==="
rg -n --hidden --no-ignore-vcs "getAvatarAttribute\\(" app-modules . || true
echo
echo "=== Searching for direct avatar magic-property usage ($message->author->avatar) ==="
rg -n --hidden --no-ignore-vcs -F "\$message->author->avatar" . || true
echo
echo "=== Searching for other '->avatar' property accesses ==="
rg -n --hidden --no-ignore-vcs "->avatar\\b" app-modules . || true
echo
echo "=== Searching for PHPStan ignore/stubs for undefined/magic properties ==="
rg -n --hidden --no-ignore-vcs "(ignoreErrors|`@phpstan-ignore`|undefined property|stub|magic property|User::\\$avatar|User\\s*::\\s*\\$avatar)" . || trueRepository: he4rt/heartdevs.com
Length of output: 1495
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/bot-discord/src/Events/MessageReceivedEvent.php"
echo "=== Contents around $FILE ==="
awk 'NR>=45 && NR<=60 {printf "%6d %s\n", NR, $0}' "$FILE"
echo
echo "=== Searching for getAvatarAttribute usage ==="
rg -n --hidden --no-ignore-vcs 'getAvatarAttribute\s*\(' app-modules . || true
echo
echo '=== Searching for direct avatar magic-property usage ($message->author->avatar) ==='
rg -n --hidden --no-ignore-vcs -F '\$message->author->avatar' app-modules . || true
echo
echo "=== Searching for any '->avatar' accesses ==="
rg -n --hidden --no-ignore-vcs '->avatar\b' app-modules . || true
echo
echo "=== Searching for PHPStan ignore/stub patterns for undefined/magic properties ==="
rg -n --hidden --no-ignore-vcs '(ignoreErrors|`@phpstan-ignore`|undefined property|stub|magic property|User::\$avatar|User\s*::\s*\$avatar)' . || trueRepository: he4rt/heartdevs.com
Length of output: 10461
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the PHPStan ignore rules for bot-discord, focusing on $avatar
IGNORE_FILE="app-modules/bot-discord/phpstan.ignore.neon"
echo "=== Contents of $IGNORE_FILE (full file) ==="
awk '{print NR ":" $0}' "$IGNORE_FILE" | sed -n '1,120p' || true
echo
echo "=== Searching for getAvatarAttribute in repo ==="
rg -n --hidden --no-ignore-vcs -F 'getAvatarAttribute' app-modules . || true
echo
echo "=== Searching for magic $author->avatar / $message->author->avatar ==="
rg -n --hidden --no-ignore-vcs -F '\$author->avatar' app-modules . || true
rg -n --hidden --no-ignore-vcs -F '\$message->author->avatar' app-modules . || true
echo
echo "=== Searching for other direct '->avatar' property reads (avoid regex escapes) ==="
rg -n --hidden --no-ignore-vcs -F '->avatar' app-modules . || trueRepository: he4rt/heartdevs.com
Length of output: 3466
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Contents of $IGNORE_FILE (full file) ==="
IGNORE_FILE="app-modules/bot-discord/phpstan.ignore.neon"
awk '{print NR ":" $0}' "$IGNORE_FILE" | sed -n '1,120p'
echo
echo "=== Search for getAvatarAttribute usage (anywhere) ==="
rg -n --hidden --no-ignore-vcs -F 'getAvatarAttribute' app-modules . || true
echo
echo "=== Search for direct magic avatar reads: '$message->author->avatar' ==="
rg -n --hidden --no-ignore-vcs -F '$message->author->avatar' app-modules . || true
echo
echo "=== Search for other magic avatar reads: '->avatar' in bot-discord module ==="
rg -n --hidden --no-ignore-vcs -F '->avatar' app-modules/bot-discord . || trueRepository: he4rt/heartdevs.com
Length of output: 112
🏁 Script executed:
#!/bin/bash
set -euo pipefail
IGNORE_FILE="app-modules/bot-discord/phpstan.ignore.neon"
echo "=== Contents of $IGNORE_FILE (full file) ==="
awk '{print NR ":" $0}' "$IGNORE_FILE" | sed -n '1,200p'
echo
echo "=== Search for getAvatarAttribute usage (anywhere) ==="
rg -n --hidden --no-ignore-vcs -F 'getAvatarAttribute' app-modules . || true
echo
echo "=== Search for direct magic avatar reads: '$message->author->avatar' ==="
rg -n --hidden --no-ignore-vcs -F '$message->author->avatar' app-modules . || true
echo
echo "=== Search for other magic avatar reads: '->avatar' in bot-discord module ==="
rg -n --hidden --no-ignore-vcs -F '->avatar' app-modules/bot-discord . || trueRepository: he4rt/heartdevs.com
Length of output: 3473
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/bot-discord/src/Events/MessageReceivedEvent.php"
echo "=== app-modules/bot-discord/src/Events/MessageReceivedEvent.php (lines 1-120) ==="
awk 'NR>=1 && NR<=120 {printf "%6d %s\n", NR, $0}' "$FILE"
echo
echo "=== app-modules/bot-discord/src/Events/MessageReceivedEvent.php (lines 40-70) ==="
awk 'NR>=40 && NR<=70 {printf "%6d %s\n", NR, $0}' "$FILE"
echo
echo "=== Search for getAvatarAttribute usage (whole repo) ==="
rg -n --hidden --no-ignore-vcs 'getAvatarAttribute\s*\(' . || true
echo
echo "=== Search for getAvatar( usage (whole repo) ==="
rg -n --hidden --no-ignore-vcs 'getAvatar\s*\(' . || true
echo
echo "=== Search for direct magic avatar reads: \\$message->author->avatar ==="
rg -n --hidden --no-ignore-vcs -F '\$message->author->avatar' . || true
echo
echo "=== Search for other direct '->avatar' reads in bot-discord module ==="
rg -n --hidden --no-ignore-vcs '->avatar\b' app-modules/bot-discord . || true
echo
echo "=== Show phpstan ignore entries for User::\\$avatar ==="
rg -n --hidden --no-ignore-vcs 'Discord\\\\Parts\\\\User\\\\User::\\\$avatar' app-modules/bot-discord/phpstan.ignore.neon || trueRepository: he4rt/heartdevs.com
Length of output: 5810
🌐 Web query:
DiscordPHP Discord\Parts\User\User avatar getter getAvatarAttribute
💡 Result:
In DiscordPHP, the getAvatarAttribute method is a helper function used to generate the URL for a user's avatar [1][2]. Method Signature: public function getAvatarAttribute(?string $format = null, int $size = 1024): string Parameters: - $format (string|null): The desired image format (e.g., 'png', 'jpg', 'webp', or 'gif'). If not provided, it defaults to 'webp', or 'gif' if the avatar is animated [2]. - $size (int): The width/height of the image in pixels. It defaults to 1024 and is automatically clamped to allowed powers of two between 16 and 4096 [2]. Usage: While you can access the default avatar URL via the $user->avatar property, getAvatarAttribute is intended for cases where you need to specify a custom image format or size [1][2]. Example: $url = $user->getAvatarAttribute('png', 2048); echo $url; // Outputs: https://cdn.discordapp.com/avatars/:user_id/:avatar_hash.png?size=2048 If the user does not have a custom avatar set, the method returns the URL to their default Discord embed avatar based on their discriminator or ID [2]. Note that a similar method exists for the Member part to fetch guild-specific avatars [3].
Citations:
- 1: http://discord-php.github.io/DiscordPHP/guide/parts/user.html
- 2: https://github.com/discord-php/DiscordPHP/blob/master/src/Discord/Parts/User/User.php
- 3: http://discord-php.github.io/DiscordPHP/guide/parts/member.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for phpstan ignore entries for Discord\\Parts\\User\\User::\\$avatar ==="
rg -n --hidden --no-ignore-vcs "Discord\\\\Parts\\\\User\\\\User::\\\$avatar" . || true
echo
echo "=== Search for phpstan ignore entries for User::\\$avatar (broader) ==="
rg -n --hidden --no-ignore-vcs "User::\\\$avatar" . || true
echo
echo "=== Search for 'avatar:' DTO field assignment patterns in bot-discord ==="
rg -n --hidden --no-ignore-vcs "avatar:\s*\$" app-modules/bot-discord . || true
echo
echo "=== Search for any getAvatarAttribute usage anywhere ==="
rg -n --hidden --no-ignore-vcs "getAvatarAttribute\s*\(" . || trueRepository: he4rt/heartdevs.com
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for phpstan ignore entries for Discord\\Parts\\User\\User::\\$avatar ==="
rg -n --hidden --no-ignore-vcs 'Discord\\\\Parts\\\\User\\\\User::\$avatar' . || true
echo
echo "=== Search for phpstan ignore entries for User::\\$avatar (broader) ==="
rg -n --hidden --no-ignore-vcs 'User::\$avatar' . || true
echo
echo "=== Search for 'avatar:' DTO field assignment patterns in bot-discord ==="
rg -n --hidden --no-ignore-vcs 'avatar:\s*\$' app-modules/bot-discord . || true
echo
echo "=== Search for any getAvatarAttribute usage anywhere ==="
rg -n --hidden --no-ignore-vcs 'getAvatarAttribute\s*\(' . || trueRepository: he4rt/heartdevs.com
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for phpstan ignore entries for Discord\\Parts\\User\\User::(avatar) ==="
rg -n --hidden --no-ignore-vcs 'Discord\\\\Parts\\\\User\\\\User::\$avatar' . || true
echo
echo "=== Search for phpstan ignore entries mentioning (User)::(avatar) ==="
rg -n --hidden --no-ignore-vcs 'User::\$avatar' . || true
echo
echo "=== Search for 'avatar:' DTO field assignment patterns in bot-discord ==="
rg -n --hidden --no-ignore-vcs 'avatar:\s*\$' app-modules/bot-discord . || true
echo
echo "=== Search for any getAvatarAttribute usage anywhere ==="
rg -n --hidden --no-ignore-vcs 'getAvatarAttribute\s*\(' . || trueRepository: he4rt/heartdevs.com
Length of output: 950
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the DTO type for `avatar`
DTO_FILE="app-modules/activity/src/Message/DTOs/NewMessageDTO.php"
echo "=== $DTO_FILE (lines 1-120) ==="
awk 'NR>=1 && NR<=140 {printf "%6d %s\n", NR, $0}' "$DTO_FILE"
echo
echo "=== Inspect bot-discord phpstan ignore entry lines mentioning User\\User::\\\$avatar ==="
IGNORE_FILE="app-modules/bot-discord/phpstan.ignore.neon"
# print the full matching line numbers (no regex confusion)
rg -n --hidden --no-ignore-vcs -F 'Discord\\Parts\\User\\User::\$avatar' "$IGNORE_FILE" || trueRepository: he4rt/heartdevs.com
Length of output: 2002
Use the explicit DiscordPHP avatar getter for static analysis
Replace avatar: $message->author->avatar in app-modules/bot-discord/src/Events/MessageReceivedEvent.php with the documented accessor ($message->author->getAvatarAttribute()). The repo’s phpstan.ignore.neon suppresses Discord\Parts\User\User::$avatar only for other files, not MessageReceivedEvent.php, so this magic-property access is likely still flagged by PHPStan.
🧰 Tools
🪛 GitHub Check: Perform Phpstan Check / Run
[failure] 53-53:
Access to an undefined property Discord\Parts\User\User::$avatar.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/bot-discord/src/Events/MessageReceivedEvent.php` at line 53,
MessageReceivedEvent uses the magic property avatar on the Discord User which
PHPStan flags; replace the property access with the explicit accessor by calling
getAvatarAttribute() on the author object (i.e., change occurrences of
$message->author->avatar to $message->author->getAvatarAttribute()) in the
MessageReceivedEvent class so static analysis recognizes the getter.
| if ($metadata !== []) { | ||
| $identity->update([ | ||
| 'metadata' => array_merge($identity->metadata ?? [], $metadata), | ||
| ]); |
There was a problem hiding this comment.
Skip no-op metadata updates.
Lines 37-40 execute an UPDATE on every message whenever any metadata exists, even if username / email / avatar did not change. This action runs on the message-ingestion hot path, so the extra writes will churn updated_at and amplify DB load for active users. Compare the merged payload with $identity->metadata first and only update when it actually changes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php`
around lines 37 - 40, The current code always calls $identity->update when
$metadata is non-empty which causes no-op DB writes; compute $merged =
array_merge($identity->metadata ?? [], $metadata) and compare it to
$identity->metadata (strict equality) and only call
$identity->update(['metadata' => $merged]) if $merged !== ($identity->metadata
?? []); this ensures ResolveExternalIdentity only updates the DB when the
metadata actually changes.
| $botToken = config('discord.token') ?? env('HE4RT_DISCORD_BOT_KEY'); | ||
| $guildId = env('HE4RT_DISCORD_GUILD'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php"
echo "== Target file =="
ls -l "$FILE" || true
sed -n '1,120p' "$FILE" | nl -ba | sed -n '15,70p'
echo
echo "== Search for discord config keys =="
rg -n "config\('discord\.(token|guild)" -S . || true
rg -n "discord\.token|HE4RT_DISCORD_(BOT_KEY|GUILD)" -S . || true
echo
echo "== Locate discord config file =="
# Try common config locations, but without assuming too much: find any config file referencing discord.*
fd -t f "discord*.php" config || true
fd -t f "*discord*" config || true
echo
echo "== Print any likely discord config =="
# Print files that mention HE4RT_DISCORD or "discord.token"
fd -t f "*discord*.php" config -x sh -c 'rg -n "HE4RT_DISCORD_|discord\.token|token" "$1" && echo "---" && sed -n "1,200p" "$1"' sh {} || true
echo
echo "== Check NewMessage::persist implementation (for other issues in scratchpad) =="
MSG="app-modules/activity/src/Message/Actions/NewMessage.php"
ls -l "$MSG" || true
sed -n '1,220p' "$MSG" | nl -ba | sed -n '1,220p'
echo
echo "== Look for avatar field handling in sync command =="
rg -n "\['avatar'\]|avatar" "$FILE" || trueRepository: he4rt/heartdevs.com
Length of output: 270
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php"
echo "== Show relevant lines (29-40) =="
sed -n '20,45p' "$FILE" | cat -n
echo
echo "== Search for config/env usage for discord token/guild =="
rg -n "config\('discord\.(token|guild)" -S . || true
rg -n "HE4RT_DISCORD_(BOT_KEY|GUILD)|discord\.token|HE4RT_DISCORD_GUILD" -S . || true
rg -n "env\('HE4RT_DISCORD" -S . || true
echo
echo "== Locate discord-related config files =="
# These commands may fail if fd isn't present; handle gracefully.
if command -v fd >/dev/null 2>&1; then
fd -t f "*discord*" config || true
fd -t f "discord*.php" config || true
else
echo "fd not available; skipping file discovery"
fi
echo
echo "== Find any config files that mention HE4RT_DISCORD or discord token keys =="
rg -n "HE4RT_DISCORD_|discord\.token|discord\.guild" -S config || true
rg -n "discord\.(token|guild)" -S . || true
echo
echo "== Inspect NewMessage::persist for exception swallowing / return type =="
MSG="app-modules/activity/src/Message/Actions/NewMessage.php"
ls -l "$MSG" || true
sed -n '1,260p' "$MSG" | cat -n
echo
echo "== Search persist() signature/usages =="
rg -n "function persist|persist\(" "$MSG" || true
rg -n "NewMessage::persist" -S . || true
echo
echo "== Search avatar handling in the sync command payload =="
sed -n '1,140p' "$FILE" | cat -n
rg -n "avatar" "$FILE" || trueRepository: he4rt/heartdevs.com
Length of output: 15012
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== config/discord.php =="
sed -n '1,120p' config/discord.php | cat -n
echo
echo "== config/he4rt.php (discord section) =="
rg -n "discord\s*=>" -n config/he4rt.php || true
sed -n '1,120p' config/he4rt.php | cat -n
sed -n '120,260p' config/he4rt.php | cat -n || true
echo
echo "== IntegrationDiscordServiceProvider (discord.token usage) =="
sed -n '1,120p' app-modules/integration-discord/src/IntegrationDiscordServiceProvider.php | cat -n
echo
echo "== SyncDiscordHistoryCommand full relevant section =="
sed -n '20,90p' app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php | cat -nRepository: he4rt/heartdevs.com
Length of output: 8671
Replace runtime env() reads with config() in SyncDiscordHistoryCommand
SyncDiscordHistoryCommand.php reads env('HE4RT_DISCORD_BOT_KEY') (via fallback) and env('HE4RT_DISCORD_GUILD') directly at runtime; switch to config-backed values instead—e.g. $botToken = config('discord.token') and $guildId = config('he4rt.discord.guild_id')—so behavior remains stable under Laravel config caching.
🧰 Tools
🪛 GitHub Check: Perform Phpstan Check / Run
[failure] 30-30:
Called 'env' outside of the config directory which returns null when the config is cached, use 'config'.
[failure] 29-29:
Called 'env' outside of the config directory which returns null when the config is cached, use 'config'.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php`
around lines 29 - 30, SyncDiscordHistoryCommand currently reads environment
variables at runtime; change the two places where $botToken and $guildId are set
to use config() instead of env() so config caching works. Replace the fallback
env('HE4RT_DISCORD_BOT_KEY') with a config key (e.g. config('discord.token'))
when assigning $botToken, and replace env('HE4RT_DISCORD_GUILD') with a config
lookup (e.g. config('he4rt.discord.guild_id')) when assigning $guildId; update
any references in the class to use these config-backed variables.
| try { | ||
| $newMessageAction->persist(new NewMessageDTO( | ||
| tenantId: $tenantProvider->tenant_id, | ||
| provider: IdentityProvider::Discord, | ||
| providerUsername: $author['username'], | ||
| externalAccountId: $author['id'], | ||
| providerMessageId: $m['id'], | ||
| channelId: $m['channel_id'], | ||
| content: $m['content'] ?? '', | ||
| sentAt: new DateTimeImmutable($m['timestamp']), | ||
| avatar: $author['avatar'] ?? null, | ||
| )); | ||
| $count++; | ||
| } catch (Throwable $e) { | ||
| // Silently skip individual message failures to keep progress | ||
| } |
There was a problem hiding this comment.
The success counter never sees persistence failures.
Lines 69-84 assume NewMessage::persist() will throw on write errors, but app-modules/activity/src/Message/Actions/NewMessage.php catches Throwable internally and only logs. That means $count++ still runs after failed writes and this command can report a successful sync after partial data loss. Return a success flag from persist() or use a non-swallowing path for batch imports.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php`
around lines 69 - 84, The success counter is incremented regardless because
NewMessageAction::persist() swallows exceptions; update the call site in
SyncDiscordHistoryCommand (the block creating NewMessageDTO and calling
$newMessageAction->persist(...)) to only increment $count when persist() returns
a success flag, and change NewMessageAction::persist() (in
app-modules/activity/src/Message/Actions/NewMessage.php) to return a boolean
(true on successful persistence, false or throw on failure) or rethrow failures
instead of swallowing them; then modify SyncDiscordHistoryCommand to check the
returned boolean (or catch rethrown exceptions) and only $count++ on
true/success so failed writes are not counted.
| ->when( | ||
| session('active_provider'), | ||
| fn (Builder $query, string $provider) => $query->where('provider', $provider) | ||
| ) |
There was a problem hiding this comment.
Apply the provider scope to global search as well.
getEloquentQuery() is now filtered by session('active_provider'), but getGlobalSearchEloquentQuery() still returns the parent query unchanged. That makes the table provider-scoped while Filament global search can still surface ExternalIdentity records from other providers, which breaks the new topbar scope contract. Please extract the scope into a shared helper and reuse it in both builders.
Also applies to: 79-84
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/ExternalIdentityResource.php`
around lines 69 - 72, getEloquentQuery is being scoped by
session('active_provider') but getGlobalSearchEloquentQuery is not, causing
global search to bypass the provider filter; extract the provider filtering into
a shared helper (e.g., private static function applyProviderScope(Builder
$query): Builder or a protected method) that applies the
session('active_provider') when present and then call that helper from both
getEloquentQuery() and getGlobalSearchEloquentQuery() (also update the same
pattern used around the other builder code at the region noted for lines 79-84)
so both the table and global search use the identical provider scope.
| <a | ||
| href="{{ route('oauth.redirect', ['tenant' => request()->getHost(), 'panel' => $panelId, 'provider' => 'discord']) }}" | ||
| class="group flex items-center gap-3 rounded-xl bg-[#5865F2] pl-2 pr-4 py-1.5 text-sm font-bold text-white shadow-lg shadow-[#5865F2]/20 transition-all hover:scale-[1.02] active:scale-[0.98]" | ||
| > | ||
| <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20 backdrop-blur"> | ||
| @svg ('fab-discord', 'h-5 w-5') | ||
| Continuar com Discord | ||
| </a> | ||
| </div> | ||
| <div class="text-left"> | ||
| <div class="text-[10px] font-semibold uppercase tracking-wider text-white/70">Discord</div> | ||
| <div class="text-[12px] leading-none">Continuar com Discord</div> | ||
| </div> | ||
| </a> | ||
|
|
||
| <a | ||
| href="{{ route('oauth.redirect', ['tenant' => request()->getHost(), 'panel' => 'app', 'provider' => 'github']) }}" | ||
| class="flex items-center justify-center gap-2.5 rounded-lg bg-zinc-800 px-4 py-2.5 text-sm font-medium text-white ring-1 ring-zinc-700 transition hover:bg-zinc-700" | ||
| > | ||
| <a | ||
| href="{{ route('oauth.redirect', ['tenant' => request()->getHost(), 'panel' => $panelId, 'provider' => 'github']) }}" | ||
| class="group flex items-center gap-3 rounded-xl bg-[#181717] pl-2 pr-4 py-1.5 text-sm font-bold text-white shadow-lg shadow-black/20 transition-all hover:scale-[1.02] active:scale-[0.98]" | ||
| > | ||
| <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20 backdrop-blur"> | ||
| @svg ('fab-github', 'h-5 w-5') | ||
| Continuar com GitHub | ||
| </a> | ||
| </div> | ||
| <div class="text-left"> | ||
| <div class="text-[10px] font-semibold uppercase tracking-wider text-white/70">GitHub</div> | ||
| <div class="text-[12px] leading-none">Continuar com GitHub</div> | ||
| </div> | ||
| </a> | ||
|
|
||
| <a | ||
| href="{{ route('oauth.redirect', ['tenant' => request()->getHost(), 'panel' => 'app', 'provider' => 'twitch']) }}" | ||
| class="flex items-center justify-center gap-2.5 rounded-lg bg-[#9146FF] px-4 py-2.5 text-sm font-medium text-white transition hover:bg-[#7B2FF0]" | ||
| > | ||
| <a | ||
| href="{{ route('oauth.redirect', ['tenant' => request()->getHost(), 'panel' => $panelId, 'provider' => 'twitch']) }}" | ||
| class="group flex items-center gap-3 rounded-xl bg-[#9146FF] pl-2 pr-4 py-1.5 text-sm font-bold text-white shadow-lg shadow-[#9146FF]/20 transition-all hover:scale-[1.02] active:scale-[0.98]" | ||
| > | ||
| <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20 backdrop-blur"> | ||
| @svg ('fab-twitch', 'h-5 w-5') | ||
| Continuar com Twitch | ||
| </a> | ||
| </div> | ||
|
|
||
| {{-- Divider --}} | ||
| <div class="relative my-6"> | ||
| <div class="absolute inset-0 flex items-center"> | ||
| <div class="w-full border-t border-zinc-800"></div> | ||
| </div> | ||
| <div class="relative flex justify-center text-xs uppercase"> | ||
| <span class="bg-gray-950 px-3 text-zinc-500">ou</span> | ||
| <div class="text-left"> | ||
| <div class="text-[10px] font-semibold uppercase tracking-wider text-white/70">Twitch</div> | ||
| <div class="text-[12px] leading-none">Continuar com Twitch</div> | ||
| </div> | ||
| </a> |
There was a problem hiding this comment.
OAuth is deriving the tenant from the host, not the panel's tenant key.
These links always send request()->getHost() as tenant, but app/Providers/Filament/AdminPanelProvider.php uses slug-based tenancy and app/Providers/Filament/AppPanelProvider.php does the same outside production. That means the redirect URL carries localhost/the domain instead of the tenant route key, so OAuth login will resolve the wrong tenant or 404 on admin and local/staging app panels. Build this parameter from the current tenant route key instead of the request host.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/panel-app/resources/views/auth/login.blade.php` around lines 91 -
128, The OAuth links in login.blade.php are passing request()->getHost() as the
'tenant' param to route('oauth.redirect'), causing wrong tenant resolution;
update each anchor href to use the current tenant's route key instead of
request()->getHost() (e.g., replace request()->getHost() with the current
tenant's route key retrieval such as tenant()->getRouteKey() or
currentTenant()->getRouteKey() depending on your tenant helper). Ensure all
three providers (Discord/GitHub/Twitch) that build the route in this file use
the tenant route key and keep panelId and provider unchanged.
| {{-- Filament email/password form --}} | ||
| <div class="fi-login-form animate-in fade-in slide-in-from-bottom-6 duration-1000"> | ||
| <div class="[&_label]:text-white [&_span]:text-white [&_a]:text-purple-400"> | ||
| {{ $this->form }} | ||
| </div> | ||
|
|
||
| {{-- Filament email/password form --}} | ||
| {{ $this->content }} | ||
| <div class="mt-8"> | ||
| @php | ||
| $action = $this->getAuthenticateFormAction(); | ||
| @endphp | ||
|
|
||
| <button | ||
| type="submit" | ||
| wire:click="authenticate" | ||
| wire:loading.attr="disabled" | ||
| class="relative flex h-[52px] w-full items-center justify-center rounded-xl bg-purple-600 px-4 text-sm font-bold text-white shadow-lg shadow-purple-900/20 transition-all hover:scale-[1.02] hover:bg-purple-500 active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed" | ||
| > | ||
| <div wire:loading.remove wire:target="authenticate"> | ||
| {{ $action->getLabel() }} | ||
| </div> | ||
| <div wire:loading wire:target="authenticate"> | ||
| <svg class="h-6 w-6 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | ||
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | ||
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | ||
| </svg> | ||
| </div> | ||
| </button> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/panel-app/resources/views/auth/login.blade.php"
echo "== File exists =="
ls -la "$FILE"
echo "== Relevant section (lines 110-220) =="
nl -ba "$FILE" | sed -n '110,220p'
echo "== Search for wire:submit in file =="
rg -n "wire:submit" "$FILE" || true
echo "== Search for authenticate usage in file =="
rg -n "authenticate" "$FILE" || trueRepository: he4rt/heartdevs.com
Length of output: 291
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/panel-app/resources/views/auth/login.blade.php"
echo "== File exists =="
ls -la "$FILE"
echo "== Relevant section (lines 110-220) =="
nl -ba "$FILE" | sed -n '110,220p'
echo "== Search for wire:submit in file =="
rg -n "wire:submit" "$FILE" || true
echo "== Search for authenticate usage in file =="
rg -n "authenticate" "$FILE" || trueRepository: he4rt/heartdevs.com
Length of output: 291
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/panel-app/resources/views/auth/login.blade.php"
echo "== Relevant section (approx lines 120-200) =="
awk 'NR>=120 && NR<=200 {printf "%5d\t%s\n", NR, $0}' "$FILE"
echo "== Search for wire:submit in file =="
rg -n "wire:submit" "$FILE" || true
echo "== Search for authenticate usage in file =="
rg -n "authenticate" "$FILE" || trueRepository: he4rt/heartdevs.com
Length of output: 3563
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/panel-app/resources/views/auth/login.blade.php"
echo "== File head (1-120) =="
awk 'NR>=1 && NR<=120 {printf "%5d\t%s\n", NR, $0}' "$FILE"
echo "== File tail (200-end) =="
awk 'NR>=200 {printf "%5d\t%s\n", NR, $0}' "$FILE"
echo "== Search for '<form' in file =="
rg -n "<form" "$FILE" || trueRepository: he4rt/heartdevs.com
Length of output: 9202
Restore a real form submit path.
This template has no <form> / wire:submit wrapper; authentication is wired only via wire:click="authenticate" on the button (type="submit" without a form). Pressing Enter in the password field won’t submit with normal semantics/accessibility.
Proposed fix
- <div class="fi-login-form animate-in fade-in slide-in-from-bottom-6 duration-1000">
+ <form wire:submit="authenticate" class="fi-login-form animate-in fade-in slide-in-from-bottom-6 duration-1000">
<div class="[&_label]:text-white [&_span]:text-white [&_a]:text-purple-400">
{{ $this->form }}
</div>
<div class="mt-8">
`@php`
$action = $this->getAuthenticateFormAction();
`@endphp`
<button
type="submit"
- wire:click="authenticate"
wire:loading.attr="disabled"
class="relative flex h-[52px] w-full items-center justify-center rounded-xl bg-purple-600 px-4 text-sm font-bold text-white shadow-lg shadow-purple-900/20 transition-all hover:scale-[1.02] hover:bg-purple-500 active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed"
>
<div wire:loading.remove wire:target="authenticate">
{{ $action->getLabel() }}
@@
- </div>
+ </form>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/panel-app/resources/views/auth/login.blade.php` around lines 141
- 167, The login form is missing a surrounding <form> element with wire:submit
to handle authentication properly, so pressing Enter won't work as expected.
Wrap the relevant inputs and the submit button inside a <form> tag, using
wire:submit.prevent="authenticate" for submission. Remove the
wire:click="authenticate" from the button since the form submit event will
handle it. This will restore proper semantic form behavior and accessibility.
| public function form(\Filament\Schemas\Schema $form): \Filament\Schemas\Schema | ||
| { | ||
| return $form | ||
| ->components([ | ||
| $this->getEmailFormComponent()->label('E-mail'), | ||
| $this->getPasswordFormComponent()->label('Senha'), | ||
| $this->getRememberFormComponent()->label('Lembrar de mim'), | ||
| ]) | ||
| ->statePath('data'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file
FILE="app-modules/panel-app/src/Pages/LoginPage.php"
if [ ! -f "$FILE" ]; then
echo "Missing $FILE"
exit 1
fi
echo "=== File head around form() and component getters ==="
# Print lines 40-120 with numbers
nl -ba "$FILE" | sed -n '40,130p'
echo "=== Extract method signatures for get*FormComponent() ==="
# Try to locate those methods within the file
rg -n "function get(Email|Password|Remember)FormComponent\s*\(" "$FILE" || true
# Print around each match (if any)
for name in Email Password Remember; do
line=$(rg -n "function get${name}FormComponent\s*\(" "$FILE" -n -o | head -n1 | cut -d: -f1 || true)
done
# Use sed to print entire file only if small, else just key parts
wc -l "$FILE"
echo "=== Show any phpdoc/return types for get*FormComponent() and their implementations ==="
# Print likely sections: from first occurrence of "getEmailFormComponent" up to end
start=$(rg -n "getEmailFormComponent" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${start:-}" ]; then
nl -ba "$FILE" | sed -n "${start},+250p"
else
echo "No getEmailFormComponent found in $FILE"
fi
echo "=== Search for getEmailFormComponent implementations/usages across repo ==="
rg -n "getEmailFormComponent|getPasswordFormComponent|getRememberFormComponent" app-modules/panel-app/src/Pages -S || true
echo "=== Search Filament schema component type imports used in LoginPage.php ==="
rg -n "use Filament\\\\Schemas" "$FILE" || true
rg -n "Filament\\\\Schemas\\\\Components\\\\Component" "$FILE" || trueRepository: he4rt/heartdevs.com
Length of output: 179
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/panel-app/src/Pages/LoginPage.php"
if [ ! -f "$FILE" ]; then
echo "Missing $FILE"
exit 1
fi
line_with_numbers() {
# awk equivalent of nl -ba
awk '{printf "%d:%s\n", NR, $0}'
}
echo "=== File: $FILE (total lines) ==="
wc -l "$FILE"
echo "=== Snippet around form() ==="
line_with_numbers < "$FILE" | sed -n '55,110p'
echo "=== Find get*FormComponent method signatures ==="
rg -n "function get(Email|Password|Remember)FormComponent\s*\(" "$FILE" || true
rg -n "get(Email|Password|Remember)FormComponent\s*\(" "$FILE" || true
echo "=== Show declared return types & implementations (up to next method) ==="
for m in Email Password Remember; do
# Print from method line to before next "public function" or "protected function" (heuristic)
start=$(rg -n "function get${m}FormComponent\s*\(" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${start:-}" ]; then
# take next 120 lines max
line_with_numbers < "$FILE" | sed -n "$start,$((start+140))p"
else
echo "No get${m}FormComponent() found"
fi
done
echo "=== Imports / type usage related to Filament schemas/components ==="
rg -n "^use Filament\\\\Schemas" "$FILE" || true
rg -n "Filament\\\\Schemas" "$FILE" || true
echo "=== Repo-wide occurrences of those getters (to infer return types) ==="
rg -n "get(Email|Password|Remember)FormComponent\s*\(" app-modules/panel-app/src -S || trueRepository: he4rt/heartdevs.com
Length of output: 2128
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app-modules/panel-app/src/Pages/LoginPage.php"
echo "=== Full file (numbered) ==="
awk '{printf "%d:%s\n", NR, $0}' "$FILE"
echo "=== Search for method definitions (not usages) in repo ==="
rg -n --hidden --no-ignore-vcs "function get(Email|Password|Remember)FormComponent\s*\(" app-modules -S || true
echo "=== Search for class declaration / extends in LoginPage.php ==="
rg -n "class LoginPage|extends|use " "$FILE" -S || trueRepository: he4rt/heartdevs.com
Length of output: 2481
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== composer.json dependencies (filament*) ==="
ls -la
rg -n "\"filament/" composer.json composer.lock **/composer.json **/composer.lock 2>/dev/null || true
# If composer.json exists, print relevant sections
if [ -f composer.json ]; then
echo "=== composer.json (filament sections) ==="
rg -n "\"filament/" -n composer.json || true
fi
# Search entire repo for filament/schemas usage to infer major version
echo "=== Search for Filament\\Schemas usage across repo ==="
rg -n "Filament\\\\Schemas" . -S || true
# Search for Filament v version in composer.lock (if present)
if [ -f composer.lock ]; then
echo "=== composer.lock (filament version lines) ==="
rg -n "\"name\": \"filament/" composer.lock || true
fi
# Also show the LoginPage inheritance path for context (only local files)
echo "=== Search for Filament\\Auth\\Pages\\Login subclass in repo ==="
rg -n "extends Login\\b" app-modules -S || trueRepository: he4rt/heartdevs.com
Length of output: 11425
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search stubs for getEmailFormComponent/getPasswordFormComponent/getRememberFormComponent ==="
rg -n "get(Email|Password|Remember)FormComponent\s*\(" stubs app phpstan* **/*.neon 2>/dev/null || true
echo "=== Search stubs for Filament\\\\Schemas\\\\Components\\\\Component::label ==="
rg -n "class Component|function label\s*\(" stubs 2>/dev/null || true
echo "=== Search for string 'Component::label' in repo ==="
rg -n "Components\\\\Component::label|Component::label" . -S 2>/dev/null || true
echo "=== Inspect phpstan modules/config possibly defining filament stubs ==="
ls -ლა
ls -la stubs || true
for f in phpstan.neon phpstan.modules.php phpstan.ignore.neon; do
if [ -f "$f" ]; then
echo "---- $f ----"
head -n 120 "$f"
fi
doneRepository: he4rt/heartdevs.com
Length of output: 744
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the local Login class that already uses the same pattern
FILE="app/Filament/Pages/Login.php"
if [ -f "$FILE" ]; then
echo "=== $FILE (total lines) ==="
wc -l "$FILE"
echo "=== $FILE head ==="
head -n 120 "$FILE" | cat
echo "=== Search for getEmailFormComponent/getPasswordFormComponent/getRememberFormComponent definitions ==="
rg -n "function get(Email|Password|Remember)FormComponent\s*\(" "$FILE" || true
echo "=== Search for their return types in this file ==="
rg -n "get(Email|Password|Remember)FormComponent\s*\):|get(Email|Password|Remember)FormComponent\s*\(" "$FILE" || true
else
echo "Missing $FILE"
fi
echo "=== Search repo for definitions of getEmailFormComponent ==="
rg -n "function getEmailFormComponent\s*\(" app app-modules stubs 2>/dev/null || true
echo "=== Search repo for definitions of getPasswordFormComponent ==="
rg -n "function getPasswordFormComponent\s*\(" app app-modules stubs 2>/dev/null || true
echo "=== Search repo for definitions of getRememberFormComponent ==="
rg -n "function getRememberFormComponent\s*\(" app app-modules stubs 2>/dev/null || true
echo "=== List stubs directory if it exists ==="
if [ -d "stubs" ]; then
ls -la stubs | head -n 50
else
echo "No stubs/ directory"
fiRepository: he4rt/heartdevs.com
Length of output: 2870
🌐 Web query:
Filament 5.6.5 Filament\Auth\Pages\Login getEmailFormComponent getPasswordFormComponent getRememberFormComponent return type label method
💡 Result:
In Filament 5.6.5, the class Filament\Auth\Pages\Login defines these form-component methods (for the built-in login form): 1) getEmailFormComponent - Return type: Component - What it returns: a TextInput::make('email') with label ('filament-panels::auth/pages/login.form.email.label') and typical email/required/autocomplete/autofocus settings.[1] 2) getPasswordFormComponent - Return type: Component - What it returns: a TextInput::make('password') with label ('filament-panels::auth/pages/login.form.password.label'), a conditional hint for password reset, and typical password/revealable/autocomplete/required settings.[1] 3) getRememberFormComponent - Return type: Component - What it returns: a Checkbox::make('remember') with label ('filament-panels::auth/pages/login.form.remember.label').[1]
Citations:
🌐 Web query:
Filament\Schemas\Components\Component label method exists
💡 Result:
Yes, the label method exists in Filament and is available for components [1][2][3]. It is provided by the HasLabel trait, which is used by various components and fields within the Filament framework [1][4][5][6]. You can use the label method to manually set or override the label of a field or component [2][3][7]. Key details regarding the label method: - Usage: It accepts a string, a translation string, or a closure to dynamically calculate the label [2][3][7]. - Functionality: If a label is not explicitly set, Filament typically determines it automatically based on the component's name [2][3][7]. - Related Methods: The HasLabel trait also provides related methods such as hiddenLabel to hide the label and translateLabel to automatically translate the label using Laravel's localization features [1][2][8][6]. The implementation can be found in the Filament source code within the HasLabel concern [1].
Citations:
- 1: https://github.com/filamentphp/filament/blob/3.x/packages/forms/src/Components/Concerns/HasLabel.php
- 2: https://filamentphp.com/docs/3.x/forms/fields/getting-started
- 3: https://github.com/filamentphp/filament/blob/ead6642f/packages/forms/docs/01-overview.md
- 4: https://github.com/filamentphp/filament/blob/ead6642f/packages/forms/src/Components/Field.php
- 5: https://github.com/filamentphp/filament/blob/ead6642f/packages/schemas/src/Components/Component.php
- 6: Adds 'translateLabel()' method to HasLabel concerns filamentphp/filament#3668
- 7: https://filamentphp.com/docs/4.x/forms/builder
- 8: https://filamentphp.com/docs/2.x/forms/fields
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the exact resolved version for filament/schemas and filament/forms/actions/support in composer.lock
for pkg in "filament/schemas" "filament/forms" "filament/support" "filament/filament"; do
echo "=== $pkg ==="
# Print the block around the first occurrence of the package name
rg -n "\"name\": \"$pkg\"" composer.lock | head -n 1 | cut -d: -f1 | {
read -r line || true
if [ -n "${line:-}" ]; then
start=$((line-5))
end=$((line+35))
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {print}' composer.lock | sed -n '1,80p'
fi
}
echo
doneRepository: he4rt/heartdevs.com
Length of output: 6479
🌐 Web query:
filament v5.6.5 packages/schemas/src/Components/Component.php label()
💡 Result:
In Filament, the label method is used to define or override the label for a component [1][2][3]. While the base Component class [4][5] provides the structural foundation for all schema-based UI elements, the label method itself is typically implemented via the HasLabel trait [6], which is commonly included in fields and other components (such as those found in packages/forms/src/Components/Field.php) [7][6]. The label method accepts a static string, a translation string, or a closure for dynamic label generation [1][8][3]. Usage example: use Filament\Forms\Components\TextInput; // Static label TextInput::make('name') ->label('Full name'); // Translation string TextInput::make('name') ->label(('fields.name')); // Dynamic label using a closure TextInput::make('name') ->label(fn => 'Dynamic Label'); If you are developing a custom component, ensure your class uses the HasLabel trait to gain access to the label and getLabel methods [6]. Note that to hide a label while remaining accessible to screen readers, you should use the hiddenLabel method instead of providing an empty string to label [8].
Citations:
- 1: https://filamentphp.com/docs/4.x/forms/overview
- 2: https://filamentphp.com/docs/3.x/forms/fields/getting-started
- 3: https://filamentphp.com/docs/4.x/forms/builder
- 4: https://github.com/filamentphp/filament/blob/ead6642f/packages/schemas/src/Components/Component.php
- 5: https://deepwiki.com/filamentphp/filament/7.1-schema-component-model
- 6: https://filamentphp.com/api/3.x/Filament/Forms/Components/Concerns/HasLabel.html
- 7: https://github.com/filamentphp/filament/blob/ead6642f/packages/forms/src/Components/Field.php
- 8: https://github.com/filamentphp/filament/blob/ead6642f/packages/forms/docs/01-overview.md
🌐 Web query:
filament v5.6.5 Filament\Auth\Pages\Login getEmailFormComponent return type
💡 Result:
In Filament, the getEmailFormComponent() method returns an instance of Filament\Forms\Components\Component (or, more specifically, the Filament\Forms\Components\TextInput class which extends Component) [1][2]. This method is commonly overridden when customizing authentication pages to modify the behavior or appearance of the email input field [3][4][5]. When overriding this method in your own class (e.g., a custom Login page extending Filament\Pages\Auth\Login), you should ensure your implementation also returns a Component instance [1][3][6].
Citations:
- 1: https://github.com/filamentphp/filament/blob/ead6642f/packages/panels/src/Auth/Pages/Login.php
- 2: https://github.com/filamentphp/filament/blob/4.x/packages/panels/src/Auth/Pages/Login.php
- 3: Implement Cloudflair Turnstile in the CustomLogin.php filamentphp/filament#18803
- 4: Need help on overriding login behaviour filamentphp/filament#8260
- 5: https://www.answeroverflow.com/m/1163830928167145546
- 6: https://notes.gowdhamp.in/posts/filamentphp-custom-login/
🌐 Web query:
filament v5.6.5 packages/schemas/src/Components/Component.php use HasLabel
💡 Result:
In Filament v5.6.5, the base Component class located at packages/schemas/src/Components/Component.php does not use the HasLabel trait [1]. While many specific components in Filament (such as Section, Tabs, or various form fields) utilize HasLabel to provide labeling functionality [2][3][4], it is not included in the core Component class itself [1]. The HasLabel trait is typically implemented by concrete components that require a label, allowing them to provide methods such as label(), getLabel(), and hiddenLabel() [5][6]. For custom components, if you require labeling support, you must explicitly import and use the HasLabel trait within your component class: use Filament\Schemas\Components\Concerns\HasLabel; class MyCustomComponent extends Component { use HasLabel; //... }
Citations:
- 1: https://github.com/filamentphp/filament/blob/ead6642f/packages/schemas/src/Components/Component.php
- 2: https://github.com/filamentphp/filament/blob/ead6642f/packages/schemas/src/Components/Section.php
- 3: https://github.com/filamentphp/filament/blob/ead6642f/packages/forms/src/Components/Field.php
- 4: https://github.com/filamentphp/filament/blob/ead6642f/packages/schemas/src/Components/Tabs.php
- 5: https://github.com/filamentphp/filament/blob/3.x/packages/forms/src/Components/Concerns/HasLabel.php
- 6: https://filamentphp.com/api/3.x/Filament/Forms/Components/Concerns/HasLabel.html
Fix phpstan failure in LoginPage::form() from calling ->label() on the schema base Component
getEmailFormComponent(), getPasswordFormComponent(), and getRememberFormComponent() are typed to return \Filament\Schemas\Components\Component (in Filament v5.6.5, that base Component doesn’t provide label()—it exists only on specific components that use HasLabel). The ->label(...) chains in form() (calls at lines 71-73) therefore keep breaking static analysis.
Relabel by overriding those getters with their concrete component return types (e.g., the email/password fields and remember checkbox) and call ->label() inside the getter overrides, rather than chaining ->label() from the Component return type.
🧰 Tools
🪛 GitHub Check: Perform Phpstan Check / Run
[failure] 73-73:
Call to an undefined method Filament\Schemas\Components\Component::label().
[failure] 72-72:
Call to an undefined method Filament\Schemas\Components\Component::label().
[failure] 71-71:
Call to an undefined method Filament\Schemas\Components\Component::label().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/panel-app/src/Pages/LoginPage.php` around lines 67 - 75, The
phpstan failure is caused by calling ->label() on methods that are typed to the
base \Filament\Schemas\Components\Component; update getEmailFormComponent(),
getPasswordFormComponent(), and getRememberFormComponent() to return concrete
component classes (e.g., \Filament\Forms\Components\TextInput for email, a
password-capable TextInput or specific Password component for password, and
\Filament\Forms\Components\Checkbox for the remember field) and move the
->label('...') calls into those getter implementations (so each getter returns
the labeled concrete component), then leave LoginPage::form() to only assemble
the returned components without chaining ->label().
| .fi-topbar { | ||
| @apply !bg-white/80 dark:!bg-zinc-950/80 !backdrop-blur-xl !border-b !border-slate-200/70 dark:!border-white/5 !sticky !top-0 !z-30 !shadow-sm; | ||
| height: var(--topbar-height) !important; | ||
| } |
There was a problem hiding this comment.
Fix the current Stylelint violations.
Lines 26, 75, and 93 violate declaration-empty-line-before, so this stylesheet will fail the existing lint rules until those blank lines are added.
Also applies to: 73-76, 91-94
🧰 Tools
🪛 Stylelint (17.12.0)
[error] 26-26: Expected empty line before declaration (declaration-empty-line-before)
(declaration-empty-line-before)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@resources/css/filament/admin/theme.css` around lines 24 - 27, The stylesheet
is failing stylelint's declaration-empty-line-before rule; add a single blank
line before the affected declaration blocks so the rule is
satisfied—specifically insert an empty line before the declaration inside the
.fi-topbar rule and likewise in the other affected selector blocks referenced
around the ranges 73-76 and 91-94; keep existing properties and !important flags
unchanged, just ensure a blank line immediately before each offending
declaration to satisfy declaration-empty-line-before.
| $providers = \He4rt\Identity\ExternalIdentity\Enums\IdentityProvider::supportedProviders(); | ||
| $activeProviderValue = session('active_provider', \He4rt\Identity\ExternalIdentity\Enums\IdentityProvider::Discord->value); | ||
| $currentProvider = \He4rt\Identity\ExternalIdentity\Enums\IdentityProvider::tryFrom($activeProviderValue) ?? $providers[1]; |
There was a problem hiding this comment.
Use an explicit enum fallback instead of $providers[1].
If session('active_provider') is stale or invalid, tryFrom() returns null and this fallback becomes fragile: it can hit an undefined offset or silently pick the wrong provider if supportedProviders() is reordered. Falling back to a concrete enum like IdentityProvider::Discord is safer and matches the middleware default.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@resources/views/filament/admin/components/topbar-tenant-menu.blade.php`
around lines 6 - 8, The current fallback for $currentProvider uses
$providers[1], which is fragile; change the fallback to a concrete enum value
(e.g. \He4rt\Identity\ExternalIdentity\Enums\IdentityProvider::Discord) so that
when IdentityProvider::tryFrom($activeProviderValue) returns null you explicitly
default to IdentityProvider::Discord; update the assignment for $currentProvider
(the line referencing IdentityProvider::tryFrom and $providers[1]) to use the
explicit enum fallback and ensure session default (session('active_provider',
...)) remains consistent with that same enum.
Registro de Alterações - 28/05/2026 a 02/06/2026
🚀 Issue: Redesign de Identidade Visual, Evolução do Login e Ferramentas de Marketing
💬 Resumo das Solicitações (Prompts do Terminal)
sync-historypara buscar dados históricos do Discord.💎 Entregas Técnicas
UI/UX, Design & Branding
Dados, Bot & Backend
php artisan discord:sync-historypermitindo o backfill de mensagens e perfis (incluindo avatares) de canais específicos.ResolveExternalIdentitypara persistir metadados ricos do Discord e atualizar perfis automaticamente em cada interação.Infraestrutura & Estabilidade
📂 Arquivos Modificados/Criados
app/Providers/Filament/AppPanelProvider.phpapp/Providers/Filament/AdminPanelProvider.phpapp-modules/activity/src/Message/Actions/NewMessage.phpapp-modules/activity/src/Message/DTOs/NewMessageDTO.phpapp-modules/bot-discord/src/Events/MessageReceivedEvent.phpapp-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.phpapp-modules/integration-discord/src/IntegrationDiscordServiceProvider.phpapp-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.phpapp-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.phpapp-modules/panel-admin/src/Http/Middleware/SetProviderScope.phpapp-modules/panel-app/resources/views/auth/login.blade.phpapp-modules/panel-app/src/Pages/LoginPage.phpapp/Filament/Pages/Login.phpresources/css/filament/app/theme.cssresources/css/filament/admin/theme.cssresources/views/filament/admin/components/brand.blade.phpresources/views/filament/admin/components/topbar-content.blade.phpresources/views/filament/admin/components/topbar-tenant-menu.blade.phpapp-modules/he4rt/resources/views/components/base/logo.blade.phpDescription
This PR implements He4rt's visual branding overhaul, enhances the Discord bot integration with message synchronization capabilities, and improves the admin panel with tenant/provider scoping. Key additions include a new artisan command to backfill Discord message history, metadata enrichment for external identities (avatars and usernames), redesigned authentication pages with glassmorphism styling, and tenant-aware provider filtering across the admin panel.
References
Related to PR
#301targeting 4.x branch. Context from feat/branding-and-bot-enhancements feature branch.Dependencies & Requirements
php artisan discord:sync-historyfor backfilling Discord messagesconfig('app.display_timezone')with fallback toAmerica/Sao_PauloContributor Summary
Changes Summary