Skip to content

Commit 9808bee

Browse files
committed
Refactor: broadcasting + APP_PORT env added
1 parent 8bf15e4 commit 9808bee

8 files changed

Lines changed: 81 additions & 45 deletions

File tree

.docker/deploy

.docker/scripts/staging.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ echo -e "${BLUE}📝 Setting up environment...${NC}"
2828
# 3. Start dev services (without assets container)
2929
echo -e "${BLUE}🐳 Starting containers...${NC}"
3030
export COMPOSE_PROJECT_NAME=dev
31-
"$SCRIPTS_DIR/compose.sh" up php nginx mysql mailhog --build -d
31+
"$SCRIPTS_DIR/compose.sh" up php nginx mysql mailhog reverb --build -d
3232

3333
# 4. Wait for services to be healthy
3434
echo -e "${BLUE}⏳ Waiting for database...${NC}"

.env.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ APP_KEY=
44
APP_DEBUG=true
55
APP_TIMEZONE=UTC
66
APP_URL=http://localhost:8000
7+
APP_PORT=8000
78

89
APP_LOCALE=es_UY
910
APP_FALLBACK_LOCALE=en

documentation/ai/KNOWLEDGE_BASE.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# MTAV - Core Knowledge Base
22

3-
**Last Updated:** 2025-11-20
3+
**Last Updated:** 2025-01-27
44

55
This document contains the essential architectural principles, patterns, and constraints needed for daily development work on MTAV.
66

@@ -58,7 +58,15 @@ This document contains the essential architectural principles, patterns, and con
5858
- **Admin/Member are mutually exclusive** - cannot be both
5959
- **Project-based scoping** - most models automatically scoped to current project context
6060

61-
**Reference**: See `policies-reference.md` for detailed authorization patterns.
61+
**User Type Implementation**: Uses a **pseudo-STI pattern** (not true Single Table Inheritance):
62+
- `Admin` and `Member` classes extend `User` but use **global scopes** to filter queries
63+
- `Admin` scope: `where('is_admin', true)`
64+
- `Member` scope: `where('is_admin', false)->whereNotNull('family_id')`
65+
- No `type` discriminator column - type determined by `is_admin` boolean and `family_id` presence
66+
- `User` model creates helper instances (`adminCast`, `memberCast`) via `Admin::make()` and `Member::make()` for type-specific behavior
67+
- **Important**: Queries on `Admin::query()` or `Member::query()` are automatically filtered by global scopes
68+
69+
**Reference**: See `policies-reference.md` for detailed authorization patterns and `core/USER_SYSTEM.md` for user type implementation details.
6270

6371
---
6472

@@ -132,7 +140,11 @@ This document contains the essential architectural principles, patterns, and con
132140

133141
## Queue Workers (Background Jobs)
134142

135-
**Development**: Uses `sync` driver - jobs execute immediately in the request (no background processing)
143+
**Development**: Queue driver is configurable via `QUEUE_CONNECTION` environment variable:
144+
- **Default in config**: `database` (set in `config/queue.php` line 16)
145+
- **Typical dev setup**: Override via `.env` to use `sync` or `deferred` for immediate/queued execution
146+
- **Tests**: Always use `sync` driver (set in `phpunit.xml` line 40) - jobs execute immediately in test requests
147+
- **No background workers in dev**: Development typically uses `sync` or `deferred` to avoid running queue workers
136148

137149
**Production**: Uses `database` driver with supervisord-managed queue workers:
138150
- **3-5 queue worker processes** managed by supervisord
@@ -142,7 +154,7 @@ This document contains the essential architectural principles, patterns, and con
142154
- Container: `queue` service in production compose
143155

144156
**Queue Configuration**:
145-
- Default connection: `database` (set in `config/queue.php`)
157+
- Default connection: `database` (set in `config/queue.php` line 16, can be overridden via `QUEUE_CONNECTION` env var)
146158
- Table: `jobs` (created during migrations)
147159
- Retry logic: 3 attempts, 90-second timeout between retries
148160

documentation/ai/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ documentation/ai/
109109
- Test helpers go in `tests/Helpers/` (autoloaded by Pest)
110110

111111
**Key architectural patterns** (internalize these):
112-
- **STI (Single Table Inheritance)**: User → Admin/Member (see `core/USER_SYSTEM.md`)
112+
- **Pseudo-STI (Scope-Based Type Discrimination)**: User → Admin/Member via global scopes, not true STI (see `core/USER_SYSTEM.md`)
113113
- **Family Atomicity**: Families are atomic units, members mirror family's project
114114
- **Two-Level Auth**: Global scopes (query) + Policies (action) (see `core/SCOPING.md`)
115115
- **Universe Fixture**: `universe.sql` loaded once, rolled back per test
@@ -187,7 +187,7 @@ documentation/ai/
187187
### Core System Patterns
188188

189189
**`core/USER_SYSTEM.md`** - Complete user type system
190-
- STI (Single Table Inheritance) pattern
190+
- Pseudo-STI (scope-based type discrimination) pattern
191191
- Member/Admin/Superadmin capabilities
192192
- Database schema, relationships
193193
- Validation rules, constraints

documentation/ai/core/USER_SYSTEM.md

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Overview
44

5-
MTAV uses **Single Table Inheritance (STI)** for user management with three distinct actor types:
5+
MTAV uses a **pseudo-STI pattern** (scope-based type discrimination) for user management with three distinct actor types:
66

77
```
88
Superadmin (highest privileges)
@@ -12,6 +12,13 @@ Admin (project-scoped management)
1212
Member (family-bound participation)
1313
```
1414

15+
**Implementation Pattern**: This is **NOT true Single Table Inheritance** (no `type` discriminator column). Instead:
16+
- `Admin` and `Member` classes extend `User`
17+
- Type discrimination via **global scopes** on `is_admin` boolean and `family_id` presence
18+
- `Admin::query()` automatically scoped to `where('is_admin', true)`
19+
- `Member::query()` automatically scoped to `where('is_admin', false)->whereNotNull('family_id')`
20+
- `User` model creates helper instances (`adminCast`, `memberCast`) for type-specific behavior
21+
1522
**Key Principle**: Admins and Members are **mutually exclusive** - a user cannot be both.
1623

1724
---
@@ -237,8 +244,9 @@ $user->isSuperadmin() // true if email in config array AND is_admin = true
237244

238245
### User (Base Model)
239246

240-
**Pattern**: Single Table Inheritance (STI)
241-
**Discriminator**: `is_admin` field
247+
**Pattern**: Pseudo-STI (scope-based type discrimination)
248+
**Type Discriminator**: `is_admin` boolean field + `family_id` presence
249+
**Implementation**: Global scopes on `Admin` and `Member` subclasses filter queries automatically
242250

243251
**Validation Rules**:
244252
- `firstname`: REQUIRED, string, max:255
@@ -262,13 +270,18 @@ public function activeProjects(): BelongsToMany
262270

263271
**Model Methods**:
264272
```php
265-
public function isSuperadmin(): bool
266-
public function isAdmin(): bool
267-
public function isMember(): bool
268-
public function toAdmin(): ?Admin
269-
public function toMember(): ?Member
273+
public function isSuperadmin(): bool // Checks email in config + is_admin = true
274+
public function isAdmin(): bool // Returns is_admin || isSuperadmin()
275+
public function isMember(): bool // Returns !is_admin
276+
public function asAdmin(): ?Admin // Returns adminCast helper instance
277+
public function asMember(): ?Member // Returns memberCast helper instance
270278
```
271279

280+
**Type Casting Behavior**:
281+
- `User` model's `booted()` method creates `Admin` or `Member` instances via `Admin::make()` and `Member::make()`
282+
- These are stored as `adminCast` and `memberCast` properties for type-specific method access
283+
- **Important**: These are helper instances, not the actual model - queries still go through global scopes
284+
272285
**Soft Delete Behavior**:
273286
- Uses `SoftDeletes` trait
274287
- Setting `deleted_at` marks user as deleted
@@ -278,20 +291,30 @@ public function toMember(): ?Member
278291
### Admin Model
279292

280293
**Extends**: User
281-
**Global Scope**: `where('is_admin', true)`
294+
**Global Scope**: `where('is_admin', true)` (applied automatically to all `Admin::query()` calls)
295+
**Table**: Uses `users` table (shared with all user types)
296+
**Key Behavior**:
297+
- All queries on `Admin::query()` are automatically filtered to `is_admin = true`
298+
- Cannot query admins via `User::query()` without bypassing scope
299+
- Helper instance created in `User::booted()` as `adminCast` property
282300

283301
**Additional Methods**:
284-
- Project management helpers
285-
- Admin-specific relationships
302+
- Project management helpers (`manages()`)
303+
- Admin-specific relationships (`events()`, `upcomingEvents()`)
286304

287305
### Member Model
288306

289307
**Extends**: User
290-
**Global Scope**: `where('is_admin', false)`
308+
**Global Scope**: `where('is_admin', false)->whereNotNull('family_id')` (applied automatically to all `Member::query()` calls)
309+
**Table**: Uses `users` table (shared with all user types)
310+
**Key Behavior**:
311+
- All queries on `Member::query()` are automatically filtered to `is_admin = false AND family_id IS NOT NULL`
312+
- Cannot query members via `User::query()` without bypassing scope
313+
- Helper instance created in `User::booted()` as `memberCast` property
291314

292315
**Additional Methods**:
293-
- Family-specific helpers
294-
- Member-specific relationships
316+
- Family-specific helpers (`family()`, `project()`)
317+
- Member-specific relationships (`rsvps()`, `events()`)
295318

296319
---
297320

resources/js/components/layout/header/AppSidebarHeader.vue

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ import { useBroadcasting } from '@/composables/useBroadcasting';
1010
const { onMessage, onPrivateMessage, onProjectMessage, onAnyMessage } = useBroadcasting();
1111
1212
// Listen for navigation messages
13-
onMessage('user.navigation', (message) => {
14-
console.log('[AppSidebarHeader] Navigation message received:', message);
15-
});
13+
// onMessage('user.navigation', (message) => {
14+
// console.log('[AppSidebarHeader] Navigation message received:', message);
15+
// });
1616
1717
// Listen for any private channel message
18-
onPrivateMessage((message) => {
19-
console.log('[AppSidebarHeader] Private channel message:', message);
20-
});
18+
// onPrivateMessage((message) => {
19+
// console.log('[AppSidebarHeader] Private channel message:', message);
20+
// });
2121
2222
// Listen for project messages
23-
onProjectMessage((message, projectId) => {
24-
console.log('[AppSidebarHeader] Project message:', message, 'Project ID:', projectId);
25-
});
23+
// onProjectMessage((message, projectId) => {
24+
// console.log('[AppSidebarHeader] Project message:', message, 'Project ID:', projectId);
25+
// });
2626
2727
// Listen to everything
28-
onAnyMessage((message) => {
29-
console.log('[AppSidebarHeader] ANY message received:', message);
30-
});
28+
// onAnyMessage((message) => {
29+
// console.log('[AppSidebarHeader] ANY message received:', message);
30+
// });
3131
</script>
3232

3333
<template>

resources/js/composables/useBroadcasting.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,16 @@ export function useBroadcasting() {
6262
const initializeChannels = () => {
6363
if (!auth.value.user) return;
6464

65-
console.log('[useBroadcasting] Initializing channels for user:', auth.value.user.id);
66-
console.log('[useBroadcasting] Available projects:', auth.value.projects);
65+
// console.log('[useBroadcasting] Initializing channels for user:', auth.value.user.id);
66+
// console.log('[useBroadcasting] Available projects:', auth.value.projects);
6767

6868
// Connect to private channel
6969
connectToPrivateChannel();
7070

7171
// Connect to project channels
7272
if (projects.value.length) {
7373
projects.value.forEach((project: any) => {
74-
console.log('[useBroadcasting] Connecting to project channel:', project.id);
74+
// console.log('[useBroadcasting] Connecting to project channel:', project.id);
7575
connectToProjectChannel(project.id);
7676
});
7777
}
@@ -99,7 +99,7 @@ export function useBroadcasting() {
9999
// Listen to all known message types
100100
MESSAGE_TYPES.forEach((type) => {
101101
channel.listen(`.${type}`, (data: BroadcastMessage) => {
102-
console.log('[useBroadcasting] Private channel event:', { type, data });
102+
// console.log('[useBroadcasting] Private channel event:', { type, data });
103103
triggerPrivateCallbacks(data);
104104
triggerMessageCallbacks(data);
105105
triggerAnyCallbacks(data);
@@ -119,20 +119,20 @@ export function useBroadcasting() {
119119
const channelName = `projects.${projectId}`;
120120
const key = `presence-${channelName}`;
121121

122-
console.log('[useBroadcasting] Attempting to join presence channel:', channelName);
122+
// console.log('[useBroadcasting] Attempting to join presence channel:', channelName);
123123

124124
if (subscriptions.has(key)) return; // Already subscribed
125125

126126
const channel = echo().join(channelName) as PresenceChannel;
127127

128-
console.log('[useBroadcasting] Joined presence channel:', channelName);
128+
// console.log('[useBroadcasting] Joined presence channel:', channelName);
129129

130130
// Initialize online users list for this project
131131
onlineUsersByProject.set(projectId, []);
132132

133133
// Handle initial presence
134134
channel.here((users: any[]) => {
135-
console.log('[useBroadcasting] Users already in project channel:', users);
135+
// console.log('[useBroadcasting] Users already in project channel:', users);
136136
const onlineUsers = users.map((user) => ({
137137
id: user.id,
138138
name: user.name,
@@ -143,7 +143,7 @@ export function useBroadcasting() {
143143

144144
// Handle user joining
145145
channel.joining((user: any) => {
146-
console.log('[useBroadcasting] User joining project channel:', user);
146+
// console.log('[useBroadcasting] User joining project channel:', user);
147147
const users = onlineUsersByProject.get(projectId) || [];
148148
const newUser: OnlineUser = {
149149
id: user.id,
@@ -155,7 +155,7 @@ export function useBroadcasting() {
155155

156156
// Handle user leaving
157157
channel.leaving((user: any) => {
158-
console.log('[useBroadcasting] User leaving project channel:', user);
158+
// console.log('[useBroadcasting] User leaving project channel:', user);
159159
const users = onlineUsersByProject.get(projectId) || [];
160160
onlineUsersByProject.set(
161161
projectId,
@@ -166,7 +166,7 @@ export function useBroadcasting() {
166166
// Listen to all known message types
167167
MESSAGE_TYPES.forEach((type) => {
168168
channel.listen(`.${type}`, (data: BroadcastMessage) => {
169-
console.log('[useBroadcasting] Project channel event:', { type, data, projectId });
169+
// console.log('[useBroadcasting] Project channel event:', { type, data, projectId });
170170
triggerProjectCallbacks(data, projectId);
171171
triggerMessageCallbacks(data);
172172
triggerAnyCallbacks(data);
@@ -194,7 +194,7 @@ export function useBroadcasting() {
194194
// Listen to all known message types
195195
MESSAGE_TYPES.forEach((type) => {
196196
channel.listen(`.${type}`, (data: BroadcastMessage) => {
197-
console.log('[useBroadcasting] Global channel event:', { type, data });
197+
// console.log('[useBroadcasting] Global channel event:', { type, data });
198198
triggerGlobalCallbacks(data);
199199
triggerMessageCallbacks(data);
200200
triggerAnyCallbacks(data);

0 commit comments

Comments
 (0)