From 3a9ab87a6d64c4607a7f75e6c30d7cf98e04120e Mon Sep 17 00:00:00 2001 From: myrmidex Date: Thu, 7 Aug 2025 21:28:47 +0200 Subject: [PATCH 01/48] Fix logo path --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 577b087..d8fed45 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Fedi Feed Router
-FFR Logo +FFR Logo
`ffr` is a self-hosted tool for routing content from RSS/Atom feeds to the fediverse. -- 2.45.2 From c17a858e639351e0690f2fecac73f209f43c8a73 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Fri, 8 Aug 2025 21:54:22 +0200 Subject: [PATCH 02/48] Remove previous onboarding implementation --- .../Services/OnboardingRedirectService.php | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 backend/app/Services/OnboardingRedirectService.php diff --git a/backend/app/Services/OnboardingRedirectService.php b/backend/app/Services/OnboardingRedirectService.php deleted file mode 100644 index 6382fbc..0000000 --- a/backend/app/Services/OnboardingRedirectService.php +++ /dev/null @@ -1,20 +0,0 @@ -input('redirect_to'); - - if ($redirectTo) { - return redirect($redirectTo)->with('success', $successMessage); - } - - return redirect()->route($defaultRoute)->with('success', $successMessage); - } -} \ No newline at end of file -- 2.45.2 From 17320ad05a441790c819f0ed3518d53dc7aa9774 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 9 Aug 2025 00:03:45 +0200 Subject: [PATCH 03/48] Add onboarding --- .../Api/V1/OnboardingController.php | 222 ++++++++++++++++++ .../app/Services/Auth/LemmyAuthService.php | 33 +++ backend/routes/api.php | 33 +-- frontend/src/App.tsx | 51 +++- frontend/src/contexts/OnboardingContext.tsx | 68 ++++++ frontend/src/lib/api.ts | 83 +++++++ .../src/pages/onboarding/OnboardingLayout.tsx | 17 ++ .../src/pages/onboarding/OnboardingWizard.tsx | 25 ++ .../pages/onboarding/steps/ChannelStep.tsx | 178 ++++++++++++++ .../pages/onboarding/steps/CompleteStep.tsx | 86 +++++++ .../src/pages/onboarding/steps/FeedStep.tsx | 193 +++++++++++++++ .../pages/onboarding/steps/PlatformStep.tsx | 138 +++++++++++ .../pages/onboarding/steps/WelcomeStep.tsx | 43 ++++ 13 files changed, 1145 insertions(+), 25 deletions(-) create mode 100644 backend/app/Http/Controllers/Api/V1/OnboardingController.php create mode 100644 frontend/src/contexts/OnboardingContext.tsx create mode 100644 frontend/src/pages/onboarding/OnboardingLayout.tsx create mode 100644 frontend/src/pages/onboarding/OnboardingWizard.tsx create mode 100644 frontend/src/pages/onboarding/steps/ChannelStep.tsx create mode 100644 frontend/src/pages/onboarding/steps/CompleteStep.tsx create mode 100644 frontend/src/pages/onboarding/steps/FeedStep.tsx create mode 100644 frontend/src/pages/onboarding/steps/PlatformStep.tsx create mode 100644 frontend/src/pages/onboarding/steps/WelcomeStep.tsx diff --git a/backend/app/Http/Controllers/Api/V1/OnboardingController.php b/backend/app/Http/Controllers/Api/V1/OnboardingController.php new file mode 100644 index 0000000..a1c3b1b --- /dev/null +++ b/backend/app/Http/Controllers/Api/V1/OnboardingController.php @@ -0,0 +1,222 @@ +exists(); + $hasFeed = Feed::where('is_active', true)->exists(); + $hasChannel = PlatformChannel::where('is_active', true)->exists(); + + $needsOnboarding = !$hasPlatformAccount || !$hasFeed || !$hasChannel; + + // Determine current step + $currentStep = null; + if ($needsOnboarding) { + if (!$hasPlatformAccount) { + $currentStep = 'platform'; + } elseif (!$hasFeed) { + $currentStep = 'feed'; + } elseif (!$hasChannel) { + $currentStep = 'channel'; + } + } + + return $this->sendResponse([ + 'needs_onboarding' => $needsOnboarding, + 'current_step' => $currentStep, + 'has_platform_account' => $hasPlatformAccount, + 'has_feed' => $hasFeed, + 'has_channel' => $hasChannel, + ], 'Onboarding status retrieved successfully.'); + } + + /** + * Get onboarding options (languages, platform instances) + */ + public function options(): JsonResponse + { + $languages = Language::where('is_active', true) + ->orderBy('name') + ->get(['id', 'short_code', 'name', 'native_name', 'is_active']); + + $platformInstances = PlatformInstance::where('is_active', true) + ->orderBy('name') + ->get(['id', 'platform', 'url', 'name', 'description', 'is_active']); + + return $this->sendResponse([ + 'languages' => $languages, + 'platform_instances' => $platformInstances, + ], 'Onboarding options retrieved successfully.'); + } + + /** + * Create platform account for onboarding + */ + public function createPlatform(Request $request): JsonResponse + { + $validator = Validator::make($request->all(), [ + 'instance_url' => 'required|url|max:255', + 'username' => 'required|string|max:255', + 'password' => 'required|string|min:6', + 'platform' => 'required|in:lemmy', + ]); + + if ($validator->fails()) { + throw new ValidationException($validator); + } + + $validated = $validator->validated(); + + try { + // Create or get platform instance + $platformInstance = PlatformInstance::firstOrCreate([ + 'url' => $validated['instance_url'], + 'platform' => $validated['platform'], + ], [ + 'name' => parse_url($validated['instance_url'], PHP_URL_HOST) ?? 'Lemmy Instance', + 'is_active' => true, + ]); + + // Authenticate with Lemmy API + $authResponse = $this->lemmyAuthService->authenticate( + $validated['instance_url'], + $validated['username'], + $validated['password'] + ); + + // Create platform account with the current schema + $platformAccount = PlatformAccount::create([ + 'platform' => $validated['platform'], + 'instance_url' => $validated['instance_url'], + 'username' => $validated['username'], + 'password' => $validated['password'], + 'api_token' => $authResponse['jwt'] ?? null, + 'settings' => [ + 'display_name' => $authResponse['person_view']['person']['display_name'] ?? null, + 'description' => $authResponse['person_view']['person']['bio'] ?? null, + 'person_id' => $authResponse['person_view']['person']['id'] ?? null, + 'platform_instance_id' => $platformInstance->id, + ], + 'is_active' => true, + 'status' => 'active', + ]); + + return $this->sendResponse( + new PlatformAccountResource($platformAccount), + 'Platform account created successfully.' + ); + + } catch (\Exception $e) { + return $this->sendError('Failed to create platform account: ' . $e->getMessage(), [], 422); + } + } + + /** + * Create feed for onboarding + */ + public function createFeed(Request $request): JsonResponse + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'url' => 'required|url|max:500', + 'type' => 'required|in:website,rss', + 'language_id' => 'required|exists:languages,id', + 'description' => 'nullable|string|max:1000', + ]); + + if ($validator->fails()) { + throw new ValidationException($validator); + } + + $validated = $validator->validated(); + + $feed = Feed::create([ + 'name' => $validated['name'], + 'url' => $validated['url'], + 'type' => $validated['type'], + 'language_id' => $validated['language_id'], + 'description' => $validated['description'] ?? null, + 'is_active' => true, + ]); + + return $this->sendResponse( + new FeedResource($feed->load('language')), + 'Feed created successfully.' + ); + } + + /** + * Create channel for onboarding + */ + public function createChannel(Request $request): JsonResponse + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'platform_instance_id' => 'required|exists:platform_instances,id', + 'language_id' => 'required|exists:languages,id', + 'description' => 'nullable|string|max:1000', + ]); + + if ($validator->fails()) { + throw new ValidationException($validator); + } + + $validated = $validator->validated(); + + $channel = PlatformChannel::create([ + 'platform_instance_id' => $validated['platform_instance_id'], + 'channel_id' => $validated['name'], // For Lemmy, this is the community name + 'name' => $validated['name'], + 'display_name' => ucfirst($validated['name']), + 'description' => $validated['description'] ?? null, + 'language_id' => $validated['language_id'], + 'is_active' => true, + ]); + + return $this->sendResponse( + new PlatformChannelResource($channel->load(['platformInstance', 'language'])), + 'Channel created successfully.' + ); + } + + /** + * Mark onboarding as complete + */ + public function complete(): JsonResponse + { + // In a real implementation, you might want to update a user preference + // or create a setting that tracks onboarding completion + // For now, we'll just return success since the onboarding status + // is determined by the existence of platform accounts, feeds, and channels + + return $this->sendResponse( + ['completed' => true], + 'Onboarding completed successfully.' + ); + } +} \ No newline at end of file diff --git a/backend/app/Services/Auth/LemmyAuthService.php b/backend/app/Services/Auth/LemmyAuthService.php index 1ee58b2..6c0afda 100644 --- a/backend/app/Services/Auth/LemmyAuthService.php +++ b/backend/app/Services/Auth/LemmyAuthService.php @@ -6,6 +6,7 @@ use App\Exceptions\PlatformAuthException; use App\Models\PlatformAccount; use App\Modules\Lemmy\Services\LemmyApiService; +use Exception; use Illuminate\Support\Facades\Cache; class LemmyAuthService @@ -38,4 +39,36 @@ public static function getToken(PlatformAccount $account): string return $token; } + + /** + * Authenticate with Lemmy API and return user data with JWT + * @throws PlatformAuthException + */ + public function authenticate(string $instanceUrl, string $username, string $password): array + { + try { + $api = new LemmyApiService($instanceUrl); + $token = $api->login($username, $password); + + if (!$token) { + throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed for user: ' . $username); + } + + // Get user info with the token + // For now, we'll return a basic response structure + // In a real implementation, you might want to fetch user details + return [ + 'jwt' => $token, + 'person_view' => [ + 'person' => [ + 'id' => 0, // Would need API call to get actual user info + 'display_name' => null, + 'bio' => null, + ] + ] + ]; + } catch (Exception $e) { + throw new PlatformAuthException(PlatformEnum::LEMMY, 'Authentication failed: ' . $e->getMessage()); + } + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index 4b3b990..f89d445 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -5,11 +5,11 @@ use App\Http\Controllers\Api\V1\DashboardController; use App\Http\Controllers\Api\V1\FeedsController; use App\Http\Controllers\Api\V1\LogsController; +use App\Http\Controllers\Api\V1\OnboardingController; use App\Http\Controllers\Api\V1\PlatformAccountsController; use App\Http\Controllers\Api\V1\PlatformChannelsController; use App\Http\Controllers\Api\V1\RoutingController; use App\Http\Controllers\Api\V1\SettingsController; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; /* @@ -27,23 +27,29 @@ // Public authentication routes Route::post('/auth/login', [AuthController::class, 'login'])->name('api.auth.login'); Route::post('/auth/register', [AuthController::class, 'register'])->name('api.auth.register'); - + // Protected authentication routes Route::middleware('auth:sanctum')->group(function () { Route::post('/auth/logout', [AuthController::class, 'logout'])->name('api.auth.logout'); Route::get('/auth/me', [AuthController::class, 'me'])->name('api.auth.me'); }); - // For demo purposes, making most endpoints public. In production, wrap in auth:sanctum middleware - // Route::middleware('auth:sanctum')->group(function () { + // Onboarding + Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status'); + Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options'); + Route::post('/onboarding/platform', [OnboardingController::class, 'createPlatform'])->name('api.onboarding.platform'); + Route::post('/onboarding/feed', [OnboardingController::class, 'createFeed'])->name('api.onboarding.feed'); + Route::post('/onboarding/channel', [OnboardingController::class, 'createChannel'])->name('api.onboarding.channel'); + Route::post('/onboarding/complete', [OnboardingController::class, 'complete'])->name('api.onboarding.complete'); + // Dashboard stats Route::get('/dashboard/stats', [DashboardController::class, 'stats'])->name('api.dashboard.stats'); - + // Articles Route::get('/articles', [ArticlesController::class, 'index'])->name('api.articles.index'); Route::post('/articles/{article}/approve', [ArticlesController::class, 'approve'])->name('api.articles.approve'); Route::post('/articles/{article}/reject', [ArticlesController::class, 'reject'])->name('api.articles.reject'); - + // Platform Accounts Route::apiResource('platform-accounts', PlatformAccountsController::class)->names([ 'index' => 'api.platform-accounts.index', @@ -54,7 +60,7 @@ ]); Route::post('/platform-accounts/{platformAccount}/set-active', [PlatformAccountsController::class, 'setActive']) ->name('api.platform-accounts.set-active'); - + // Platform Channels Route::apiResource('platform-channels', PlatformChannelsController::class)->names([ 'index' => 'api.platform-channels.index', @@ -65,7 +71,7 @@ ]); Route::post('/platform-channels/{channel}/toggle', [PlatformChannelsController::class, 'toggle']) ->name('api.platform-channels.toggle'); - + // Feeds Route::apiResource('feeds', FeedsController::class)->names([ 'index' => 'api.feeds.index', @@ -75,7 +81,7 @@ 'destroy' => 'api.feeds.destroy', ]); Route::post('/feeds/{feed}/toggle', [FeedsController::class, 'toggle'])->name('api.feeds.toggle'); - + // Routing Route::get('/routing', [RoutingController::class, 'index'])->name('api.routing.index'); Route::post('/routing', [RoutingController::class, 'store'])->name('api.routing.store'); @@ -83,14 +89,11 @@ Route::put('/routing/{feed}/{channel}', [RoutingController::class, 'update'])->name('api.routing.update'); Route::delete('/routing/{feed}/{channel}', [RoutingController::class, 'destroy'])->name('api.routing.destroy'); Route::post('/routing/{feed}/{channel}/toggle', [RoutingController::class, 'toggle'])->name('api.routing.toggle'); - + // Settings Route::get('/settings', [SettingsController::class, 'index'])->name('api.settings.index'); Route::put('/settings', [SettingsController::class, 'update'])->name('api.settings.update'); - + // Logs Route::get('/logs', [LogsController::class, 'index'])->name('api.logs.index'); - - // Close the auth:sanctum middleware group when ready - // }); -}); \ No newline at end of file +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d523794..04a0e4b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,19 +5,50 @@ import Dashboard from './pages/Dashboard'; import Articles from './pages/Articles'; import Feeds from './pages/Feeds'; import Settings from './pages/Settings'; +import OnboardingWizard from './pages/onboarding/OnboardingWizard'; +import { OnboardingProvider, useOnboarding } from './contexts/OnboardingContext'; + +const AppContent: React.FC = () => { + const { isLoading } = useOnboarding(); + + if (isLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + return ( + + {/* Onboarding routes - outside of main layout */} + } /> + + {/* Main app routes - with layout */} + + + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + + ); +}; const App: React.FC = () => { return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - - + + + ); }; diff --git a/frontend/src/contexts/OnboardingContext.tsx b/frontend/src/contexts/OnboardingContext.tsx new file mode 100644 index 0000000..47b9287 --- /dev/null +++ b/frontend/src/contexts/OnboardingContext.tsx @@ -0,0 +1,68 @@ +import React, { createContext, useContext, type ReactNode } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { apiClient, type OnboardingStatus } from '../lib/api'; + +interface OnboardingContextValue { + onboardingStatus: OnboardingStatus | undefined; + isLoading: boolean; + needsOnboarding: boolean; +} + +const OnboardingContext = createContext(null); + +interface OnboardingProviderProps { + children: ReactNode; +} + +export const OnboardingProvider: React.FC = ({ children }) => { + const navigate = useNavigate(); + const location = useLocation(); + + const { data: onboardingStatus, isLoading } = useQuery({ + queryKey: ['onboarding-status'], + queryFn: () => apiClient.getOnboardingStatus(), + retry: 1, + }); + + const needsOnboarding = onboardingStatus?.needs_onboarding ?? false; + const isOnOnboardingPage = location.pathname.startsWith('/onboarding'); + + // Redirect logic + React.useEffect(() => { + if (isLoading) return; + + if (needsOnboarding && !isOnOnboardingPage) { + // User needs onboarding but is not on onboarding pages + const targetStep = onboardingStatus?.current_step; + if (targetStep) { + navigate(`/onboarding/${targetStep}`, { replace: true }); + } else { + navigate('/onboarding', { replace: true }); + } + } else if (!needsOnboarding && isOnOnboardingPage) { + // User doesn't need onboarding but is on onboarding pages + navigate('/dashboard', { replace: true }); + } + }, [onboardingStatus, isLoading, needsOnboarding, isOnOnboardingPage, navigate]); + + const value: OnboardingContextValue = { + onboardingStatus, + isLoading, + needsOnboarding, + }; + + return ( + + {children} + + ); +}; + +export const useOnboarding = () => { + const context = useContext(OnboardingContext); + if (!context) { + throw new Error('useOnboarding must be used within an OnboardingProvider'); + } + return context; +}; \ No newline at end of file diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 1404909..b2fd143 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -127,6 +127,59 @@ export interface DashboardStats { current_period: string; } +// Onboarding types +export interface Language { + id: number; + short_code: string; + name: string; + native_name: string; + is_active: boolean; +} + +export interface PlatformInstance { + id: number; + platform: 'lemmy'; + url: string; + name: string; + description: string | null; + is_active: boolean; +} + +export interface OnboardingStatus { + needs_onboarding: boolean; + current_step: 'platform' | 'feed' | 'channel' | 'complete' | null; + has_platform_account: boolean; + has_feed: boolean; + has_channel: boolean; +} + +export interface OnboardingOptions { + languages: Language[]; + platform_instances: PlatformInstance[]; +} + +export interface PlatformAccountRequest { + instance_url: string; + username: string; + password: string; + platform: 'lemmy'; +} + +export interface FeedRequest { + name: string; + url: string; + type: 'rss' | 'website'; + language_id: number; + description?: string; +} + +export interface ChannelRequest { + name: string; + platform_instance_id: number; + language_id: number; + description?: string; +} + // API Client class class ApiClient { constructor() { @@ -205,6 +258,36 @@ class ApiClient { const response = await axios.put>('/settings', data); return response.data.data; } + + // Onboarding endpoints + async getOnboardingStatus(): Promise { + const response = await axios.get>('/onboarding/status'); + return response.data.data; + } + + async getOnboardingOptions(): Promise { + const response = await axios.get>('/onboarding/options'); + return response.data.data; + } + + async createPlatformAccount(data: PlatformAccountRequest): Promise { + const response = await axios.post>('/onboarding/platform', data); + return response.data.data; + } + + async createFeedForOnboarding(data: FeedRequest): Promise { + const response = await axios.post>('/onboarding/feed', data); + return response.data.data; + } + + async createChannelForOnboarding(data: ChannelRequest): Promise { + const response = await axios.post>('/onboarding/channel', data); + return response.data.data; + } + + async completeOnboarding(): Promise { + await axios.post('/onboarding/complete'); + } } export const apiClient = new ApiClient(); \ No newline at end of file diff --git a/frontend/src/pages/onboarding/OnboardingLayout.tsx b/frontend/src/pages/onboarding/OnboardingLayout.tsx new file mode 100644 index 0000000..4985823 --- /dev/null +++ b/frontend/src/pages/onboarding/OnboardingLayout.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +interface OnboardingLayoutProps { + children: React.ReactNode; +} + +const OnboardingLayout: React.FC = ({ children }) => { + return ( +
+
+ {children} +
+
+ ); +}; + +export default OnboardingLayout; \ No newline at end of file diff --git a/frontend/src/pages/onboarding/OnboardingWizard.tsx b/frontend/src/pages/onboarding/OnboardingWizard.tsx new file mode 100644 index 0000000..b8379bb --- /dev/null +++ b/frontend/src/pages/onboarding/OnboardingWizard.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import OnboardingLayout from './OnboardingLayout'; +import WelcomeStep from './steps/WelcomeStep'; +import PlatformStep from './steps/PlatformStep'; +import FeedStep from './steps/FeedStep'; +import ChannelStep from './steps/ChannelStep'; +import CompleteStep from './steps/CompleteStep'; + +const OnboardingWizard: React.FC = () => { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +}; + +export default OnboardingWizard; \ No newline at end of file diff --git a/frontend/src/pages/onboarding/steps/ChannelStep.tsx b/frontend/src/pages/onboarding/steps/ChannelStep.tsx new file mode 100644 index 0000000..b15b244 --- /dev/null +++ b/frontend/src/pages/onboarding/steps/ChannelStep.tsx @@ -0,0 +1,178 @@ +import React, { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { apiClient, type ChannelRequest, type Language, type PlatformInstance } from '../../../lib/api'; + +const ChannelStep: React.FC = () => { + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + name: '', + platform_instance_id: 0, + language_id: 0, + description: '' + }); + const [errors, setErrors] = useState>({}); + + // Get onboarding options (languages, platform instances) + const { data: options, isLoading: optionsLoading } = useQuery({ + queryKey: ['onboarding-options'], + queryFn: () => apiClient.getOnboardingOptions() + }); + + const createChannelMutation = useMutation({ + mutationFn: (data: ChannelRequest) => apiClient.createChannelForOnboarding(data), + onSuccess: () => { + navigate('/onboarding/complete'); + }, + onError: (error: any) => { + if (error.response?.data?.errors) { + setErrors(error.response.data.errors); + } else { + setErrors({ general: [error.response?.data?.message || 'An error occurred'] }); + } + } + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setErrors({}); + createChannelMutation.mutate(formData); + }; + + const handleChange = (field: keyof ChannelRequest, value: string | number) => { + setFormData(prev => ({ ...prev, [field]: value })); + // Clear field error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: [] })); + } + }; + + if (optionsLoading) { + return
Loading...
; + } + + return ( +
+

Configure Your Channel

+

+ Set up a Lemmy community where articles will be posted +

+ + {/* Progress indicator */} +
+
+
+
3
+
4
+
+ +
+ {errors.general && ( +
+

{errors.general[0]}

+
+ )} + +
+ + handleChange('name', e.target.value)} + placeholder="technology" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + /> +

Enter the community name (without the @ or instance)

+ {errors.name && ( +

{errors.name[0]}

+ )} +
+ +
+ + + {errors.platform_instance_id && ( +

{errors.platform_instance_id[0]}

+ )} +
+ +
+ + + {errors.language_id && ( +

{errors.language_id[0]}

+ )} +
+ +
+ + + @error('feedDescription')

{{ $message }}

@enderror +
+ +
+ + +
+
+
+ @endif + + {{-- Step 4: Channel --}} + @if ($step === 4) +
+

Configure Your Channel

+

+ Set up a Lemmy community where articles will be posted +

+ + {{-- Progress indicator --}} +
+
+
+
3
+
4
+
+ +
+ @if (!empty($errors['general'])) +
+

{{ $errors['general'] }}

+
+ @endif + +
+ + +

Enter the community name (without the @ or instance)

+ @error('channelName')

{{ $message }}

@enderror +
+ +
+ + + @error('platformInstanceId')

{{ $message }}

@enderror +
+ +
+ + + @error('channelLanguageId')

{{ $message }}

@enderror +
+ +
+ + + @error('channelDescription')

{{ $message }}

@enderror +
+ +
+ + +
+
+
+ @endif + + {{-- Step 5: Route --}} + @if ($step === 5) +
+

Create Your First Route

+

+ Connect your feed to a channel by creating a route. This tells FFR which articles to post where. +

+ + {{-- Progress indicator --}} +
+
+
+
+
4
+
5
+
+ +
+ @if (!empty($errors['general'])) +
+

{{ $errors['general'] }}

+
+ @endif + +
+ + + @error('routeFeedId')

{{ $message }}

@enderror +
+ +
+ + + @if ($channels->isEmpty()) +

+ No channels available. Please create a channel first. +

+ @endif + @error('routeChannelId')

{{ $message }}

@enderror +
+ +
+ + +

+ Higher priority routes are processed first (default: 50) +

+ @error('routePriority')

{{ $message }}

@enderror +
+ +
+ + +
+
+
+ @endif + + {{-- Step 6: Complete --}} + @if ($step === 6) +
+
+
+ + + +
+

Setup Complete!

+

+ Great! You've successfully configured FFR. Your feeds will now be monitored and articles will be automatically posted to your configured channels. +

+
+ + {{-- Progress indicator --}} +
+
+
+
+
+
+ +
+
+

What happens next?

+
    +
  • • Your feeds will be checked regularly for new articles
  • +
  • • New articles will be automatically posted to your channels
  • +
  • • You can monitor activity in the Articles and other sections
  • +
+
+ +
+

Want more control?

+

+ You can add more feeds, channels, and configure settings from the dashboard. +

+
+
+ +
+ + +
+ View Articles + • + Manage Feeds + • + Settings +
+
+
+ @endif + + diff --git a/resources/views/livewire/routes.blade.php b/resources/views/livewire/routes.blade.php new file mode 100644 index 0000000..364bbfa --- /dev/null +++ b/resources/views/livewire/routes.blade.php @@ -0,0 +1,380 @@ +
+
+
+

Routes

+

+ Manage connections between your feeds and channels +

+
+ +
+ +
+ @forelse ($routes as $route) +
+
+
+
+

+ {{ $route->feed?->name }} → {{ $route->platformChannel?->display_name ?? $route->platformChannel?->name }} +

+ @if ($route->is_active) + + + + + Active + + @else + + + + + Inactive + + @endif +
+
+ Priority: {{ $route->priority }} + + Feed: {{ $route->feed?->name }} + + Channel: {{ $route->platformChannel?->display_name ?? $route->platformChannel?->name }} + + Created: {{ $route->created_at->format('M d, Y') }} +
+ @if ($route->platformChannel?->description) +

+ {{ $route->platformChannel->description }} +

+ @endif + @if ($route->keywords->isNotEmpty()) +
+
+ + + + + Keywords +
+
+ @foreach ($route->keywords as $keyword) + + {{ $keyword->keyword }} + + @endforeach +
+
+ @else +
+ No keyword filters - matches all articles +
+ @endif +
+
+ + + +
+
+
+ @empty +
+ + + +

No routes

+

+ Get started by creating a new route to connect feeds with channels. +

+
+ +
+
+ @endforelse +
+ + + @if ($showCreateModal) + + @endif + + + @if ($editingRoute) + + @endif +
diff --git a/resources/views/livewire/settings.blade.php b/resources/views/livewire/settings.blade.php new file mode 100644 index 0000000..96b050c --- /dev/null +++ b/resources/views/livewire/settings.blade.php @@ -0,0 +1,90 @@ +
+
+

Settings

+

+ Configure your system preferences +

+
+ +
+ +
+
+

+ + + + + Article Processing +

+

+ Control how articles are processed and handled +

+
+
+
+
+

+ Article Processing Enabled +

+

+ Enable automatic fetching and processing of articles from feeds +

+
+ +
+ +
+
+

+ Publishing Approvals Required +

+

+ Require manual approval before articles are published to platforms +

+
+ +
+
+
+ + + @if ($successMessage) +
+

{{ $successMessage }}

+
+ @endif + + @if ($errorMessage) +
+

{{ $errorMessage }}

+
+ @endif +
+
diff --git a/routes/web.php b/routes/web.php index 74bb7ca..b7fa3da 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,16 +1,36 @@ route('dashboard'); }); -Route::get('/dashboard', function () { - return view('dashboard'); -})->middleware(['auth', 'verified'])->name('dashboard'); +// Onboarding routes (protected by auth, but need incomplete onboarding) +Route::middleware(['auth', 'onboarding.incomplete'])->group(function () { + Route::get('/onboarding', Onboarding::class)->name('onboarding'); +}); +// Main app routes (protected by auth and require completed onboarding) +Route::middleware(['auth', 'onboarding.complete'])->group(function () { + Route::get('/dashboard', Dashboard::class)->name('dashboard'); + Route::get('/articles', Articles::class)->name('articles'); + Route::get('/feeds', Feeds::class)->name('feeds'); + Route::get('/channels', Channels::class)->name('channels'); + Route::get('/routes', Routes::class)->name('routes'); + Route::get('/settings', Settings::class)->name('settings'); +}); + +// Profile routes (auth protected, no onboarding check needed) Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); -- 2.45.2 From 4e0f0bb0728e2b6afe46cf4f0a11c710b0f17d48 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Fri, 23 Jan 2026 00:08:32 +0100 Subject: [PATCH 47/48] 73 - Fix dev environment --- Dockerfile.dev | 127 +++++++++++++++++++++++++ docker/dev/docker-compose.yml | 82 ++++++++++++++++ docker/dev/podman/.env.dev | 62 ------------ docker/dev/podman/Dockerfile | 53 ----------- docker/dev/podman/container-start.sh | 73 -------------- docker/dev/podman/docker-compose.yml | 76 --------------- docker/dev/podman/nginx.conf | 87 ----------------- docker/dev/podman/podman-sail-alias.sh | 52 ---------- docker/dev/podman/start-dev.sh | 81 ---------------- shell.nix | 109 +++++++++++++++++++++ vite.config.js | 9 ++ 11 files changed, 327 insertions(+), 484 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 docker/dev/docker-compose.yml delete mode 100644 docker/dev/podman/.env.dev delete mode 100644 docker/dev/podman/Dockerfile delete mode 100755 docker/dev/podman/container-start.sh delete mode 100644 docker/dev/podman/docker-compose.yml delete mode 100644 docker/dev/podman/nginx.conf delete mode 100755 docker/dev/podman/podman-sail-alias.sh delete mode 100755 docker/dev/podman/start-dev.sh create mode 100644 shell.nix diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..124ace1 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,127 @@ +# Development Dockerfile with FrankenPHP +FROM dunglas/frankenphp:latest-php8.3-alpine + +# Install system dependencies + development tools +RUN apk add --no-cache \ + nodejs \ + npm \ + git \ + mysql-client \ + vim \ + bash \ + nano + +# Install PHP extensions including xdebug for development +RUN install-php-extensions \ + pdo_mysql \ + opcache \ + zip \ + gd \ + intl \ + bcmath \ + redis \ + pcntl \ + xdebug + +# Install Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Set working directory +WORKDIR /app + +# Configure PHP for development +RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" + +# Configure Xdebug (disabled by default to reduce noise) +RUN echo "xdebug.mode=off" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo ";xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo ";xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo ";xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + +# Configure Caddy for development (simpler, no worker mode) +RUN cat > /etc/caddy/Caddyfile < /start.sh <<'EOF' +#!/bin/sh +set -e + +# Create .env file if it doesn't exist +if [ ! -f ".env" ]; then + echo "Creating .env file from .env.example..." + cp .env.example .env +fi + +# Install dependencies if volumes are empty +if [ ! -f "vendor/autoload.php" ]; then + echo "Installing composer dependencies..." + composer install +fi + +# Always reinstall node_modules in container to get correct native binaries for Alpine/musl +echo "Installing npm dependencies..." +rm -rf node_modules 2>/dev/null || true +rm -rf /app/.npm 2>/dev/null || true +npm install --cache /tmp/.npm + +# Clear Laravel caches +php artisan config:clear || true +php artisan cache:clear || true + +# Wait for database and run migrations +echo "Waiting for database..." +sleep 5 +php artisan migrate --force || echo "Migration failed or not needed" + +# Run seeders +echo "Running seeders..." +php artisan db:seed --force || echo "Seeding skipped or already done" + +# Generate app key if not set +if [ -z "$APP_KEY" ] || [ "$APP_KEY" = "base64:YOUR_KEY_HERE" ]; then + echo "Generating application key..." + php artisan key:generate +fi + +# Start Vite dev server in background +npm run dev & + +# Start Horizon (queue worker) in background +php artisan horizon & + +# Start FrankenPHP +exec frankenphp run --config /etc/caddy/Caddyfile +EOF + +RUN chmod +x /start.sh + +# Expose ports +EXPOSE 8000 5173 + +# Use the startup script +CMD ["/start.sh"] diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml new file mode 100644 index 0000000..fcb03bb --- /dev/null +++ b/docker/dev/docker-compose.yml @@ -0,0 +1,82 @@ +# =================== +# FFR Development Services +# =================== +# Port allocation: +# App: 8000 (frankenphp), 5173 (vite) +# DB: 3307 (mysql) +# Redis: 6380 + +services: + app: + build: + context: ../.. + dockerfile: Dockerfile.dev + container_name: ffr_dev_app + restart: unless-stopped + ports: + - "8000:8000" + - "5173:5173" + volumes: + - ../..:/app + environment: + APP_NAME: "FFR" + APP_ENV: "${APP_ENV:-local}" + APP_DEBUG: "${APP_DEBUG:-true}" + APP_URL: "${APP_URL:-http://localhost:8000}" + DB_CONNECTION: mysql + DB_HOST: db + DB_PORT: 3306 + DB_DATABASE: "${DB_DATABASE:-ffr_dev}" + DB_USERNAME: "${DB_USERNAME:-ffr}" + DB_PASSWORD: "${DB_PASSWORD:-ffr}" + REDIS_HOST: redis + REDIS_PORT: 6379 + SESSION_DRIVER: redis + CACHE_STORE: redis + QUEUE_CONNECTION: redis + VITE_HOST: "0.0.0.0" + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + networks: + - ffr-network + + db: + image: mariadb:11 + container_name: ffr_dev_db + restart: unless-stopped + ports: + - "3307:3306" + environment: + MYSQL_DATABASE: "${DB_DATABASE:-ffr_dev}" + MYSQL_USER: "${DB_USERNAME:-ffr}" + MYSQL_PASSWORD: "${DB_PASSWORD:-ffr}" + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-ffr_root_dev}" + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - ffr-network + + redis: + image: redis:7-alpine + container_name: ffr_dev_redis + restart: unless-stopped + ports: + - "6380:6379" + networks: + - ffr-network + +networks: + ffr-network: + driver: bridge + +volumes: + db_data: diff --git a/docker/dev/podman/.env.dev b/docker/dev/podman/.env.dev deleted file mode 100644 index f81be47..0000000 --- a/docker/dev/podman/.env.dev +++ /dev/null @@ -1,62 +0,0 @@ -APP_NAME="FFR Development" -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_TIMEZONE=UTC -APP_URL=http://localhost:8000 - -APP_LOCALE=en -APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US - -APP_MAINTENANCE_DRIVER=file -APP_MAINTENANCE_STORE=database - -BCRYPT_ROUNDS=12 - -LOG_CHANNEL=stack -LOG_STACK=single -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST=db -DB_PORT=3306 -DB_DATABASE=ffr_dev -DB_USERNAME=ffr_user -DB_PASSWORD=ffr_password - -SESSION_DRIVER=redis -SESSION_LIFETIME=120 -SESSION_ENCRYPT=false -SESSION_PATH=/ -SESSION_DOMAIN=null - -BROADCAST_CONNECTION=log -FILESYSTEM_DISK=local -QUEUE_CONNECTION=redis - -CACHE_STORE=redis -CACHE_PREFIX= - -REDIS_CLIENT=phpredis -REDIS_HOST=redis -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=log -MAIL_HOST=127.0.0.1 -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="hello@example.com" -MAIL_FROM_NAME="${APP_NAME}" - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false - -VITE_APP_NAME="${APP_NAME}" \ No newline at end of file diff --git a/docker/dev/podman/Dockerfile b/docker/dev/podman/Dockerfile deleted file mode 100644 index 3708dfe..0000000 --- a/docker/dev/podman/Dockerfile +++ /dev/null @@ -1,53 +0,0 @@ -FROM docker.io/library/php:8.4-fpm - -# Install system dependencies including nginx -RUN apt-get update && apt-get install -y \ - git \ - curl \ - libpng-dev \ - libonig-dev \ - libxml2-dev \ - zip \ - unzip \ - nginx \ - default-mysql-client \ - && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd \ - && pecl install redis xdebug \ - && docker-php-ext-enable redis xdebug - -# Install Node.js 22.x LTS (latest LTS version) -RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ - && apt-get install -y nodejs - -# Install Composer -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -# Set working directory -WORKDIR /var/www/html - -# Copy application code -COPY . . - -# Install PHP dependencies in backend -WORKDIR /var/www/html/backend -RUN composer install --optimize-autoloader --no-scripts - -# Build React frontend -WORKDIR /var/www/html/frontend -RUN npm install && npm run build - -# Back to main directory -WORKDIR /var/www/html - -# Set permissions -RUN chown -R www-data:www-data /var/www/html \ - && chmod -R 755 /var/www/html/backend/storage \ - && chmod -R 755 /var/www/html/backend/bootstrap/cache - -# Copy and set up container start script -COPY docker/dev/podman/container-start.sh /usr/local/bin/container-start.sh -RUN chmod +x /usr/local/bin/container-start.sh - -EXPOSE 80 - -CMD ["/usr/local/bin/container-start.sh"] \ No newline at end of file diff --git a/docker/dev/podman/container-start.sh b/docker/dev/podman/container-start.sh deleted file mode 100755 index 2f95f04..0000000 --- a/docker/dev/podman/container-start.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash - -# Copy development environment configuration to backend -cp /var/www/html/docker/dev/podman/.env.dev /var/www/html/backend/.env - -# Setup nginx configuration for development -cp /var/www/html/docker/dev/podman/nginx.conf /etc/nginx/sites-available/default - -# Install/update dependencies -echo "Installing PHP dependencies..." -cd /var/www/html/backend -composer install --no-interaction - -# Ensure APP_KEY is set in backend/.env -ENV_APP_KEY="${APP_KEY}" -if [ -n "$ENV_APP_KEY" ]; then - echo "Using APP_KEY from environment" - sed -i "s|^APP_KEY=.*|APP_KEY=${ENV_APP_KEY}|" /var/www/html/backend/.env || true -fi - -# Generate application key if still missing -CURRENT_APP_KEY=$(grep "^APP_KEY=" /var/www/html/backend/.env | cut -d'=' -f2) -if [ -z "$CURRENT_APP_KEY" ]; then - echo "Generating application key..." - php artisan key:generate --force -fi - -# Verify APP_KEY -APP_KEY=$(grep "^APP_KEY=" /var/www/html/backend/.env | cut -d'=' -f2) -if [ -n "$APP_KEY" ]; then - echo "✅ APP_KEY successfully set." -else - echo "❌ ERROR: APP_KEY not set!" -fi - -# Wait for database to be ready -echo "Waiting for database..." -while ! mysql -h db -u ffr_user -pffr_password --connect-timeout=2 -e "SELECT 1" >/dev/null 2>&1; do - echo "Database not ready, waiting..." - sleep 1 -done -echo "Database connection established!" - -# Run migrations and seeders -php artisan migrate --force -php artisan db:seed --force - -# Build frontend if not already built -cd /var/www/html/frontend -if [ ! -d "dist" ]; then - echo "Building React frontend..." - npm run build -fi - -# Start services -echo "Starting services..." - -# Start React dev server -cd /var/www/html/frontend -npm run dev -- --host 0.0.0.0 --port 5173 & - -# Start Laravel backend -cd /var/www/html/backend -php artisan serve --host=127.0.0.1 --port=8000 & - -# Start Horizon (manages queue workers in dev) -php artisan horizon & - -# Start nginx -nginx -g "daemon off;" & - -# Wait for background processes -wait diff --git a/docker/dev/podman/docker-compose.yml b/docker/dev/podman/docker-compose.yml deleted file mode 100644 index a7e86d3..0000000 --- a/docker/dev/podman/docker-compose.yml +++ /dev/null @@ -1,76 +0,0 @@ -services: - app: - build: - context: ../../.. - dockerfile: docker/dev/podman/Dockerfile - container_name: ffr-dev-app - restart: unless-stopped - working_dir: /var/www/html - environment: - - APP_ENV=local - - APP_DEBUG=true - - APP_KEY=base64:5VABFQKtzx6flRFn7rQUQYI/G8xLnkUSYPVaYz2s/4M= - - DB_CONNECTION=mysql - - DB_HOST=db - - DB_PORT=3306 - - DB_DATABASE=ffr_dev - - DB_USERNAME=ffr_user - - DB_PASSWORD=ffr_password - - REDIS_HOST=redis - - REDIS_PORT=6379 - - QUEUE_CONNECTION=redis - - CACHE_DRIVER=redis - - SESSION_DRIVER=redis - - VITE_PORT=5173 - volumes: - - ../../../:/var/www/html:Z - - /var/www/html/node_modules - - /var/www/html/vendor - ports: - - "8000:80" - depends_on: - db: - condition: service_healthy - redis: - condition: service_started - networks: - - ffr-dev-network - - db: - image: docker.io/library/mysql:8.4 - container_name: ffr-dev-db - restart: unless-stopped - environment: - - MYSQL_DATABASE=ffr_dev - - MYSQL_USER=ffr_user - - MYSQL_PASSWORD=ffr_password - - MYSQL_ROOT_PASSWORD=root_password - volumes: - - db_data:/var/lib/mysql - ports: - - "3307:3306" - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "ffr_user", "-pffr_password"] - timeout: 5s - retries: 5 - interval: 3s - start_period: 30s - networks: - - ffr-dev-network - - redis: - image: docker.io/library/redis:7-alpine - container_name: ffr-dev-redis - restart: unless-stopped - ports: - - "6380:6379" - networks: - - ffr-dev-network - -networks: - ffr-dev-network: - driver: bridge - -volumes: - db_data: - driver: local \ No newline at end of file diff --git a/docker/dev/podman/nginx.conf b/docker/dev/podman/nginx.conf deleted file mode 100644 index ef692f5..0000000 --- a/docker/dev/podman/nginx.conf +++ /dev/null @@ -1,87 +0,0 @@ -server { - listen 80; - server_name localhost; - - # Proxy API requests to Laravel backend - location /api/ { - proxy_pass http://127.0.0.1:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_redirect off; - } - - # Serve Laravel public assets (images, etc.) - location /images/ { - alias /var/www/html/backend/public/images/; - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Proxy Vite dev server assets - location /@vite/ { - proxy_pass http://127.0.0.1:5173; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_redirect off; - } - - # Proxy Vite HMR WebSocket - location /@vite/client { - proxy_pass http://127.0.0.1:5173; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_redirect off; - } - - # Proxy node_modules for Vite deps - location /node_modules/ { - proxy_pass http://127.0.0.1:5173; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_redirect off; - } - - # Proxy /src/ for Vite source files - location /src/ { - proxy_pass http://127.0.0.1:5173; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_redirect off; - } - - # Proxy React dev server for development (catch-all) - location / { - proxy_pass http://127.0.0.1:5173; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # WebSocket support for HMR - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_redirect off; - } - - # Security headers - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options DENY; - add_header X-XSS-Protection "1; mode=block"; - - # Gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; -} \ No newline at end of file diff --git a/docker/dev/podman/podman-sail-alias.sh b/docker/dev/podman/podman-sail-alias.sh deleted file mode 100755 index 07b90ed..0000000 --- a/docker/dev/podman/podman-sail-alias.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash - -# Podman aliases for Laravel Sail compatibility -# Source this file to use Sail commands with Podman -# Usage: source docker/dev/podman/podman-sail-alias.sh - -# Create docker alias pointing to podman -alias docker='podman' - -# Create docker-compose alias pointing to podman-compose -alias docker-compose='podman-compose' - -# Sail wrapper function that uses podman-compose -sail() { - if [[ -f docker/dev/podman/docker-compose.yml ]]; then - podman-compose -f docker/dev/podman/docker-compose.yml "$@" - else - echo "❌ Podman compose file not found at docker/dev/podman/docker-compose.yml" - return 1 - fi -} - -# FFR-specific helper functions -ffr-test() { - echo "🧪 Running FFR tests..." - podman exec ffr-dev-app php artisan test "$@" -} - -ffr-artisan() { - echo "🔧 Running artisan command..." - podman exec ffr-dev-app php artisan "$@" -} - -ffr-logs() { - echo "📋 Showing FFR application logs..." - podman-compose -f docker/dev/podman/docker-compose.yml logs -f app -} - -ffr-shell() { - echo "🐚 Opening shell in FFR container..." - podman exec -it ffr-dev-app bash -} - -echo "✅ FFR Podman aliases set up for Laravel Sail compatibility" -echo "🐳 'docker' → 'podman'" -echo "🔧 'docker-compose' → 'podman-compose'" -echo "⛵ 'sail' → uses podman-compose with dev configuration" -echo "🚀 FFR-specific commands:" -echo " 'ffr-test' → run tests" -echo " 'ffr-artisan' → run artisan commands" -echo " 'ffr-logs' → view application logs" -echo " 'ffr-shell' → open container shell" \ No newline at end of file diff --git a/docker/dev/podman/start-dev.sh b/docker/dev/podman/start-dev.sh deleted file mode 100755 index 55f455f..0000000 --- a/docker/dev/podman/start-dev.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/bash - -# Podman development environment startup script for FFR - -set -e - -echo "🚀 Starting FFR development environment with Podman..." - -# Check if .env exists -if [ ! -f .env ]; then - echo "📋 Creating .env file from .env.example..." - cp .env.example .env -fi - -# Check if podman-compose is available -if ! command -v podman-compose &> /dev/null; then - echo "❌ podman-compose not found." - exit -fi - -# Start services -echo "🔧 Starting services..." -podman-compose -f docker/dev/podman/docker-compose.yml up -d - -# Wait for database to be ready -echo "⏳ Waiting for database to be ready..." -sleep 10 - -# Install/update dependencies if needed -echo "📦 Installing dependencies..." -podman exec ffr-dev-app bash -c "cd /var/www/html/backend && composer install" -podman exec ffr-dev-app bash -c "cd /var/www/html/frontend && npm install" - -# Run migrations and seeders -echo "🗃️ Running database migrations..." -podman exec ffr-dev-app bash -c "cd /var/www/html/backend && php artisan migrate --force" -echo "🌱 Running database seeders..." -podman exec ffr-dev-app bash -c "cd /var/www/html/backend && php artisan db:seed --force" - -# Wait for container services to be fully ready -echo "⏳ Waiting for container services to initialize..." -sleep 5 - -# Start React dev server if not already running -echo "🚀 Starting React dev server..." -podman exec -d ffr-dev-app bash -c "cd /var/www/html/frontend && npm run dev -- --host 0.0.0.0 --port 5173 > /dev/null 2>&1 &" -sleep 5 - -# Verify Vite is running -if podman exec ffr-dev-app bash -c "curl -s http://localhost:5173 > /dev/null 2>&1"; then - echo "✅ Vite dev server is running" -else - echo "⚠️ Vite dev server may not have started properly" -fi - -# Check Laravel Horizon status inside the app container -echo "🔍 Checking Laravel Horizon in app container..." -HSTATUS="$(podman exec ffr-dev-app bash -lc "cd /var/www/html/backend && php artisan horizon:status" 2>/dev/null || echo "Horizon status: unknown")" -echo "$HSTATUS" -if echo "$HSTATUS" | grep -qi 'inactive'; then - echo "ℹ️ Horizon is inactive. Attempting to start..." - podman exec -d ffr-dev-app bash -lc "cd /var/www/html/backend && php artisan horizon > /dev/null 2>&1 &" || true - sleep 2 - podman exec ffr-dev-app bash -lc "cd /var/www/html/backend && php artisan horizon:status" || true -else - echo "✅ Horizon appears to be running." -fi -# Show supervisors summary (non-fatal if unavailable) -podman exec ffr-dev-app bash -lc "cd /var/www/html/backend && php artisan horizon:supervisors | sed -n '1,80p'" || true - -echo "✅ Development environment is ready!" -echo "🌐 Application: http://localhost:8000" -echo "🔥 Vite dev server: http://localhost:5173" -echo "💾 Database: localhost:3307" -echo "🔴 Redis: localhost:6380" -echo "" -echo "📋 Useful commands:" -echo " Stop: podman-compose -f docker/dev/podman/docker-compose.yml down" -echo " Logs: podman-compose -f docker/dev/podman/docker-compose.yml logs -f" -echo " Exec: podman exec -it ffr-dev-app bash" -echo " Tests: podman exec ffr-dev-app php artisan test" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..20e63fc --- /dev/null +++ b/shell.nix @@ -0,0 +1,109 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + # PHP and tools + php84 + php84Packages.composer + + # Node.js and npm + nodejs_22 + + # Container tools + podman + podman-compose + + # Database client (for direct DB access) + mariadb.client + + # Redis client + redis + + # Utilities + git + curl + gnumake + ]; + + shellHook = '' + export USER_ID=$(id -u) + export GROUP_ID=$(id -g) + export PODMAN_USERNS=keep-id + + # Compose file location + COMPOSE_FILE="$PWD/docker/dev/docker-compose.yml" + + # =================== + # ALIASES + # =================== + alias pc='podman-compose -f $COMPOSE_FILE' + + # =================== + # DEV COMMANDS + # =================== + dev-up() { + echo "Starting services..." + PODMAN_USERNS=keep-id podman-compose -f $COMPOSE_FILE up -d "$@" + echo "" + podman-compose -f $COMPOSE_FILE ps + echo "" + echo "App available at: http://localhost:8000" + } + + dev-down() { + if [[ "$1" == "-v" ]]; then + echo "Stopping services and removing volumes..." + podman-compose -f $COMPOSE_FILE down -v + else + echo "Stopping services..." + podman-compose -f $COMPOSE_FILE down + fi + } + + dev-restart() { + echo "Restarting services..." + podman-compose -f $COMPOSE_FILE restart "$@" + } + + dev-logs() { + podman-compose -f $COMPOSE_FILE logs -f app "$@" + } + + dev-logs-db() { + podman-compose -f $COMPOSE_FILE logs -f db "$@" + } + + dev-shell() { + podman-compose -f $COMPOSE_FILE exec app sh + } + + dev-artisan() { + podman-compose -f $COMPOSE_FILE exec app php artisan "$@" + } + + # =================== + # WELCOME MESSAGE + # =================== + echo "" + echo "╔═══════════════════════════════════════════════════════════╗" + echo "║ FFR Dev Environment ║" + echo "╚═══════════════════════════════════════════════════════════╝" + echo "" + echo " Podman: $(podman --version | cut -d' ' -f3)" + echo "" + echo "Commands:" + echo " dev-up [services] Start all or specific services" + echo " dev-down [-v] Stop services (-v removes volumes)" + echo " dev-restart Restart services" + echo " dev-logs Tail app logs" + echo " dev-logs-db Tail database logs" + echo " dev-shell Shell into app container" + echo " dev-artisan Run artisan command" + echo "" + echo "Services:" + echo " app Laravel + Vite http://localhost:8000" + echo " db MariaDB localhost:3307" + echo " redis Redis localhost:6380" + echo "" + ''; +} diff --git a/vite.config.js b/vite.config.js index 421b569..9e105b4 100644 --- a/vite.config.js +++ b/vite.config.js @@ -8,4 +8,13 @@ export default defineConfig({ refresh: true, }), ], + server: { + host: '0.0.0.0', + port: 5173, + strictPort: true, + cors: true, + hmr: { + host: 'localhost', + }, + }, }); -- 2.45.2 From b6290c0f8d8992af848db3011a40acb2e5dd826e Mon Sep 17 00:00:00 2001 From: myrmidex Date: Fri, 23 Jan 2026 00:30:05 +0100 Subject: [PATCH 48/48] 73 - Fix prod environment --- Dockerfile | 127 ++++++++++++ README.md | 299 ++++++++++----------------- docker/production/Dockerfile | 87 -------- docker/production/docker-compose.yml | 59 +++--- docker/production/nginx.conf | 82 -------- docker/production/start-app.sh | 51 ----- docker/production/supervisord.conf | 45 ---- shell.nix | 31 ++- 8 files changed, 291 insertions(+), 490 deletions(-) create mode 100644 Dockerfile delete mode 100644 docker/production/Dockerfile delete mode 100644 docker/production/nginx.conf delete mode 100644 docker/production/start-app.sh delete mode 100644 docker/production/supervisord.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..11ce824 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,127 @@ +# Production Dockerfile with FrankenPHP +FROM dunglas/frankenphp:latest-php8.3-alpine + +# Install system dependencies +RUN apk add --no-cache \ + nodejs \ + npm \ + git \ + mysql-client + +# Install PHP extensions +RUN install-php-extensions \ + pdo_mysql \ + opcache \ + zip \ + gd \ + intl \ + bcmath \ + redis \ + pcntl + +# Install Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Set working directory +WORKDIR /app + +# Set fixed production environment variables +ENV APP_ENV=production \ + APP_DEBUG=false \ + DB_CONNECTION=mysql \ + DB_HOST=db \ + DB_PORT=3306 \ + SESSION_DRIVER=redis \ + CACHE_STORE=redis \ + QUEUE_CONNECTION=redis \ + LOG_CHANNEL=stack \ + LOG_LEVEL=error + +# Copy application code first +COPY . . + +# Install PHP dependencies (production only) +RUN composer install --no-dev --no-interaction --optimize-autoloader + +# Install ALL Node dependencies (including dev for building) +RUN npm ci + +# Build frontend assets +RUN npm run build + +# Remove node_modules after build to save space +RUN rm -rf node_modules + +# Laravel optimizations +RUN php artisan config:cache \ + && php artisan route:cache \ + && php artisan view:cache \ + && composer dump-autoload --optimize + +# Set permissions +RUN chown -R www-data:www-data /app/storage /app/bootstrap/cache + +# Configure Caddy +RUN cat > /etc/caddy/Caddyfile < /start-prod.sh <<'EOF' +#!/bin/sh +set -e + +# Wait for database to be ready +echo "Waiting for database..." +for i in $(seq 1 30); do + if mysqladmin ping -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" --silent 2>/dev/null; then + echo "Database is ready!" + break + fi + echo "Waiting for database... ($i/30)" + sleep 2 +done + +# Run migrations +echo "Running migrations..." +php artisan migrate --force || echo "Migrations failed or already up-to-date" + +# Start Horizon in the background +php artisan horizon & + +# Start FrankenPHP +exec frankenphp run --config /etc/caddy/Caddyfile +EOF + +RUN chmod +x /start-prod.sh + +# Start with our script +CMD ["/start-prod.sh"] diff --git a/README.md b/README.md index 1153f7b..47f8177 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,128 @@ -# Fedi Feed Router (FFR) v1.0.0 +# FFR (Feed to Fediverse Router) -
-FFR Logo +A Laravel-based application for routing RSS/Atom feeds to Fediverse platforms like Lemmy. Built with Laravel, Livewire, and FrankenPHP for a modern, single-container deployment. -**A minimal working version — limited to two hardcoded sources, designed for self-hosters.** -*Future versions will expand configurability and support.* -
+## Features ---- +- **Feed aggregation** - Fetch articles from multiple RSS/Atom feeds +- **Fediverse publishing** - Automatically post to Lemmy communities +- **Route configuration** - Map feeds to specific channels with keywords +- **Approval workflow** - Optional manual approval before publishing +- **Queue processing** - Background job handling with Laravel Horizon +- **Single container deployment** - Simplified hosting with FrankenPHP -## 🔰 Project Overview +## Self-hosting -**One-liner:** FFR routes content from RSS/Atom feeds to the fediverse based on keyword matching. +The production image is available at `codeberg.org/lvl0/ffr:latest`. -FFR is a self-hosted tool that monitors RSS/Atom feeds, filters articles based on keywords, and automatically publishes matching content to fediverse platforms like Lemmy. This v1.0.0 release provides a working foundation with two hardcoded news sources (CBC and BBC), designed specifically for self-hosters who want a simple, privacy-first solution without SaaS dependencies. +### docker-compose.yml -## ⚙️ Features +```yaml +services: + app: + image: codeberg.org/lvl0/ffr:latest + container_name: ffr_app + restart: always + ports: + - "8000:8000" + environment: + APP_KEY: "${APP_KEY}" + APP_URL: "${APP_URL}" + DB_DATABASE: "${DB_DATABASE}" + DB_USERNAME: "${DB_USERNAME}" + DB_PASSWORD: "${DB_PASSWORD}" + REDIS_HOST: redis + REDIS_PORT: 6379 + volumes: + - app_storage:/app/storage + depends_on: + - db + - redis + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/up"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s -Current v1.0.0 features: -- ✅ Fetches articles from two hardcoded RSS feeds (CBC News, BBC News) -- ✅ Keyword-based content filtering and matching -- ✅ Automatic posting to Lemmy communities -- ✅ Web dashboard for monitoring and management -- ✅ Docker-based deployment for easy self-hosting -- ✅ Privacy-first design with no external dependencies + db: + image: mariadb:11 + container_name: ffr_db + restart: always + environment: + MYSQL_DATABASE: "${DB_DATABASE}" + MYSQL_USER: "${DB_USERNAME}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" + volumes: + - db_data:/var/lib/mysql -Limitations (to be addressed in future versions): -- Feed sources are currently hardcoded (not user-configurable) -- Only supports Lemmy as target platform -- Basic keyword matching (no regex or complex rules yet) + redis: + image: redis:7-alpine + container_name: ffr_redis + restart: always + volumes: + - redis_data:/data -## 🚀 Installation - -### Quick Start with Docker - -1. **Clone the repository:** - ```bash - git clone https://codeberg.org/lvl0/ffr.git - cd ffr - ``` - -2. **Create environment file:** - ```bash - cp docker/production/.env.example .env - ``` - -3. **Configure your environment variables:** - ```env - # Required variables only - APP_URL=http://your-domain.com:8000 - DB_PASSWORD=your-secure-db-password - DB_ROOT_PASSWORD=your-secure-root-password - ``` - -4. **Start the application:** - ```bash - docker-compose -f docker/production/docker-compose.yml up -d - ``` - -The application will be available at `http://localhost:8000` - -### System Requirements - -- Docker and Docker Compose (or Podman) -- 2GB RAM minimum -- 10GB disk space -- Linux/macOS/Windows with WSL2 - -## 🕹️ Usage - -### Web Interface - -Access the dashboard at `http://localhost:8000` to: -- View fetched articles -- Monitor posting queue -- Check system logs -- Manage keywords (coming in v2.0) - -### Manual Commands - -Trigger article refresh manually: -```bash -docker compose exec app php artisan article:refresh +volumes: + db_data: + redis_data: + app_storage: ``` -View application logs: -```bash -docker compose logs -f app -``` +### Environment Variables -### Scheduled Tasks +| Variable | Required | Description | +|----------|----------|-------------| +| `APP_KEY` | Yes | Encryption key. Generate with: `echo "base64:$(openssl rand -base64 32)"` | +| `APP_URL` | Yes | Your domain (e.g., `https://ffr.example.com`) | +| `DB_DATABASE` | Yes | Database name | +| `DB_USERNAME` | Yes | Database user | +| `DB_PASSWORD` | Yes | Database password | +| `DB_ROOT_PASSWORD` | Yes | MariaDB root password | -The application automatically: -- Fetches new articles every hour -- Publishes matching articles every 5 minutes -- Syncs with Lemmy communities every 10 minutes +## Development -## 📜 Logging & Debugging - -**Log locations:** -- Application logs: Available in web dashboard under "Logs" section -- Docker logs: `docker compose logs -f app` -- Laravel logs: Inside container at `/var/www/html/backend/storage/logs/` - -**Debug mode:** -To enable debug mode for troubleshooting, add to your `.env`: -```env -APP_DEBUG=true -``` -⚠️ Remember to disable debug mode in production! - -## 🤝 Contributing - -We welcome contributions! Here's how you can help: - -1. **Report bugs:** Open an issue describing the problem -2. **Suggest features:** Create an issue with your idea -3. **Submit PRs:** Fork, create a feature branch, and submit a pull request -4. **Improve docs:** Documentation improvements are always appreciated - -For development setup, see the [Development Setup](#development-setup) section below. - -## 📘 License - -This project is licensed under the GNU Affero General Public License v3.0 (AGPLv3). -See [LICENSE](LICENSE) file for details. - -## 🧭 Roadmap - -### v1.0.0 (Current Release) -- ✅ Basic feed fetching from hardcoded sources -- ✅ Keyword filtering -- ✅ Lemmy posting -- ✅ Web dashboard -- ✅ Docker deployment - -### v2.0.0 (Planned) -- [ ] User-configurable feed sources -- [ ] Advanced filtering rules (regex, boolean logic) -- [ ] Support for Mastodon and other ActivityPub platforms -- [ ] API for external integrations -- [ ] Multi-user support with permissions - -### v3.0.0 (Future) -- [ ] Machine learning-based content categorization -- [ ] Feed discovery and recommendations -- [ ] Scheduled posting with optimal timing -- [ ] Analytics and insights dashboard - ---- - -## Development Setup - -For contributors and developers who want to work on FFR: - -### Prerequisites - -- Podman and podman-compose (or Docker) -- Git -- PHP 8.2+ (for local development) -- Node.js 18+ (for frontend development) - -### Quick Start - -1. **Clone and start the development environment:** - ```bash - git clone https://codeberg.org/lvl0/ffr.git - cd ffr - ./docker/dev/podman/start-dev.sh - ``` - -2. **Access the development environment:** - - Web interface: http://localhost:8000 - - Vite dev server: http://localhost:5173 - - Database: localhost:3307 - - Redis: localhost:6380 - -### Development Commands +### NixOS / Nix ```bash -# Run tests with coverage -podman-compose -f docker/dev/podman/docker-compose.yml exec app bash -c "cd backend && XDEBUG_MODE=coverage php artisan test --coverage-html=coverage-report" - -# Execute artisan commands -podman-compose -f docker/dev/podman/docker-compose.yml exec app php artisan migrate -podman-compose -f docker/dev/podman/docker-compose.yml exec app php artisan tinker - -# View logs -podman-compose -f docker/dev/podman/docker-compose.yml logs -f - -# Access container shell -podman-compose -f docker/dev/podman/docker-compose.yml exec app bash - -# Stop environment -podman-compose -f docker/dev/podman/docker-compose.yml down +git clone https://codeberg.org/lvl0/ffr.git +cd ffr +nix-shell ``` -### Development Features +The shell will display available commands and optionally start the containers for you. -- **Hot reload:** Vite automatically reloads frontend changes -- **Pre-seeded database:** Sample data for immediate testing -- **Laravel Horizon:** Queue monitoring dashboard -- **Xdebug:** Configured for debugging and code coverage -- **Redis:** For caching, sessions, and queues +#### Available Commands ---- +| Command | Description | +|---------|-------------| +| `dev-up` | Start development environment | +| `dev-down` | Stop development environment | +| `dev-restart` | Restart containers | +| `dev-logs` | Follow app logs | +| `dev-logs-db` | Follow database logs | +| `dev-shell` | Enter app container | +| `dev-artisan ` | Run artisan commands | +| `prod-build [tag]` | Build and push prod image (default: latest) | + +#### Services + +| Service | URL | +|---------|-----| +| App | http://localhost:8000 | +| Vite | http://localhost:5173 | +| MariaDB | localhost:3307 | +| Redis | localhost:6380 | + +### Other Platforms + +Contributions welcome for development setup instructions on other platforms. + +## License + +This project is open-source software licensed under the [AGPL-3.0 license](LICENSE). ## Support -For help and support: -- 💬 Open a [Discussion](https://codeberg.org/lvl0/ffr/discussions) -- 🐛 Report [Issues](https://codeberg.org/lvl0/ffr/issues) - ---- - -
-Built with ❤️ for the self-hosting community -
\ No newline at end of file +For issues and questions, please use [Codeberg Issues](https://codeberg.org/lvl0/ffr/issues). diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile deleted file mode 100644 index 8690f35..0000000 --- a/docker/production/Dockerfile +++ /dev/null @@ -1,87 +0,0 @@ -# Multi-stage build for FFR Laravel application -FROM node:22-alpine AS frontend-builder - -WORKDIR /app - -# Copy frontend package files -COPY frontend/package*.json ./ - -# Install Node dependencies -RUN npm ci - -# Copy frontend source -COPY frontend/ ./ - -# Build frontend assets -RUN npm run build - -# PHP runtime stage -FROM php:8.4-fpm-alpine - -# Install system dependencies -RUN apk add --no-cache \ - git \ - curl \ - libpng-dev \ - libxml2-dev \ - zip \ - unzip \ - oniguruma-dev \ - mysql-client \ - nginx \ - supervisor \ - autoconf \ - gcc \ - g++ \ - make - -# Install PHP extensions -RUN docker-php-ext-install \ - pdo_mysql \ - mbstring \ - exif \ - pcntl \ - bcmath \ - gd \ - && pecl install redis \ - && docker-php-ext-enable redis - -# Install Composer -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -# Set working directory -WORKDIR /var/www/html - -# Copy application code -COPY . . - -# Install PHP dependencies in backend directory -WORKDIR /var/www/html/backend -RUN composer install --no-dev --optimize-autoloader --no-interaction - -# Copy built frontend assets from builder stage to frontend dist -COPY --from=frontend-builder /app/dist/ /var/www/html/frontend/dist/ - -# Back to main directory -WORKDIR /var/www/html - -# Copy nginx and supervisor configurations -COPY docker/production/nginx.conf /etc/nginx/http.d/default.conf -COPY docker/production/supervisord.conf /etc/supervisord.conf -COPY docker/production/start-app.sh /usr/local/bin/start-app - -# Set proper permissions -RUN chown -R www-data:www-data /var/www/html \ - && chmod -R 755 /var/www/html/backend/storage \ - && chmod -R 755 /var/www/html/backend/bootstrap/cache \ - && chmod +x /usr/local/bin/start-app - -# Expose port 80 for nginx -EXPOSE 80 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD cd /var/www/html/backend && php artisan --version || exit 1 - -# Start the application -CMD ["/usr/local/bin/start-app"] \ No newline at end of file diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml index 8b9aa31..20ce7c5 100644 --- a/docker/production/docker-compose.yml +++ b/docker/production/docker-compose.yml @@ -1,23 +1,28 @@ +# =================== +# FFR Production Services +# =================== + services: app: - image: codeberg.org/lvl0/ffr:v1.0.0-rc1 - container_name: ffr-app + build: + context: ../.. + dockerfile: Dockerfile + image: codeberg.org/lvl0/ffr:latest + container_name: ffr_app restart: unless-stopped - environment: - - APP_URL=${APP_URL} - - DB_CONNECTION=mysql - - DB_HOST=db - - DB_PORT=3306 - - DB_DATABASE=ffr - - DB_USERNAME=ffr_user - - DB_PASSWORD=${DB_PASSWORD} - - REDIS_HOST=redis - - REDIS_PORT=6379 - - CACHE_DRIVER=redis - - SESSION_DRIVER=redis - - QUEUE_CONNECTION=redis ports: - - "8000:80" + - "8000:8000" + environment: + APP_NAME: "${APP_NAME:-FFR}" + APP_KEY: "${APP_KEY}" + APP_URL: "${APP_URL}" + DB_HOST: db + DB_PORT: 3306 + DB_DATABASE: "${DB_DATABASE:-ffr}" + DB_USERNAME: "${DB_USERNAME:-ffr}" + DB_PASSWORD: "${DB_PASSWORD}" + REDIS_HOST: redis + REDIS_PORT: 6379 depends_on: db: condition: service_healthy @@ -27,28 +32,28 @@ services: - ffr-network db: - image: docker.io/library/mysql:8.4 - container_name: ffr-db + image: mariadb:11 + container_name: ffr_db restart: unless-stopped environment: - - MYSQL_DATABASE=ffr - - MYSQL_USER=ffr_user - - MYSQL_PASSWORD=${DB_PASSWORD} - - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + MYSQL_DATABASE: "${DB_DATABASE:-ffr}" + MYSQL_USER: "${DB_USERNAME:-ffr}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" volumes: - db_data:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "ffr_user", "-p${DB_PASSWORD}"] + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s timeout: 5s retries: 5 - interval: 3s start_period: 30s networks: - ffr-network redis: - image: docker.io/library/redis:7-alpine - container_name: ffr-redis + image: redis:7-alpine + container_name: ffr_redis restart: unless-stopped volumes: - redis_data:/data @@ -61,6 +66,4 @@ networks: volumes: db_data: - driver: local redis_data: - driver: local diff --git a/docker/production/nginx.conf b/docker/production/nginx.conf deleted file mode 100644 index 5ff7994..0000000 --- a/docker/production/nginx.conf +++ /dev/null @@ -1,82 +0,0 @@ -server { - listen 80; - server_name localhost; - - # Serve static React build files - root /var/www/html/frontend/dist; - index index.html; - - # API requests to Laravel backend - location /api/ { - root /var/www/html/backend/public; - try_files /index.php =404; - - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /var/www/html/backend/public/index.php; - include fastcgi_params; - - # Increase timeouts for long-running requests - fastcgi_read_timeout 300; - fastcgi_send_timeout 300; - } - - # Serve Laravel public assets (images, etc.) - location /images/ { - alias /var/www/html/backend/public/images/; - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # React app - catch all routes - location / { - try_files $uri $uri/ /index.html; - } - - # Static assets with far-future expiry - location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - # Security headers - add_header X-Frame-Options "SAMEORIGIN"; - add_header X-Content-Type-Options "nosniff"; - add_header X-XSS-Protection "1; mode=block"; - - # Deny access to hidden files - location ~ /\.ht { - deny all; - } - - # Deny access to sensitive files - location ~ /\.(env|git) { - deny all; - } - - location = /favicon.ico { - access_log off; - log_not_found off; - } - - location = /robots.txt { - access_log off; - log_not_found off; - } - - # Gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_proxied any; - gzip_comp_level 6; - gzip_types - text/plain - text/css - text/xml - text/javascript - application/javascript - application/xml+rss - application/json; -} \ No newline at end of file diff --git a/docker/production/start-app.sh b/docker/production/start-app.sh deleted file mode 100644 index 0d0df99..0000000 --- a/docker/production/start-app.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh - -# Create .env file if it doesn't exist -if [ ! -f /var/www/html/backend/.env ]; then - cp /var/www/html/backend/.env.example /var/www/html/backend/.env 2>/dev/null || touch /var/www/html/backend/.env -fi - -# Wait for database to be ready using PHP -echo "Waiting for database..." -until php -r " -\$host = getenv('DB_HOST') ?: 'db'; -\$user = getenv('DB_USERNAME') ?: 'ffr_user'; -\$pass = getenv('DB_PASSWORD'); -\$db = getenv('DB_DATABASE') ?: 'ffr'; -try { - \$pdo = new PDO(\"mysql:host=\$host;dbname=\$db\", \$user, \$pass, [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false - ]); - echo 'Database ready'; - exit(0); -} catch (Exception \$e) { - exit(1); -} -" 2>/dev/null; do - echo "Database not ready, waiting..." - sleep 1 -done -echo "Database connection established!" - -# Generate app key if not set -if ! grep -q "APP_KEY=base64:" /var/www/html/backend/.env; then - cd /var/www/html/backend && php artisan key:generate --force -fi - -# Laravel optimizations for production -cd /var/www/html/backend -php artisan config:cache -php artisan route:cache -php artisan view:cache - -# Run migrations -cd /var/www/html/backend -php artisan migrate --force - -# Run all seeders (same as dev) -cd /var/www/html/backend -php artisan db:seed --force - -# Start supervisor to manage nginx and php-fpm -supervisord -c /etc/supervisord.conf \ No newline at end of file diff --git a/docker/production/supervisord.conf b/docker/production/supervisord.conf deleted file mode 100644 index 5df63d5..0000000 --- a/docker/production/supervisord.conf +++ /dev/null @@ -1,45 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/var/log/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:nginx] -command=nginx -g "daemon off;" -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=10 - -[program:php-fpm] -command=php-fpm --nodaemonize -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=10 - -[program:horizon] -command=php /var/www/html/backend/artisan horizon -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=20 - -[program:scheduler] -command=sh -c "while true; do php /var/www/html/backend/artisan schedule:run --verbose --no-interaction; sleep 60; done" -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=20 \ No newline at end of file diff --git a/shell.nix b/shell.nix index 20e63fc..e14efc4 100644 --- a/shell.nix +++ b/shell.nix @@ -3,8 +3,8 @@ pkgs.mkShell { buildInputs = with pkgs; [ # PHP and tools - php84 - php84Packages.composer + php83 + php83Packages.composer # Node.js and npm nodejs_22 @@ -81,6 +81,32 @@ pkgs.mkShell { podman-compose -f $COMPOSE_FILE exec app php artisan "$@" } + # =================== + # PROD COMMANDS + # =================== + prod-build() { + local tag="''${1:-latest}" + local image="codeberg.org/lvl0/ffr:$tag" + + echo "Building production image: $image" + if ! podman build -t "$image" -f Dockerfile .; then + echo "Build failed!" + return 1 + fi + + echo "" + echo "Pushing to registry..." + if ! podman push "$image"; then + echo "" + echo "Push failed! You may need to login first:" + echo " podman login codeberg.org" + return 1 + fi + + echo "" + echo "Done! Image pushed: $image" + } + # =================== # WELCOME MESSAGE # =================== @@ -99,6 +125,7 @@ pkgs.mkShell { echo " dev-logs-db Tail database logs" echo " dev-shell Shell into app container" echo " dev-artisan Run artisan command" + echo " prod-build [tag] Build and push prod image (default: latest)" echo "" echo "Services:" echo " app Laravel + Vite http://localhost:8000" -- 2.45.2