Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
01a4ee5
docs(live): spec da pesquisa de viabilidade e do MVP de live streaming
Clintonrocha98 Aug 29, 2026
34c1c57
docs(live): plano de implementação do MVP
Clintonrocha98 Aug 29, 2026
b53edb7
feat(live): serviço mediamtx no ambiente docker local
Clintonrocha98 Aug 29, 2026
f928f39
feat(live): módulo com auth hook do mediamtx e consulta de status
Clintonrocha98 Aug 29, 2026
dd9376f
feat(portal): página /live com player HLS e polling de status
Clintonrocha98 Aug 29, 2026
7ad2965
fix(portal): move a diretiva @vite para dentro do main da página de live
Clintonrocha98 Aug 29, 2026
153e457
fix(live): nega ações desconhecidas no hook e protege o parse do read…
Clintonrocha98 Aug 29, 2026
8da7d99
fix(live): restringe a Control API ao localhost e documenta o bind do…
Clintonrocha98 Aug 29, 2026
b672f8e
chore(portal): atualiza o lock com a dependência he4rt/live e aria-li…
Clintonrocha98 Aug 29, 2026
c0cc7e3
docs(live): front matter no plano do MVP
Clintonrocha98 Aug 29, 2026
c7ae713
docs(live): spec da fase 2 — chat via Reverb, admin e robustez
Clintonrocha98 Aug 29, 2026
4ff388d
docs(live): plano de implementação da fase 2
Clintonrocha98 Aug 29, 2026
1e5704c
feat(identity): provider He4rtLives para atividade nativa da plataforma
Clintonrocha98 Aug 29, 2026
6d099f1
fix(identity): braços explícitos do He4rtLives nos matches do provider
Clintonrocha98 Aug 29, 2026
8185e25
feat(live): model Live com ciclo de vida e stream key por live
Clintonrocha98 Aug 29, 2026
5e972c4
feat(live): ingest contra o banco, webhook de sinal e cache do status
Clintonrocha98 Aug 29, 2026
90d5129
fix(activity): retrospectiva do Discord filtra provider e enum de mod…
Clintonrocha98 Aug 29, 2026
e4cb03c
feat(live): chat de domínio persistido no pipeline de atividade
Clintonrocha98 Aug 29, 2026
446a1c9
feat(live): audiência com heartbeat, pico e amostras por minuto
Clintonrocha98 Aug 29, 2026
67eeae9
chore(portal): laravel reverb e echo como infraestrutura de broadcast
Clintonrocha98 Aug 29, 2026
4534967
feat(portal): chat em tempo real, heartbeat de audiência e player res…
Clintonrocha98 Aug 29, 2026
65b1139
chore(portal): declara dependências de activity e identity no módulo
Clintonrocha98 Aug 29, 2026
e3c4313
feat(panel-admin): gestão de lives com moderação de chat e gráfico de…
Clintonrocha98 Aug 29, 2026
5ef1ac8
fix(panel-admin): entrada de navegação para o resource de lives
Clintonrocha98 Aug 29, 2026
5e612fc
feat(live): webhook de sinal do mediamtx e cors do hls pinado ao portal
Clintonrocha98 Aug 29, 2026
2b971cc
fix(live): broadcast e heartbeat degradam graciosamente sem reverb ou…
Clintonrocha98 Aug 29, 2026
ad78ba7
chore(live): bind 0.0.0.0 no serve do composer dev para o ingest local
Clintonrocha98 Aug 29, 2026
e66831b
feat(panel-admin): ingest da live nos dois campos que o obs espera
Clintonrocha98 Aug 29, 2026
5beca44
docs(live): context do módulo com glossário e fronteiras
Clintonrocha98 Aug 29, 2026
b8a78b3
feat(live): comando dev de simulação de comentários de chat
Clintonrocha98 Aug 30, 2026
4126977
feat(panel-admin): action para ligar/desligar simulação de chat da live
Clintonrocha98 Aug 30, 2026
62213b3
feat(portal): modo teatro e chat recolhível na página de live
Clintonrocha98 Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ai/guidelines/workflow/02-triage-labels.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
| `mod:integration-devto` | `integration-devto` | Dev.to integration |
| `mod:integration-discord`| `integration-discord` | Discord OAuth/API |
| `mod:integration-twitch` | `integration-twitch` | Twitch integration |
| `mod:live` | `live` | Live streaming |
| `mod:marketing` | `marketing` | Encurtador, campanhas |
| `mod:moderation` | `moderation` | Moderation pipeline |
| `mod:panel-admin` | `panel-admin` | Admin Filament panel |
Expand Down
23 changes: 22 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

BROADCAST_CONNECTION=null
BROADCAST_CONNECTION=reverb
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync

Expand Down Expand Up @@ -90,3 +90,24 @@ GITHUB_API_TOKEN=
# Dev.to — slug da organização cujos artigos o contents:sync-articles busca.
# A chave de API de cada pessoa é conectada pelo painel (/app/profile → Conexões), não aqui.
DEVTO_ORG_SLUG=he4rt

# Live streaming (mediamtx local — ver docs/specs/2026-08-29-live-mvp-design.md)
# Para publicar (OBS) o container mediamtx precisa alcançar o Laravel via
# host.docker.internal: rode `php artisan serve --host=0.0.0.0` (o bind padrão
# 127.0.0.1 não é alcançável a partir do container).
LIVE_WEBHOOK_SECRET=
LIVE_RTMP_SERVER=rtmp://localhost:1935
LIVE_HLS_URL=http://localhost:8888/live/index.m3u8
LIVE_CONTROL_API_URL=http://localhost:9997

REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http

VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
30 changes: 16 additions & 14 deletions CONTEXT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@ This is a modular monorepo (`internachi/modular`). Each bounded context lives un

## Contexts

| Context | Path | Description |
| ------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Moderation | `app-modules/moderation/` | Content moderation pipeline — classification, routing, enforcement, appeals |
| Bot Discord | `app-modules/bot-discord/` | Discord bot runtime (Laracord websocket, slash commands, event handlers) |
| Integration Discord | `app-modules/integration-discord/` | Discord platform transport (REST API via Saloon), OAuth, ETL |
| Identity | `app-modules/identity/` | Users, tenants, external identities, authentication |
| Events | `app-modules/events/` | Event participation lifecycle — enrollment, check-in, attendance, XP dispatch |
| Gamification | `app-modules/gamification/` | Character progression — XP, levels, badges, seasons, daily bonuses |
| Panel Admin | `app-modules/panel-admin/` | Filament admin panel — dashboards, resources, moderation UI, marketing |
| Integration Twitch | `app-modules/integration-twitch/` | Twitch platform transport (Helix API via Saloon), OAuth, EventSub webhooks |
| Integration GitHub | `app-modules/integration-github/` | GitHub transport (REST via Saloon), OAuth, community contribution ingestion (backfill + webhooks) + event lake |
| Onboarding | `app-modules/onboarding/` | Universal, mandatory entry layer — polymorphic onboarding state machines by type; owns the per-type completion gate (APTO) |
| Squads | `app-modules/squads/` | Squad lifecycle, membership and governance (captain/sub-captain, elections, removal, reallocation) |
| Marketing | `app-modules/marketing/` | Divulgação e sua medição — links curtos (`/l/{slug}`), destino versionado, captura crua de cliques |
| Context | Path | Description |
| ------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Moderation | `app-modules/moderation/` | Content moderation pipeline — classification, routing, enforcement, appeals |
| Bot Discord | `app-modules/bot-discord/` | Discord bot runtime (Laracord websocket, slash commands, event handlers) |
| Integration Discord | `app-modules/integration-discord/` | Discord platform transport (REST API via Saloon), OAuth, ETL |
| Identity | `app-modules/identity/` | Users, tenants, external identities, authentication |
| Events | `app-modules/events/` | Event participation lifecycle — enrollment, check-in, attendance, XP dispatch |
| Gamification | `app-modules/gamification/` | Character progression — XP, levels, badges, seasons, daily bonuses |
| Panel Admin | `app-modules/panel-admin/` | Filament admin panel — dashboards, resources, moderation UI, marketing |
| Integration Twitch | `app-modules/integration-twitch/` | Twitch platform transport (Helix API via Saloon), OAuth, EventSub webhooks |
| Integration GitHub | `app-modules/integration-github/` | GitHub transport (REST via Saloon), OAuth, community contribution ingestion (backfill + webhooks) + event lake |
| Onboarding | `app-modules/onboarding/` | Universal, mandatory entry layer — polymorphic onboarding state machines by type; owns the per-type completion gate (APTO) |
| Squads | `app-modules/squads/` | Squad lifecycle, membership and governance (captain/sub-captain, elections, removal, reallocation) |
| Marketing | `app-modules/marketing/` | Divulgação e sua medição — links curtos (`/l/{slug}`), destino versionado, captura crua de cliques |
| Contents | `app-modules/contents/` | Canonical catalogue of content published on external platforms (articles now, video next) — delegated types + provider contract |
| Live | `app-modules/live/` | Transmissões ao vivo da plataforma — ciclo de vida da live, stream keys, autorização do mediamtx, chat e audiência |

## Relationships

Expand Down Expand Up @@ -75,3 +76,4 @@ This is a modular monorepo (`internachi/modular`). Each bounded context lives un
- **Squads** depends on Onboarding (reads the `Squads`-completion gate, "APTO") and Identity (users/tenants). It never imports from presentation; the panels depend on it.
- **Marketing** owns short links and their click record. It depends only on **Identity** (`created_by` / `user_id` on a click) and on no other context. It renders no UI and registers no route: `portal` owns the public `/l/{slug}` edge and the "link unavailable" page, and `panel-admin` owns the staff CRUD and dashboards — both depend on `marketing`, never the reverse.
- **Contents** owns the canonical record of externally published content. It never talks HTTP (the `integration-*` modules implement its provider contract) and never awards anything — it emits `ArticlePublished` and `activity` decides. It depends on Identity (authorship resolution, `ExternalIdentityConnected`); `activity` and the panels depend on it, never the reverse.
- **Live** owns the live-broadcast lifecycle, stream keys, media-server authorization and audience. It depends on Identity (`He4rtLives` synthetic identities via `ResolveExternalIdentity`) and Activity (chat messages persist as `Message` rows, moderation trail as `moderation_events`) — domain→domain, sanctioned by the phase-2 spec. It renders no UI: `portal` owns the public `/live` page and `panel-admin` owns management, both depending on `live`, never the reverse. The mediamtx media server is infra (docker), not a module; it talks to `live` only via the auth hook and the signal webhook.
4 changes: 3 additions & 1 deletion app-modules/activity/src/Moderation/Enums/ModerationType.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum ModerationType: string implements HasColor, HasLabel
case Warn = 'warn';
case Kick = 'kick';
case Suspension = 'suspension';
case MessageDeleted = 'message_deleted';

public function getLabel(): string
{
Expand All @@ -30,6 +31,7 @@ public function getLabel(): string
self::Warn => 'Warning',
self::Kick => 'Kick',
self::Suspension => 'Suspension',
self::MessageDeleted => 'Mensagem apagada',
};
}

Expand All @@ -39,7 +41,7 @@ public function getColor(): string
self::Ban, self::Kick => 'danger',
self::Unban, self::Unmute => 'success',
self::Mute, self::Suspension => 'warning',
self::Warn => 'info',
self::Warn, self::MessageDeleted => 'info',
};
}
}
5 changes: 4 additions & 1 deletion app-modules/activity/src/Retrospective/DiscordSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,9 @@ private function filtersKey(SourceFilters $filters): string
* Base de mensagens do recorte. hideBots derruba source_kind='bot' mas mantém
* linhas históricas com source_kind nulo. As exclusions entram aqui (e não na
* composição) porque mexem no dado: o que é excluído some dos slides e também
* dos números (ADR-0001).
* dos números (ADR-0001). Filtra pelo provider da identidade porque messages
* agora também guarda o chat das lives (IdentityProvider::He4rtLives), que não
* é Discord e não pode inflar a retrospectiva dele.
*
* @return Builder<Message>
*/
Expand All @@ -393,6 +395,7 @@ private function messages(Period $period, SourceFilters $filters): Builder

return Message::query()
->whereBetween('sent_at', [$period->since, $period->until])
->whereHas('provider', fn (Builder $query): Builder => $query->where('provider', IdentityProvider::Discord))
->when(
$filters->hideBots,
fn (Builder $query): Builder => $query->where(function (Builder $inner): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

use Carbon\CarbonImmutable;
use He4rt\Activity\Message\Models\Message;
use He4rt\Activity\Retrospective\DiscordSource;
use He4rt\Community\Retrospective\DTOs\Period;
use He4rt\Community\Retrospective\DTOs\SourceFilters;
use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider;
use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity;

it('não conta mensagens do chat da live na retrospectiva do Discord', function (): void {
$discordIdentity = ExternalIdentity::factory()->create(['provider' => IdentityProvider::Discord]);
$liveIdentity = ExternalIdentity::factory()->create(['provider' => IdentityProvider::He4rtLives]);

Message::factory()->for($discordIdentity, 'provider')->create(['sent_at' => now()]);
Message::factory()->for($liveIdentity, 'provider')->create(['sent_at' => now()]);

$since = CarbonImmutable::now()->subDay();
$until = CarbonImmutable::now()->addDay();

$result = new DiscordSource()->collect(
Period::of($since, $until),
new SourceFilters(),
);

$messages = [];
foreach ($result->slides as $slide) {
if ($slide->kind() === 'discord.messages') {
$messages = $slide->toArray();
}
}

expect($messages['total'] ?? 0)->toBe(1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use He4rt\Community\Retrospective\DTOs\SourceFilters;
use He4rt\Community\Retrospective\DTOs\SourceResult;
use He4rt\Community\Retrospective\Enums\ExclusionKind;
use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider;
use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity;

beforeEach(function (): void {
Expand Down Expand Up @@ -40,7 +41,10 @@ function dcSlide(SourceResult $result, string $kind): array

function dcIdentity(string $name): ExternalIdentity
{
return ExternalIdentity::factory()->create(['metadata' => ['username' => $name]]);
return ExternalIdentity::factory()->create([
'provider' => IdentityProvider::Discord,
'metadata' => ['username' => $name],
]);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum IdentityProvider: string implements HasColor, HasDescription, HasIcon, HasL
case Spotify = 'spotify';
case Steam = 'steam';
case GitHub = 'github';
case He4rtLives = 'he4rt-lives';
case Xbox = 'xbox';
case YouTube = 'youtube';
case Twitter = 'twitter';
Expand Down Expand Up @@ -94,6 +95,7 @@ public function getCredentialsType(): CredentialsType
{
return match ($this) {
self::DevTo => CredentialsType::ApiKey,
self::He4rtLives => CredentialsType::OAuth2,
default => CredentialsType::OAuth2,
};
}
Expand All @@ -102,6 +104,7 @@ public function getApiKeyClient(): ?ApiKeyClientContract
{
return match ($this) {
self::DevTo => resolve(DevToApiKeyClient::class),
self::He4rtLives => null,
default => null,
};
}
Expand All @@ -113,6 +116,7 @@ public function getClient(): ?OAuthClientContract
self::Discord => resolve(DiscordOAuthClient::class),
self::GitHub => resolve(GitHubOAuthClient::class),
self::DevTo => resolve(DevToOAuthClient::class),
self::He4rtLives => null,
default => null,
};
}
Expand All @@ -126,6 +130,7 @@ public function getColor(): array
self::Spotify => Color::hex('#1DB954'),
self::Steam => Color::Slate,
self::GitHub => Color::Neutral,
self::He4rtLives => Color::Rose,
self::Xbox => Color::Green,
self::YouTube => Color::Red,
self::Twitter => Color::Sky,
Expand Down Expand Up @@ -159,6 +164,7 @@ public function getIcon(): string
self::Spotify => 'fab-spotify',
self::Steam => 'fab-steam',
self::GitHub => 'fab-github',
self::He4rtLives => 'heroicon-o-video-camera',
self::Xbox => 'fab-xbox',
self::YouTube => 'fab-youtube',
self::Twitter => 'fab-twitter',
Expand Down Expand Up @@ -197,6 +203,7 @@ public function getDescription(): string
self::Spotify => 'Conecte sua conta do Spotify para rastrear músicas ouvidas.',
self::Steam => 'Conecte sua conta da Steam para rastrear jogos.',
self::GitHub => 'Conecte sua conta do GitHub para rastrear contribuições.',
self::He4rtLives => 'Identidade nativa da plataforma he4rt, usada pelo chat das lives.',
self::Xbox => 'Conecte sua conta do Xbox para gameficações.',
self::YouTube => 'Conecte sua conta do YouTube para rastrear vídeos.',
self::Twitter => 'Conecte sua conta do Twitter/X para interações sociais.',
Expand Down Expand Up @@ -229,6 +236,7 @@ public function getScopes(?string $panel = null): array
$scopes = match ($this) {
self::Discord => config('services.discord.scopes'),
self::Twitch => config('services.twitch.scopes.'.($panel ?? 'app'), config('services.twitch.scopes.app')),
self::He4rtLives => '',
default => '',
};

Expand All @@ -251,6 +259,7 @@ public function getRedirectUri(): string
public function getType(): IdentityType
{
return match ($this) {
self::He4rtLives => IdentityType::External,
default => IdentityType::External,
};
}
Expand All @@ -259,6 +268,7 @@ public function getMessageAdapter(): ?MessageActivityAdapter
{
return match ($this) {
self::Discord => resolve(DiscordMessageAdapter::class),
self::He4rtLives => null,
default => null,
};
}
Expand Down
26 changes: 26 additions & 0 deletions app-modules/identity/tests/Feature/He4rtLivesProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

use He4rt\Identity\ExternalIdentity\Enums\CredentialsType;
use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider;
use He4rt\Identity\ExternalIdentity\Enums\IdentityType;

it('expõe o provider da plataforma de lives da he4rt', function (): void {
$provider = IdentityProvider::from('he4rt-lives');

expect($provider)->toBe(IdentityProvider::He4rtLives)
->and($provider->getLabel())->not->toBeEmpty()
->and($provider->getColor())->not->toBeNull();
});

it('resolve o braço explícito de He4rtLives em cada match do provider', function (): void {
$provider = IdentityProvider::He4rtLives;

expect($provider->getType())->toBe(IdentityType::External)
->and($provider->getCredentialsType())->toBe(CredentialsType::OAuth2)
->and($provider->getApiKeyClient())->toBeNull()
->and($provider->getClient())->toBeNull()
->and($provider->getScopes())->toBeEmpty()
->and($provider->getMessageAdapter())->toBeNull();
});
Loading