Merge pull request 'feature/33-full-regression-test' (#43) from feature/33-full-regression-test into main
Reviewed-on: https://codeberg.org/lvl0/ffr/pulls/43
This commit is contained in:
commit
6ccff513f4
278 changed files with 24055 additions and 3956 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -23,3 +23,5 @@ yarn-error.log
|
||||||
/.nova
|
/.nova
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
|
/backend/coverage-report*
|
||||||
|
/.claude
|
||||||
|
|
|
||||||
66
Dockerfile
66
Dockerfile
|
|
@ -1,66 +0,0 @@
|
||||||
# Multi-stage build for Laravel with React frontend
|
|
||||||
FROM node:22-alpine AS frontend-builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install --only=production
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM php:8.4-fpm-alpine
|
|
||||||
|
|
||||||
# Install system dependencies and PHP extensions
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
git \
|
|
||||||
curl \
|
|
||||||
libpng-dev \
|
|
||||||
oniguruma-dev \
|
|
||||||
libxml2-dev \
|
|
||||||
zip \
|
|
||||||
unzip \
|
|
||||||
autoconf \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
make \
|
|
||||||
gettext \
|
|
||||||
&& 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 composer files
|
|
||||||
COPY composer*.json ./
|
|
||||||
|
|
||||||
# Copy application files (needed for artisan in composer scripts)
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-reqs
|
|
||||||
|
|
||||||
# Copy production environment file and generate APP_KEY
|
|
||||||
COPY docker/build/laravel.env .env
|
|
||||||
RUN php artisan key:generate
|
|
||||||
|
|
||||||
# Copy built frontend assets
|
|
||||||
COPY --from=frontend-builder /app/public/build /var/www/html/public/build
|
|
||||||
|
|
||||||
# Set permissions
|
|
||||||
RUN chown -R www-data:www-data /var/www/html \
|
|
||||||
&& chmod -R 755 /var/www/html/storage \
|
|
||||||
&& chmod -R 755 /var/www/html/bootstrap/cache
|
|
||||||
|
|
||||||
# Create entrypoint script and health check scripts
|
|
||||||
COPY docker/build/entrypoint.sh /entrypoint.sh
|
|
||||||
COPY docker/build/wait-for-db.php /docker/wait-for-db.php
|
|
||||||
COPY docker/build/wait-for-redis.php /docker/wait-for-redis.php
|
|
||||||
RUN chmod +x /entrypoint.sh /docker/wait-for-db.php /docker/wait-for-redis.php
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
CMD ["web"]
|
|
||||||
241
Jenkinsfile
vendored
Normal file
241
Jenkinsfile
vendored
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
APP_ENV = 'testing'
|
||||||
|
DB_CONNECTION = 'mysql'
|
||||||
|
DB_HOST = 'mysql'
|
||||||
|
DB_PORT = '3306'
|
||||||
|
DB_DATABASE = 'ffr_testing'
|
||||||
|
DB_USERNAME = 'ffr_user'
|
||||||
|
DB_PASSWORD = 'ffr_password'
|
||||||
|
CACHE_STORE = 'array'
|
||||||
|
SESSION_DRIVER = 'array'
|
||||||
|
QUEUE_CONNECTION = 'sync'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Setup Environment') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh '''
|
||||||
|
echo "Setting up environment for testing..."
|
||||||
|
cp .env.example .env.testing
|
||||||
|
echo "APP_ENV=testing" >> .env.testing
|
||||||
|
echo "DB_CONNECTION=${DB_CONNECTION}" >> .env.testing
|
||||||
|
echo "DB_HOST=${DB_HOST}" >> .env.testing
|
||||||
|
echo "DB_PORT=${DB_PORT}" >> .env.testing
|
||||||
|
echo "DB_DATABASE=${DB_DATABASE}" >> .env.testing
|
||||||
|
echo "DB_USERNAME=${DB_USERNAME}" >> .env.testing
|
||||||
|
echo "DB_PASSWORD=${DB_PASSWORD}" >> .env.testing
|
||||||
|
echo "CACHE_STORE=${CACHE_STORE}" >> .env.testing
|
||||||
|
echo "SESSION_DRIVER=${SESSION_DRIVER}" >> .env.testing
|
||||||
|
echo "QUEUE_CONNECTION=${QUEUE_CONNECTION}" >> .env.testing
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Install Dependencies') {
|
||||||
|
parallel {
|
||||||
|
stage('PHP Dependencies') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Installing PHP dependencies..."
|
||||||
|
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Node Dependencies') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Installing Node.js dependencies..."
|
||||||
|
npm ci
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Generate Application Key') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
php artisan key:generate --env=testing --force
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Database Setup') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Setting up test database..."
|
||||||
|
php artisan migrate:fresh --env=testing --force
|
||||||
|
php artisan config:clear --env=testing
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Code Quality Checks') {
|
||||||
|
parallel {
|
||||||
|
stage('PHP Syntax Check') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Checking PHP syntax..."
|
||||||
|
find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -exec php -l {} \\;
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('PHPStan Analysis') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
try {
|
||||||
|
sh '''
|
||||||
|
if [ -f "phpstan.neon" ]; then
|
||||||
|
echo "Running PHPStan static analysis..."
|
||||||
|
./vendor/bin/phpstan analyse --no-progress --error-format=table
|
||||||
|
else
|
||||||
|
echo "PHPStan configuration not found, skipping static analysis"
|
||||||
|
fi
|
||||||
|
'''
|
||||||
|
} catch (Exception e) {
|
||||||
|
unstable(message: "PHPStan found issues")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Security Audit') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
try {
|
||||||
|
sh '''
|
||||||
|
echo "Running security audit..."
|
||||||
|
composer audit
|
||||||
|
'''
|
||||||
|
} catch (Exception e) {
|
||||||
|
unstable(message: "Security vulnerabilities found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Unit Tests') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Running Unit Tests..."
|
||||||
|
php artisan test tests/Unit/ --env=testing --stop-on-failure
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
publishTestResults testResultsPattern: 'tests/Unit/results/*.xml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Feature Tests') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Running Feature Tests..."
|
||||||
|
php artisan test tests/Feature/ --env=testing --stop-on-failure
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
publishTestResults testResultsPattern: 'tests/Feature/results/*.xml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Full Regression Test Suite') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Running comprehensive regression test suite..."
|
||||||
|
chmod +x ./run-regression-tests.sh
|
||||||
|
./run-regression-tests.sh
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// Archive test results
|
||||||
|
archiveArtifacts artifacts: 'tests/reports/**/*', allowEmptyArchive: true
|
||||||
|
|
||||||
|
// Publish coverage reports if available
|
||||||
|
publishHTML([
|
||||||
|
allowMissing: false,
|
||||||
|
alwaysLinkToLastBuild: true,
|
||||||
|
keepAll: true,
|
||||||
|
reportDir: 'coverage',
|
||||||
|
reportFiles: 'index.html',
|
||||||
|
reportName: 'Coverage Report'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Performance Tests') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
try {
|
||||||
|
sh '''
|
||||||
|
echo "Running performance tests..."
|
||||||
|
|
||||||
|
# Test memory usage
|
||||||
|
php -d memory_limit=256M artisan test tests/Feature/DatabaseIntegrationTest.php --env=testing
|
||||||
|
|
||||||
|
# Test response times for API endpoints
|
||||||
|
time php artisan test tests/Feature/ApiEndpointRegressionTest.php --env=testing
|
||||||
|
'''
|
||||||
|
} catch (Exception e) {
|
||||||
|
unstable(message: "Performance tests indicated potential issues")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build Assets') {
|
||||||
|
when {
|
||||||
|
anyOf {
|
||||||
|
branch 'main'
|
||||||
|
branch 'develop'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Building production assets..."
|
||||||
|
npm run build
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// Clean up
|
||||||
|
sh '''
|
||||||
|
echo "Cleaning up..."
|
||||||
|
rm -f .env.testing
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
success {
|
||||||
|
echo '✅ All regression tests passed successfully!'
|
||||||
|
// Notify success (customize as needed)
|
||||||
|
// slackSend channel: '#dev-team', color: 'good', message: "Regression tests passed for ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo '❌ Regression tests failed!'
|
||||||
|
// Notify failure (customize as needed)
|
||||||
|
// slackSend channel: '#dev-team', color: 'danger', message: "Regression tests failed for ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
|
||||||
|
}
|
||||||
|
unstable {
|
||||||
|
echo '⚠️ Tests completed with warnings'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -190,6 +190,12 @@ # Stop environment
|
||||||
podman-compose -f docker/dev/podman/docker-compose.yml down
|
podman-compose -f docker/dev/podman/docker-compose.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
```sh
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Development Features
|
### Development Features
|
||||||
|
|
||||||
- **Hot reload**: Vite automatically reloads frontend changes
|
- **Hot reload**: Vite automatically reloads frontend changes
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Article;
|
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ArticlesController extends Controller
|
|
||||||
{
|
|
||||||
public function __invoke(Request $request): View
|
|
||||||
{
|
|
||||||
$articles = Article::with('articlePublication')
|
|
||||||
->orderBy('created_at', 'desc')
|
|
||||||
->paginate(15);
|
|
||||||
|
|
||||||
$publishingApprovalsEnabled = Setting::isPublishingApprovalsEnabled();
|
|
||||||
|
|
||||||
return view('pages.articles.index', compact('articles', 'publishingApprovalsEnabled'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function approve(Article $article): RedirectResponse
|
|
||||||
{
|
|
||||||
$article->approve('manual');
|
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Article approved and queued for publishing.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function reject(Article $article): RedirectResponse
|
|
||||||
{
|
|
||||||
$article->reject('manual');
|
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Article rejected.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Feed;
|
|
||||||
use App\Http\Requests\StoreFeedRequest;
|
|
||||||
use App\Http\Requests\UpdateFeedRequest;
|
|
||||||
use App\Services\OnboardingRedirectService;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
|
|
||||||
class FeedsController extends Controller
|
|
||||||
{
|
|
||||||
public function index(): View
|
|
||||||
{
|
|
||||||
$feeds = Feed::orderBy('is_active', 'desc')
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return view('pages.feeds.index', compact('feeds'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(): View
|
|
||||||
{
|
|
||||||
return view('pages.feeds.create');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(StoreFeedRequest $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validated();
|
|
||||||
$validated['is_active'] = $validated['is_active'] ?? true;
|
|
||||||
|
|
||||||
Feed::create($validated);
|
|
||||||
|
|
||||||
return OnboardingRedirectService::handleRedirect(
|
|
||||||
$request,
|
|
||||||
'feeds.index',
|
|
||||||
'Feed created successfully!'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(Feed $feed): View
|
|
||||||
{
|
|
||||||
return view('pages.feeds.show', compact('feed'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function edit(Feed $feed): View
|
|
||||||
{
|
|
||||||
return view('pages.feeds.edit', compact('feed'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(UpdateFeedRequest $request, Feed $feed): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validated();
|
|
||||||
$validated['is_active'] = $validated['is_active'] ?? $feed->is_active;
|
|
||||||
|
|
||||||
$feed->update($validated);
|
|
||||||
|
|
||||||
return redirect()->route('feeds.index')
|
|
||||||
->with('success', 'Feed updated successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(Feed $feed): RedirectResponse
|
|
||||||
{
|
|
||||||
$feed->delete();
|
|
||||||
|
|
||||||
return redirect()->route('feeds.index')
|
|
||||||
->with('success', 'Feed deleted successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggle(Feed $feed): RedirectResponse
|
|
||||||
{
|
|
||||||
$newStatus = !$feed->is_active;
|
|
||||||
$feed->update(['is_active' => $newStatus]);
|
|
||||||
|
|
||||||
$status = $newStatus ? 'activated' : 'deactivated';
|
|
||||||
|
|
||||||
return redirect()->route('feeds.index')
|
|
||||||
->with('success', "Feed {$status} successfully!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Log;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class LogsController extends Controller
|
|
||||||
{
|
|
||||||
public function __invoke(Request $request): View
|
|
||||||
{
|
|
||||||
$logs = Log::orderBy('created_at', 'desc')->paginate(50);
|
|
||||||
|
|
||||||
return view('pages.logs.index', compact('logs'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Feed;
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Models\PlatformInstance;
|
|
||||||
use App\Services\SystemStatusService;
|
|
||||||
use App\Services\DashboardStatsService;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
|
|
||||||
class OnboardingController extends Controller
|
|
||||||
{
|
|
||||||
public function index(Request $request): View|RedirectResponse
|
|
||||||
{
|
|
||||||
// Check if user needs onboarding
|
|
||||||
if (!$this->needsOnboarding()) {
|
|
||||||
$systemStatus = resolve(SystemStatusService::class)->getSystemStatus();
|
|
||||||
$statsService = resolve(DashboardStatsService::class);
|
|
||||||
|
|
||||||
$period = $request->get('period', 'today');
|
|
||||||
$stats = $statsService->getStats($period);
|
|
||||||
$systemStats = $statsService->getSystemStats();
|
|
||||||
$availablePeriods = $statsService->getAvailablePeriods();
|
|
||||||
|
|
||||||
return view('pages.dashboard', compact(
|
|
||||||
'systemStatus',
|
|
||||||
'stats',
|
|
||||||
'systemStats',
|
|
||||||
'availablePeriods',
|
|
||||||
'period'
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('onboarding.welcome');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function platform(): View|RedirectResponse
|
|
||||||
{
|
|
||||||
if (!$this->needsOnboarding()) {
|
|
||||||
return redirect()->route('feeds.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('onboarding.platform');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function feed(): View|RedirectResponse
|
|
||||||
{
|
|
||||||
if (!$this->needsOnboarding()) {
|
|
||||||
return redirect()->route('feeds.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->hasPlatformAccount()) {
|
|
||||||
return redirect()->route('onboarding.platform');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('onboarding.feed');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function channel(): View|RedirectResponse
|
|
||||||
{
|
|
||||||
if (!$this->needsOnboarding()) {
|
|
||||||
return redirect()->route('feeds.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->hasPlatformAccount()) {
|
|
||||||
return redirect()->route('onboarding.platform');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->hasFeed()) {
|
|
||||||
return redirect()->route('onboarding.feed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('onboarding.channel');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function complete(): View|RedirectResponse
|
|
||||||
{
|
|
||||||
if (!$this->needsOnboarding()) {
|
|
||||||
return redirect()->route('feeds.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->hasPlatformAccount() || !$this->hasFeed() || !$this->hasChannel()) {
|
|
||||||
return redirect()->route('onboarding.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
$systemStatus = resolve(SystemStatusService::class)->getSystemStatus();
|
|
||||||
|
|
||||||
return view('onboarding.complete', compact('systemStatus'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function needsOnboarding(): bool
|
|
||||||
{
|
|
||||||
return !$this->hasPlatformAccount() || !$this->hasFeed() || !$this->hasChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function hasPlatformAccount(): bool
|
|
||||||
{
|
|
||||||
return PlatformAccount::where('is_active', true)->exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function hasFeed(): bool
|
|
||||||
{
|
|
||||||
return Feed::where('is_active', true)->exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function hasChannel(): bool
|
|
||||||
{
|
|
||||||
return PlatformChannel::where('is_active', true)->exists();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformInstance;
|
|
||||||
use App\Enums\PlatformEnum;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Support\Facades\View as ViewFacade;
|
|
||||||
|
|
||||||
class PlatformAccountsController extends Controller
|
|
||||||
{
|
|
||||||
public function index(): View
|
|
||||||
{
|
|
||||||
$accounts = PlatformAccount::orderBy('platform')->orderBy('created_at', 'desc')->get();
|
|
||||||
|
|
||||||
return view('pages.platforms.index', compact('accounts'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(): View
|
|
||||||
{
|
|
||||||
return ViewFacade::make('pages.platforms.create');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'platform' => 'required|in:lemmy,mastodon,reddit',
|
|
||||||
'instance_url' => 'required|url',
|
|
||||||
'username' => 'required|string|max:255',
|
|
||||||
'password' => 'required|string',
|
|
||||||
'settings' => 'nullable|array',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create or find platform instance
|
|
||||||
$platformEnum = PlatformEnum::from($validated['platform']);
|
|
||||||
$instance = PlatformInstance::firstOrCreate([
|
|
||||||
'platform' => $platformEnum,
|
|
||||||
'url' => $validated['instance_url'],
|
|
||||||
], [
|
|
||||||
'name' => parse_url($validated['instance_url'], PHP_URL_HOST),
|
|
||||||
'description' => ucfirst($validated['platform']) . ' instance',
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$account = PlatformAccount::create($validated);
|
|
||||||
|
|
||||||
// If this is the first account for this platform, make it active
|
|
||||||
if (! PlatformAccount::where('platform', $validated['platform'])->where('is_active', true)->exists()) {
|
|
||||||
$account->setAsActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there's a redirect_to parameter for onboarding flow
|
|
||||||
$redirectTo = $request->input('redirect_to');
|
|
||||||
if ($redirectTo) {
|
|
||||||
return redirect($redirectTo)
|
|
||||||
->with('success', 'Platform account created successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('platforms.index')
|
|
||||||
->with('success', 'Platform account created successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function edit(PlatformAccount $platformAccount): View
|
|
||||||
{
|
|
||||||
return ViewFacade::make('pages.platforms.edit', compact('platformAccount'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request, PlatformAccount $platformAccount): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'instance_url' => 'required|url',
|
|
||||||
'username' => 'required|string|max:255',
|
|
||||||
'password' => 'nullable|string',
|
|
||||||
'settings' => 'nullable|array',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Don't update password if not provided
|
|
||||||
if (empty($validated['password'])) {
|
|
||||||
unset($validated['password']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$platformAccount->update($validated);
|
|
||||||
|
|
||||||
return redirect()->route('platforms.index')
|
|
||||||
->with('success', 'Platform account updated successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(PlatformAccount $platformAccount): RedirectResponse
|
|
||||||
{
|
|
||||||
$platformAccount->delete();
|
|
||||||
|
|
||||||
return redirect()->route('platforms.index')
|
|
||||||
->with('success', 'Platform account deleted successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setActive(PlatformAccount $platformAccount): RedirectResponse
|
|
||||||
{
|
|
||||||
$platformAccount->setAsActive();
|
|
||||||
|
|
||||||
return redirect()->route('platforms.index')
|
|
||||||
->with('success', "Set $platformAccount->username@$platformAccount->instance_url as active for {$platformAccount->platform->value}!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Models\PlatformInstance;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Support\Facades\View as ViewFacade;
|
|
||||||
|
|
||||||
class PlatformChannelsController extends Controller
|
|
||||||
{
|
|
||||||
public function index(): View
|
|
||||||
{
|
|
||||||
$channels = PlatformChannel::with('platformInstance')
|
|
||||||
->orderBy('platform_instance_id')
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return ViewFacade::make('pages.channels.index', compact('channels'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(): View
|
|
||||||
{
|
|
||||||
$instances = PlatformInstance::where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return ViewFacade::make('pages.channels.create', compact('instances'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'platform_instance_id' => 'required|exists:platform_instances,id',
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'display_name' => 'nullable|string|max:255',
|
|
||||||
'channel_id' => 'nullable|string|max:255',
|
|
||||||
'description' => 'nullable|string',
|
|
||||||
'language_id' => 'required|exists:languages,id',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Default is_active to true if not provided
|
|
||||||
$validated['is_active'] = $validated['is_active'] ?? true;
|
|
||||||
|
|
||||||
// Set display_name to name if not provided
|
|
||||||
$validated['display_name'] = $validated['display_name'] ?? $validated['name'];
|
|
||||||
|
|
||||||
PlatformChannel::create($validated);
|
|
||||||
|
|
||||||
// Check if there's a redirect_to parameter for onboarding flow
|
|
||||||
$redirectTo = $request->input('redirect_to');
|
|
||||||
if ($redirectTo) {
|
|
||||||
return redirect($redirectTo)
|
|
||||||
->with('success', 'Channel created successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('channels.index')
|
|
||||||
->with('success', 'Channel created successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(PlatformChannel $channel): View
|
|
||||||
{
|
|
||||||
$channel->load(['platformInstance', 'feeds']);
|
|
||||||
|
|
||||||
return ViewFacade::make('pages.channels.show', compact('channel'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function edit(PlatformChannel $channel): View
|
|
||||||
{
|
|
||||||
$instances = PlatformInstance::where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return ViewFacade::make('pages.channels.edit', compact('channel', 'instances'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request, PlatformChannel $channel): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'platform_instance_id' => 'required|exists:platform_instances,id',
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'display_name' => 'nullable|string|max:255',
|
|
||||||
'channel_id' => 'nullable|string|max:255',
|
|
||||||
'description' => 'nullable|string',
|
|
||||||
'language_id' => 'required|exists:languages,id',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$channel->update($validated);
|
|
||||||
|
|
||||||
return redirect()->route('channels.index')
|
|
||||||
->with('success', 'Channel updated successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(PlatformChannel $channel): RedirectResponse
|
|
||||||
{
|
|
||||||
$channel->delete();
|
|
||||||
|
|
||||||
return redirect()->route('channels.index')
|
|
||||||
->with('success', 'Channel deleted successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggle(PlatformChannel $channel): RedirectResponse
|
|
||||||
{
|
|
||||||
$newStatus = !$channel->is_active;
|
|
||||||
$channel->update(['is_active' => $newStatus]);
|
|
||||||
|
|
||||||
$status = $newStatus ? 'activated' : 'deactivated';
|
|
||||||
|
|
||||||
return redirect()->route('channels.index')
|
|
||||||
->with('success', "Channel {$status} successfully!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Feed;
|
|
||||||
use App\Models\Route;
|
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Services\RoutingValidationService;
|
|
||||||
use App\Exceptions\RoutingMismatchException;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
|
|
||||||
class RoutingController extends Controller
|
|
||||||
{
|
|
||||||
public function index(): View
|
|
||||||
{
|
|
||||||
$feeds = Feed::with(['channels.platformInstance'])
|
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$channels = PlatformChannel::with(['platformInstance', 'feeds'])
|
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return view('pages.routing.index', compact('feeds', 'channels'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(): View
|
|
||||||
{
|
|
||||||
$feeds = Feed::where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$channels = PlatformChannel::with('platformInstance')
|
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return view('pages.routing.create', compact('feeds', 'channels'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'feed_id' => 'required|exists:feeds,id',
|
|
||||||
'channel_ids' => 'required|array|min:1',
|
|
||||||
'channel_ids.*' => 'exists:platform_channels,id',
|
|
||||||
'priority' => 'integer|min:0|max:100',
|
|
||||||
'filters' => 'nullable|string'
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** @var Feed $feed */
|
|
||||||
$feed = Feed::findOrFail($validated['feed_id']);
|
|
||||||
|
|
||||||
/** @var Collection<int, PlatformChannel> $channels */
|
|
||||||
$channels = PlatformChannel::findMany($validated['channel_ids']);
|
|
||||||
$priority = $validated['priority'] ?? 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
app(RoutingValidationService::class)->validateLanguageCompatibility($feed, $channels);
|
|
||||||
} catch (RoutingMismatchException $e) {
|
|
||||||
return redirect()->back()
|
|
||||||
->withInput()
|
|
||||||
->withErrors(['language' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$filters = $this->parseJsonFilters($validated['filters'] ?? null);
|
|
||||||
|
|
||||||
// Attach channels to feed
|
|
||||||
$syncData = [];
|
|
||||||
foreach ($validated['channel_ids'] as $channelId) {
|
|
||||||
$syncData[$channelId] = [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => $priority,
|
|
||||||
'filters' => $filters,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$feed->channels()->syncWithoutDetaching($syncData);
|
|
||||||
|
|
||||||
return redirect()->route('routing.index')
|
|
||||||
->with('success', 'Feed routing created successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function edit(Feed $feed, PlatformChannel $channel): View
|
|
||||||
{
|
|
||||||
$routing = $feed->channels()
|
|
||||||
->wherePivot('platform_channel_id', $channel->id)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $routing) {
|
|
||||||
abort(404, 'Routing not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('pages.routing.edit', compact('feed', 'channel', 'routing'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request, Feed $feed, PlatformChannel $channel): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'priority' => 'integer|min:0|max:100',
|
|
||||||
'filters' => 'nullable|string'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$filters = $this->parseJsonFilters($validated['filters'] ?? null);
|
|
||||||
|
|
||||||
$feed->channels()->updateExistingPivot($channel->id, [
|
|
||||||
'is_active' => $validated['is_active'] ?? true,
|
|
||||||
'priority' => $validated['priority'] ?? 0,
|
|
||||||
'filters' => $filters,
|
|
||||||
'updated_at' => now()
|
|
||||||
]);
|
|
||||||
|
|
||||||
return redirect()->route('routing.index')
|
|
||||||
->with('success', 'Routing updated successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(Feed $feed, PlatformChannel $channel): RedirectResponse
|
|
||||||
{
|
|
||||||
$feed->channels()->detach($channel->id);
|
|
||||||
|
|
||||||
return redirect()->route('routing.index')
|
|
||||||
->with('success', 'Routing deleted successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggle(Request $request, Feed $feed, PlatformChannel $channel): RedirectResponse
|
|
||||||
{
|
|
||||||
$routing = Route::where('feed_id', $feed->id)
|
|
||||||
->where('platform_channel_id', $channel->id)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $routing) {
|
|
||||||
abort(404, 'Routing not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
$newStatus = ! $routing->is_active;
|
|
||||||
|
|
||||||
$feed->channels()->updateExistingPivot($channel->id, [
|
|
||||||
'is_active' => $newStatus,
|
|
||||||
'updated_at' => now()
|
|
||||||
]);
|
|
||||||
|
|
||||||
$status = $newStatus ? 'activated' : 'deactivated';
|
|
||||||
|
|
||||||
return redirect()->route('routing.index')
|
|
||||||
->with('success', "Routing {$status} successfully!");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>|null
|
|
||||||
*/
|
|
||||||
private function parseJsonFilters(?string $json): ?array
|
|
||||||
{
|
|
||||||
if (empty($json)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($json, true);
|
|
||||||
|
|
||||||
if (json_last_error() === JSON_ERROR_NONE) {
|
|
||||||
return $decoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class SettingsController extends Controller
|
|
||||||
{
|
|
||||||
public function index(): View
|
|
||||||
{
|
|
||||||
$articleProcessingEnabled = Setting::isArticleProcessingEnabled();
|
|
||||||
$publishingApprovalsEnabled = Setting::isPublishingApprovalsEnabled();
|
|
||||||
|
|
||||||
return view('pages.settings.index', compact('articleProcessingEnabled', 'publishingApprovalsEnabled'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'article_processing_enabled' => 'boolean',
|
|
||||||
'enable_publishing_approvals' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
Setting::setArticleProcessingEnabled($request->boolean('article_processing_enabled'));
|
|
||||||
Setting::setPublishingApprovalsEnabled($request->boolean('enable_publishing_approvals'));
|
|
||||||
|
|
||||||
// If redirected from onboarding, go to dashboard
|
|
||||||
if ($request->get('from') === 'onboarding') {
|
|
||||||
return redirect()->route('onboarding.index')
|
|
||||||
->with('success', 'System activated successfully! Welcome to Lemmy Poster.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('settings.index')
|
|
||||||
->with('success', 'Settings updated successfully.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use App\Models\Article;
|
|
||||||
use App\Models\ArticlePublication;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
|
|
||||||
class DashboardStatsService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array{articles_fetched: int, articles_published: int, published_percentage: float}
|
|
||||||
*/
|
|
||||||
public function getStats(string $period = 'today'): array
|
|
||||||
{
|
|
||||||
$dateRange = $this->getDateRange($period);
|
|
||||||
|
|
||||||
$articlesFetched = Article::when($dateRange, function ($query) use ($dateRange) {
|
|
||||||
return $query->whereBetween('created_at', $dateRange);
|
|
||||||
})->count();
|
|
||||||
|
|
||||||
$articlesPublished = ArticlePublication::when($dateRange, function ($query) use ($dateRange) {
|
|
||||||
return $query->whereBetween('published_at', $dateRange);
|
|
||||||
})->count();
|
|
||||||
|
|
||||||
$publishedPercentage = $articlesFetched > 0
|
|
||||||
? round(($articlesPublished / $articlesFetched) * 100, 1)
|
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
return [
|
|
||||||
'articles_fetched' => $articlesFetched,
|
|
||||||
'articles_published' => $articlesPublished,
|
|
||||||
'published_percentage' => $publishedPercentage,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function getAvailablePeriods(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'today' => 'Today',
|
|
||||||
'week' => 'This Week',
|
|
||||||
'month' => 'This Month',
|
|
||||||
'year' => 'This Year',
|
|
||||||
'all' => 'All Time',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{0: Carbon, 1: Carbon}|null
|
|
||||||
*/
|
|
||||||
private function getDateRange(string $period): ?array
|
|
||||||
{
|
|
||||||
$now = Carbon::now();
|
|
||||||
|
|
||||||
return match ($period) {
|
|
||||||
'today' => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
|
|
||||||
'week' => [$now->copy()->startOfWeek(), $now->copy()->endOfWeek()],
|
|
||||||
'month' => [$now->copy()->startOfMonth(), $now->copy()->endOfMonth()],
|
|
||||||
'year' => [$now->copy()->startOfYear(), $now->copy()->endOfYear()],
|
|
||||||
'all' => null, // No date filtering for all-time stats
|
|
||||||
default => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get additional stats for dashboard
|
|
||||||
* @return array{total_feeds: int, active_feeds: int, total_channels: int, active_channels: int, total_routes: int, active_routes: int}
|
|
||||||
*/
|
|
||||||
public function getSystemStats(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'total_feeds' => \App\Models\Feed::count(),
|
|
||||||
'active_feeds' => \App\Models\Feed::where('is_active', true)->count(),
|
|
||||||
'total_channels' => \App\Models\PlatformChannel::count(),
|
|
||||||
'active_channels' => \App\Models\PlatformChannel::where('is_active', true)->count(),
|
|
||||||
'total_routes' => \App\Models\Route::count(),
|
|
||||||
'active_routes' => \App\Models\Route::where('is_active', true)->count(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
backend/.gitignore
vendored
Normal file
2
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
vendor
|
||||||
|
/coverage-report-*
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App;
|
namespace App\Enums;
|
||||||
|
|
||||||
enum LogLevelEnum: string
|
enum LogLevelEnum: string
|
||||||
{
|
{
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Events;
|
namespace App\Events;
|
||||||
|
|
||||||
use App\LogLevelEnum;
|
use App\Enums\LogLevelEnum;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
74
backend/app/Http/Controllers/Api/V1/ArticlesController.php
Normal file
74
backend/app/Http/Controllers/Api/V1/ArticlesController.php
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Resources\ArticleResource;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ArticlesController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of articles
|
||||||
|
*/
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$perPage = min($request->get('per_page', 15), 100); // Max 100 items per page
|
||||||
|
$articles = Article::with(['feed', 'articlePublication'])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->paginate($perPage);
|
||||||
|
|
||||||
|
$publishingApprovalsEnabled = Setting::isPublishingApprovalsEnabled();
|
||||||
|
|
||||||
|
return $this->sendResponse([
|
||||||
|
'articles' => ArticleResource::collection($articles->items()),
|
||||||
|
'pagination' => [
|
||||||
|
'current_page' => $articles->currentPage(),
|
||||||
|
'last_page' => $articles->lastPage(),
|
||||||
|
'per_page' => $articles->perPage(),
|
||||||
|
'total' => $articles->total(),
|
||||||
|
'from' => $articles->firstItem(),
|
||||||
|
'to' => $articles->lastItem(),
|
||||||
|
],
|
||||||
|
'settings' => [
|
||||||
|
'publishing_approvals_enabled' => $publishingApprovalsEnabled,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve an article
|
||||||
|
*/
|
||||||
|
public function approve(Article $article): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$article->approve('manual');
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new ArticleResource($article->fresh(['feed', 'articlePublication'])),
|
||||||
|
'Article approved and queued for publishing.'
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to approve article: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject an article
|
||||||
|
*/
|
||||||
|
public function reject(Article $article): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$article->reject('manual');
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new ArticleResource($article->fresh(['feed', 'articlePublication'])),
|
||||||
|
'Article rejected.'
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to reject article: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
backend/app/Http/Controllers/Api/V1/AuthController.php
Normal file
112
backend/app/Http/Controllers/Api/V1/AuthController.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class AuthController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Login user and create token
|
||||||
|
*/
|
||||||
|
public function login(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
'password' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::where('email', $request->email)->first();
|
||||||
|
|
||||||
|
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||||
|
return $this->sendError('Invalid credentials', [], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $user->createToken('api-token')->plainTextToken;
|
||||||
|
|
||||||
|
return $this->sendResponse([
|
||||||
|
'user' => [
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => $user->name,
|
||||||
|
'email' => $user->email,
|
||||||
|
],
|
||||||
|
'token' => $token,
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
], 'Login successful');
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Login failed: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new user
|
||||||
|
*/
|
||||||
|
public function register(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|string|email|max:255|unique:users',
|
||||||
|
'password' => 'required|string|min:8|confirmed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'email' => $validated['email'],
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $user->createToken('api-token')->plainTextToken;
|
||||||
|
|
||||||
|
return $this->sendResponse([
|
||||||
|
'user' => [
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => $user->name,
|
||||||
|
'email' => $user->email,
|
||||||
|
],
|
||||||
|
'token' => $token,
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
], 'Registration successful', 201);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Registration failed: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout user (revoke token)
|
||||||
|
*/
|
||||||
|
public function logout(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request->user()->currentAccessToken()->delete();
|
||||||
|
|
||||||
|
return $this->sendResponse(null, 'Logged out successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Logout failed: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current authenticated user
|
||||||
|
*/
|
||||||
|
public function me(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->sendResponse([
|
||||||
|
'user' => [
|
||||||
|
'id' => $request->user()->id,
|
||||||
|
'name' => $request->user()->name,
|
||||||
|
'email' => $request->user()->email,
|
||||||
|
],
|
||||||
|
], 'User retrieved successfully');
|
||||||
|
}
|
||||||
|
}
|
||||||
64
backend/app/Http/Controllers/Api/V1/BaseController.php
Normal file
64
backend/app/Http/Controllers/Api/V1/BaseController.php
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class BaseController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Success response method
|
||||||
|
*/
|
||||||
|
public function sendResponse(mixed $result, string $message = 'Success', int $code = 200): JsonResponse
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $result,
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($response, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error response method
|
||||||
|
*/
|
||||||
|
public function sendError(string $error, array $errorMessages = [], int $code = 400): JsonResponse
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $error,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($errorMessages)) {
|
||||||
|
$response['errors'] = $errorMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($response, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation error response method
|
||||||
|
*/
|
||||||
|
public function sendValidationError(array $errors): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->sendError('Validation failed', $errors, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not found response method
|
||||||
|
*/
|
||||||
|
public function sendNotFound(string $message = 'Resource not found'): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->sendError($message, [], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unauthorized response method
|
||||||
|
*/
|
||||||
|
public function sendUnauthorized(string $message = 'Unauthorized'): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->sendError($message, [], 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
backend/app/Http/Controllers/Api/V1/DashboardController.php
Normal file
46
backend/app/Http/Controllers/Api/V1/DashboardController.php
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Services\DashboardStatsService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class DashboardController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private DashboardStatsService $dashboardStatsService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dashboard statistics
|
||||||
|
*/
|
||||||
|
public function stats(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$period = $request->get('period', 'today');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get article stats from service
|
||||||
|
$articleStats = $this->dashboardStatsService->getStats($period);
|
||||||
|
|
||||||
|
// Get system stats
|
||||||
|
$systemStats = $this->dashboardStatsService->getSystemStats();
|
||||||
|
|
||||||
|
// Get available periods
|
||||||
|
$availablePeriods = $this->dashboardStatsService->getAvailablePeriods();
|
||||||
|
|
||||||
|
return $this->sendResponse([
|
||||||
|
'article_stats' => $articleStats,
|
||||||
|
'system_stats' => $systemStats,
|
||||||
|
'available_periods' => $availablePeriods,
|
||||||
|
'current_period' => $period,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to fetch dashboard stats: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
backend/app/Http/Controllers/Api/V1/FeedsController.php
Normal file
133
backend/app/Http/Controllers/Api/V1/FeedsController.php
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Requests\StoreFeedRequest;
|
||||||
|
use App\Http\Requests\UpdateFeedRequest;
|
||||||
|
use App\Http\Resources\FeedResource;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class FeedsController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of feeds
|
||||||
|
*/
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$perPage = min($request->get('per_page', 15), 100);
|
||||||
|
|
||||||
|
$feeds = Feed::with(['language'])
|
||||||
|
->withCount('articles')
|
||||||
|
->orderBy('is_active', 'desc')
|
||||||
|
->orderBy('name')
|
||||||
|
->paginate($perPage);
|
||||||
|
|
||||||
|
return $this->sendResponse([
|
||||||
|
'feeds' => FeedResource::collection($feeds->items()),
|
||||||
|
'pagination' => [
|
||||||
|
'current_page' => $feeds->currentPage(),
|
||||||
|
'last_page' => $feeds->lastPage(),
|
||||||
|
'per_page' => $feeds->perPage(),
|
||||||
|
'total' => $feeds->total(),
|
||||||
|
'from' => $feeds->firstItem(),
|
||||||
|
'to' => $feeds->lastItem(),
|
||||||
|
]
|
||||||
|
], 'Feeds retrieved successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created feed
|
||||||
|
*/
|
||||||
|
public function store(StoreFeedRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validated();
|
||||||
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
|
|
||||||
|
$feed = Feed::create($validated);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new FeedResource($feed),
|
||||||
|
'Feed created successfully!',
|
||||||
|
201
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to create feed: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified feed
|
||||||
|
*/
|
||||||
|
public function show(Feed $feed): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->sendResponse(
|
||||||
|
new FeedResource($feed),
|
||||||
|
'Feed retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified feed
|
||||||
|
*/
|
||||||
|
public function update(UpdateFeedRequest $request, Feed $feed): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validated();
|
||||||
|
$validated['is_active'] = $validated['is_active'] ?? $feed->is_active;
|
||||||
|
|
||||||
|
$feed->update($validated);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new FeedResource($feed->fresh()),
|
||||||
|
'Feed updated successfully!'
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to update feed: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified feed
|
||||||
|
*/
|
||||||
|
public function destroy(Feed $feed): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$feed->delete();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
null,
|
||||||
|
'Feed deleted successfully!'
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to delete feed: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle feed active status
|
||||||
|
*/
|
||||||
|
public function toggle(Feed $feed): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$newStatus = !$feed->is_active;
|
||||||
|
$feed->update(['is_active' => $newStatus]);
|
||||||
|
|
||||||
|
$status = $newStatus ? 'activated' : 'deactivated';
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new FeedResource($feed->fresh()),
|
||||||
|
"Feed {$status} successfully!"
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to toggle feed status: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
backend/app/Http/Controllers/Api/V1/LogsController.php
Normal file
43
backend/app/Http/Controllers/Api/V1/LogsController.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Models\Log;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LogsController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of logs
|
||||||
|
*/
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$perPage = min($request->get('per_page', 20), 100);
|
||||||
|
$level = $request->get('level');
|
||||||
|
|
||||||
|
$query = Log::orderBy('created_at', 'desc');
|
||||||
|
|
||||||
|
if ($level) {
|
||||||
|
$query->where('level', $level);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $query->paginate($perPage);
|
||||||
|
|
||||||
|
return $this->sendResponse([
|
||||||
|
'logs' => $logs->items(),
|
||||||
|
'pagination' => [
|
||||||
|
'current_page' => $logs->currentPage(),
|
||||||
|
'last_page' => $logs->lastPage(),
|
||||||
|
'per_page' => $logs->perPage(),
|
||||||
|
'total' => $logs->total(),
|
||||||
|
'from' => $logs->firstItem(),
|
||||||
|
'to' => $logs->lastItem(),
|
||||||
|
],
|
||||||
|
], 'Logs retrieved successfully.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to retrieve logs: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Enums\PlatformEnum;
|
||||||
|
use App\Http\Resources\PlatformAccountResource;
|
||||||
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Models\PlatformInstance;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class PlatformAccountsController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of platform accounts
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$accounts = PlatformAccount::orderBy('platform')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
PlatformAccountResource::collection($accounts),
|
||||||
|
'Platform accounts retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created platform account
|
||||||
|
*/
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'platform' => 'required|in:lemmy,mastodon,reddit',
|
||||||
|
'instance_url' => 'required|url',
|
||||||
|
'username' => 'required|string|max:255',
|
||||||
|
'password' => 'required|string',
|
||||||
|
'settings' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create or find platform instance
|
||||||
|
$platformEnum = PlatformEnum::from($validated['platform']);
|
||||||
|
$instance = PlatformInstance::firstOrCreate([
|
||||||
|
'platform' => $platformEnum,
|
||||||
|
'url' => $validated['instance_url'],
|
||||||
|
], [
|
||||||
|
'name' => parse_url($validated['instance_url'], PHP_URL_HOST),
|
||||||
|
'description' => ucfirst($validated['platform']) . ' instance',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$account = PlatformAccount::create($validated);
|
||||||
|
|
||||||
|
// If this is the first account for this platform, make it active
|
||||||
|
if (!PlatformAccount::where('platform', $validated['platform'])
|
||||||
|
->where('is_active', true)
|
||||||
|
->exists()) {
|
||||||
|
$account->setAsActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformAccountResource($account),
|
||||||
|
'Platform account created successfully!',
|
||||||
|
201
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to create platform account: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified platform account
|
||||||
|
*/
|
||||||
|
public function show(PlatformAccount $platformAccount): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformAccountResource($platformAccount),
|
||||||
|
'Platform account retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified platform account
|
||||||
|
*/
|
||||||
|
public function update(Request $request, PlatformAccount $platformAccount): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'instance_url' => 'required|url',
|
||||||
|
'username' => 'required|string|max:255',
|
||||||
|
'password' => 'nullable|string',
|
||||||
|
'settings' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Don't update password if not provided
|
||||||
|
if (empty($validated['password'])) {
|
||||||
|
unset($validated['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$platformAccount->update($validated);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformAccountResource($platformAccount->fresh()),
|
||||||
|
'Platform account updated successfully!'
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to update platform account: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified platform account
|
||||||
|
*/
|
||||||
|
public function destroy(PlatformAccount $platformAccount): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$platformAccount->delete();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
null,
|
||||||
|
'Platform account deleted successfully!'
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to delete platform account: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set platform account as active
|
||||||
|
*/
|
||||||
|
public function setActive(PlatformAccount $platformAccount): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$platformAccount->setAsActive();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformAccountResource($platformAccount->fresh()),
|
||||||
|
"Set {$platformAccount->username}@{$platformAccount->instance_url} as active for {$platformAccount->platform->value}!"
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to set platform account as active: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Resources\PlatformChannelResource;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class PlatformChannelsController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of platform channels
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$channels = PlatformChannel::with(['platformInstance'])
|
||||||
|
->orderBy('is_active', 'desc')
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
PlatformChannelResource::collection($channels),
|
||||||
|
'Platform channels retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created platform channel
|
||||||
|
*/
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'platform_instance_id' => 'required|exists:platform_instances,id',
|
||||||
|
'channel_id' => 'required|string|max:255',
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'display_name' => 'nullable|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
|
|
||||||
|
$channel = PlatformChannel::create($validated);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformChannelResource($channel->load('platformInstance')),
|
||||||
|
'Platform channel created successfully!',
|
||||||
|
201
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to create platform channel: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified platform channel
|
||||||
|
*/
|
||||||
|
public function show(PlatformChannel $platformChannel): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformChannelResource($platformChannel->load('platformInstance')),
|
||||||
|
'Platform channel retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified platform channel
|
||||||
|
*/
|
||||||
|
public function update(Request $request, PlatformChannel $platformChannel): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'display_name' => 'nullable|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$platformChannel->update($validated);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformChannelResource($platformChannel->fresh(['platformInstance'])),
|
||||||
|
'Platform channel updated successfully!'
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to update platform channel: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified platform channel
|
||||||
|
*/
|
||||||
|
public function destroy(PlatformChannel $platformChannel): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$platformChannel->delete();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
null,
|
||||||
|
'Platform channel deleted successfully!'
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to delete platform channel: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle platform channel active status
|
||||||
|
*/
|
||||||
|
public function toggle(PlatformChannel $channel): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$newStatus = !$channel->is_active;
|
||||||
|
$channel->update(['is_active' => $newStatus]);
|
||||||
|
|
||||||
|
$status = $newStatus ? 'activated' : 'deactivated';
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformChannelResource($channel->fresh(['platformInstance'])),
|
||||||
|
"Platform channel {$status} successfully!"
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to toggle platform channel status: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
backend/app/Http/Controllers/Api/V1/RoutingController.php
Normal file
174
backend/app/Http/Controllers/Api/V1/RoutingController.php
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Resources\RouteResource;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Route;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class RoutingController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of routing configurations
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$routes = Route::with(['feed', 'platformChannel'])
|
||||||
|
->orderBy('is_active', 'desc')
|
||||||
|
->orderBy('priority', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
RouteResource::collection($routes),
|
||||||
|
'Routing configurations retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created routing configuration
|
||||||
|
*/
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'feed_id' => 'required|exists:feeds,id',
|
||||||
|
'platform_channel_id' => 'required|exists:platform_channels,id',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'priority' => 'nullable|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
|
$validated['priority'] = $validated['priority'] ?? 0;
|
||||||
|
|
||||||
|
$route = Route::create($validated);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new RouteResource($route->load(['feed', 'platformChannel'])),
|
||||||
|
'Routing configuration created successfully!',
|
||||||
|
201
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to create routing configuration: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified routing configuration
|
||||||
|
*/
|
||||||
|
public function show(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
|
{
|
||||||
|
$route = $this->findRoute($feed, $channel);
|
||||||
|
|
||||||
|
if (!$route) {
|
||||||
|
return $this->sendNotFound('Routing configuration not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$route->load(['feed', 'platformChannel']);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new RouteResource($route),
|
||||||
|
'Routing configuration retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified routing configuration
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$route = $this->findRoute($feed, $channel);
|
||||||
|
|
||||||
|
if (!$route) {
|
||||||
|
return $this->sendNotFound('Routing configuration not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'priority' => 'nullable|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Route::where('feed_id', $feed->id)
|
||||||
|
->where('platform_channel_id', $channel->id)
|
||||||
|
->update($validated);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new RouteResource($route->fresh(['feed', 'platformChannel'])),
|
||||||
|
'Routing configuration updated successfully!'
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to update routing configuration: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified routing configuration
|
||||||
|
*/
|
||||||
|
public function destroy(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$route = $this->findRoute($feed, $channel);
|
||||||
|
|
||||||
|
if (!$route) {
|
||||||
|
return $this->sendNotFound('Routing configuration not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Route::where('feed_id', $feed->id)
|
||||||
|
->where('platform_channel_id', $channel->id)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
null,
|
||||||
|
'Routing configuration deleted successfully!'
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to delete routing configuration: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle routing configuration active status
|
||||||
|
*/
|
||||||
|
public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$route = $this->findRoute($feed, $channel);
|
||||||
|
|
||||||
|
if (!$route) {
|
||||||
|
return $this->sendNotFound('Routing configuration not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$newStatus = !$route->is_active;
|
||||||
|
Route::where('feed_id', $feed->id)
|
||||||
|
->where('platform_channel_id', $channel->id)
|
||||||
|
->update(['is_active' => $newStatus]);
|
||||||
|
|
||||||
|
$status = $newStatus ? 'activated' : 'deactivated';
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new RouteResource($route->fresh(['feed', 'platformChannel'])),
|
||||||
|
"Routing configuration {$status} successfully!"
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to toggle routing configuration status: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a route by feed and channel
|
||||||
|
*/
|
||||||
|
private function findRoute(Feed $feed, PlatformChannel $channel): ?Route
|
||||||
|
{
|
||||||
|
return Route::where('feed_id', $feed->id)
|
||||||
|
->where('platform_channel_id', $channel->id)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
63
backend/app/Http/Controllers/Api/V1/SettingsController.php
Normal file
63
backend/app/Http/Controllers/Api/V1/SettingsController.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class SettingsController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display current settings
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$settings = [
|
||||||
|
'article_processing_enabled' => Setting::isArticleProcessingEnabled(),
|
||||||
|
'publishing_approvals_enabled' => Setting::isPublishingApprovalsEnabled(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->sendResponse($settings, 'Settings retrieved successfully.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to retrieve settings: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update settings
|
||||||
|
*/
|
||||||
|
public function update(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'article_processing_enabled' => 'boolean',
|
||||||
|
'enable_publishing_approvals' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isset($validated['article_processing_enabled'])) {
|
||||||
|
Setting::setArticleProcessingEnabled($validated['article_processing_enabled']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($validated['enable_publishing_approvals'])) {
|
||||||
|
Setting::setPublishingApprovalsEnabled($validated['enable_publishing_approvals']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedSettings = [
|
||||||
|
'article_processing_enabled' => Setting::isArticleProcessingEnabled(),
|
||||||
|
'publishing_approvals_enabled' => Setting::isPublishingApprovalsEnabled(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$updatedSettings,
|
||||||
|
'Settings updated successfully.'
|
||||||
|
);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->sendError('Failed to update settings: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
backend/app/Http/Resources/ArticlePublicationResource.php
Normal file
26
backend/app/Http/Resources/ArticlePublicationResource.php
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class ArticlePublicationResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'article_id' => $this->article_id,
|
||||||
|
'status' => $this->status,
|
||||||
|
'published_at' => $this->published_at?->toISOString(),
|
||||||
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend/app/Http/Resources/ArticleResource.php
Normal file
36
backend/app/Http/Resources/ArticleResource.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class ArticleResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'feed_id' => $this->feed_id,
|
||||||
|
'url' => $this->url,
|
||||||
|
'title' => $this->title,
|
||||||
|
'description' => $this->description,
|
||||||
|
'is_valid' => $this->is_valid,
|
||||||
|
'is_duplicate' => $this->is_duplicate,
|
||||||
|
'approval_status' => $this->approval_status,
|
||||||
|
'approved_at' => $this->approved_at?->toISOString(),
|
||||||
|
'approved_by' => $this->approved_by,
|
||||||
|
'fetched_at' => $this->fetched_at?->toISOString(),
|
||||||
|
'validated_at' => $this->validated_at?->toISOString(),
|
||||||
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
|
'feed' => new FeedResource($this->whenLoaded('feed')),
|
||||||
|
'article_publication' => new ArticlePublicationResource($this->whenLoaded('articlePublication')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
backend/app/Http/Resources/FeedResource.php
Normal file
32
backend/app/Http/Resources/FeedResource.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class FeedResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'url' => $this->url,
|
||||||
|
'type' => $this->type,
|
||||||
|
'is_active' => $this->is_active,
|
||||||
|
'description' => $this->description,
|
||||||
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
|
'articles_count' => $this->when(
|
||||||
|
$request->routeIs('api.feeds.*') && isset($this->articles_count),
|
||||||
|
$this->articles_count
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
backend/app/Http/Resources/PlatformAccountResource.php
Normal file
31
backend/app/Http/Resources/PlatformAccountResource.php
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PlatformAccountResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'platform' => $this->platform->value,
|
||||||
|
'instance_url' => $this->instance_url,
|
||||||
|
'username' => $this->username,
|
||||||
|
'settings' => $this->settings,
|
||||||
|
'is_active' => $this->is_active,
|
||||||
|
'last_tested_at' => $this->last_tested_at?->toISOString(),
|
||||||
|
'status' => $this->status,
|
||||||
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
|
'channels' => PlatformChannelResource::collection($this->whenLoaded('channels')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
backend/app/Http/Resources/PlatformChannelResource.php
Normal file
31
backend/app/Http/Resources/PlatformChannelResource.php
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PlatformChannelResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'platform_instance_id' => $this->platform_instance_id,
|
||||||
|
'channel_id' => $this->channel_id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'display_name' => $this->display_name,
|
||||||
|
'description' => $this->description,
|
||||||
|
'is_active' => $this->is_active,
|
||||||
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
|
'platform_instance' => new PlatformInstanceResource($this->whenLoaded('platformInstance')),
|
||||||
|
'routes' => RouteResource::collection($this->whenLoaded('routes')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
27
backend/app/Http/Resources/PlatformInstanceResource.php
Normal file
27
backend/app/Http/Resources/PlatformInstanceResource.php
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PlatformInstanceResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'url' => $this->url,
|
||||||
|
'description' => $this->description,
|
||||||
|
'is_active' => $this->is_active,
|
||||||
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
29
backend/app/Http/Resources/RouteResource.php
Normal file
29
backend/app/Http/Resources/RouteResource.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class RouteResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'feed_id' => $this->feed_id,
|
||||||
|
'platform_channel_id' => $this->platform_channel_id,
|
||||||
|
'is_active' => $this->is_active,
|
||||||
|
'priority' => $this->priority,
|
||||||
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
|
'feed' => new FeedResource($this->whenLoaded('feed')),
|
||||||
|
'platform_channel' => new PlatformChannelResource($this->whenLoaded('platformChannel')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,9 @@ class PublishToLemmyJob implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
|
public int $tries = 3;
|
||||||
|
public array $backoff = [60, 120, 300];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Article $article
|
private readonly Article $article
|
||||||
) {
|
) {
|
||||||
|
|
@ -22,7 +22,7 @@ class SyncChannelPostsJob implements ShouldQueue, ShouldBeUnique
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PlatformChannel $channel
|
private readonly PlatformChannel $channel
|
||||||
) {
|
) {
|
||||||
$this->onQueue('lemmy-posts');
|
$this->onQueue('sync');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function dispatchForAllActiveChannels(): void
|
public static function dispatchForAllActiveChannels(): void
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\ArticlePublicationFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
|
@ -14,6 +16,9 @@
|
||||||
*/
|
*/
|
||||||
class ArticlePublication extends Model
|
class ArticlePublication extends Model
|
||||||
{
|
{
|
||||||
|
/** @use HasFactory<ArticlePublicationFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'article_id',
|
'article_id',
|
||||||
'platform_channel_id',
|
'platform_channel_id',
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
@ -19,6 +20,8 @@
|
||||||
*/
|
*/
|
||||||
class Keyword extends Model
|
class Keyword extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'feed_id',
|
'feed_id',
|
||||||
'platform_channel_id',
|
'platform_channel_id',
|
||||||
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\LogLevelEnum;
|
use App\Enums\LogLevelEnum;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method static create(array<string, mixed> $array)
|
* @method static create(array $array)
|
||||||
* @property LogLevelEnum $level
|
* @property LogLevelEnum $level
|
||||||
* @property string $message
|
* @property string $message
|
||||||
* @property array<string, mixed> $context
|
* @property array<string, mixed> $context
|
||||||
|
|
@ -16,6 +17,8 @@
|
||||||
*/
|
*/
|
||||||
class Log extends Model
|
class Log extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'logs';
|
protected $table = 'logs';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|
@ -63,7 +63,7 @@ public function platformAccounts(): BelongsToMany
|
||||||
*/
|
*/
|
||||||
public function activePlatformAccounts(): BelongsToMany
|
public function activePlatformAccounts(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->platformAccounts()->where('is_active', true);
|
return $this->platformAccounts()->wherePivot('is_active', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFullNameAttribute(): string
|
public function getFullNameAttribute(): string
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\RouteFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -16,10 +18,15 @@
|
||||||
* @property Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property Carbon $updated_at
|
* @property Carbon $updated_at
|
||||||
*/
|
*/
|
||||||
class Route extends Pivot
|
class Route extends Model
|
||||||
{
|
{
|
||||||
|
/** @use HasFactory<RouteFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'routes';
|
protected $table = 'routes';
|
||||||
|
|
||||||
|
// Laravel doesn't handle composite primary keys well, so we'll use regular queries
|
||||||
|
protected $primaryKey = null;
|
||||||
public $incrementing = false;
|
public $incrementing = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|
@ -2,10 +2,18 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static updateOrCreate(string[] $array, array $array1)
|
||||||
|
* @method static create(string[] $array)
|
||||||
|
* @method static where(string $string, string $key)
|
||||||
|
*/
|
||||||
class Setting extends Model
|
class Setting extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = ['key', 'value'];
|
protected $fillable = ['key', 'value'];
|
||||||
|
|
||||||
public static function get(string $key, mixed $default = null): mixed
|
public static function get(string $key, mixed $default = null): mixed
|
||||||
|
|
@ -6,11 +6,12 @@
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable, HasApiTokens;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
use App\Events\ExceptionOccurred;
|
use App\Events\ExceptionOccurred;
|
||||||
use App\Listeners\LogExceptionToDatabase;
|
use App\Listeners\LogExceptionToDatabase;
|
||||||
use App\LogLevelEnum;
|
|
||||||
use Error;
|
use Error;
|
||||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
@ -25,6 +25,22 @@ public function boot(): void
|
||||||
LogExceptionToDatabase::class,
|
LogExceptionToDatabase::class,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
\App\Events\NewArticleFetched::class,
|
||||||
|
\App\Listeners\ValidateArticleListener::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
\App\Events\ArticleApproved::class,
|
||||||
|
\App\Listeners\PublishApprovedArticle::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
\App\Events\ArticleReadyToPublish::class,
|
||||||
|
\App\Listeners\PublishArticle::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
app()->make(ExceptionHandler::class)
|
app()->make(ExceptionHandler::class)
|
||||||
->reportable(function (Throwable $e) {
|
->reportable(function (Throwable $e) {
|
||||||
$level = $this->mapExceptionToLogLevel($e);
|
$level = $this->mapExceptionToLogLevel($e);
|
||||||
111
backend/app/Services/DashboardStatsService.php
Normal file
111
backend/app/Services/DashboardStatsService.php
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\ArticlePublication;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class DashboardStatsService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getStats(string $period = 'today'): array
|
||||||
|
{
|
||||||
|
$dateRange = $this->getDateRange($period);
|
||||||
|
|
||||||
|
// Get articles fetched for the period
|
||||||
|
$articlesFetchedQuery = Article::query();
|
||||||
|
if ($dateRange) {
|
||||||
|
$articlesFetchedQuery->whereBetween('created_at', $dateRange);
|
||||||
|
}
|
||||||
|
$articlesFetched = $articlesFetchedQuery->count();
|
||||||
|
|
||||||
|
// Get articles published for the period
|
||||||
|
$articlesPublishedQuery = ArticlePublication::query()
|
||||||
|
->whereNotNull('published_at');
|
||||||
|
if ($dateRange) {
|
||||||
|
$articlesPublishedQuery->whereBetween('published_at', $dateRange);
|
||||||
|
}
|
||||||
|
$articlesPublished = $articlesPublishedQuery->count();
|
||||||
|
|
||||||
|
// Calculate published percentage
|
||||||
|
$publishedPercentage = $articlesFetched > 0 ? round(($articlesPublished / $articlesFetched) * 100, 1) : 0.0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'articles_fetched' => $articlesFetched,
|
||||||
|
'articles_published' => $articlesPublished,
|
||||||
|
'published_percentage' => $publishedPercentage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function getAvailablePeriods(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'today' => 'Today',
|
||||||
|
'week' => 'This Week',
|
||||||
|
'month' => 'This Month',
|
||||||
|
'year' => 'This Year',
|
||||||
|
'all' => 'All Time',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: Carbon, 1: Carbon}|null
|
||||||
|
*/
|
||||||
|
private function getDateRange(string $period): ?array
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
return match ($period) {
|
||||||
|
'today' => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
|
||||||
|
'week' => [$now->copy()->startOfWeek(), $now->copy()->endOfWeek()],
|
||||||
|
'month' => [$now->copy()->startOfMonth(), $now->copy()->endOfMonth()],
|
||||||
|
'year' => [$now->copy()->startOfYear(), $now->copy()->endOfYear()],
|
||||||
|
'all' => null, // No date filtering for all-time stats
|
||||||
|
default => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get additional stats for dashboard
|
||||||
|
*/
|
||||||
|
public function getSystemStats(): array
|
||||||
|
{
|
||||||
|
// Optimize with single queries using conditional aggregation
|
||||||
|
$feedStats = DB::table('feeds')
|
||||||
|
->selectRaw('
|
||||||
|
COUNT(*) as total_feeds,
|
||||||
|
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_feeds
|
||||||
|
')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$channelStats = DB::table('platform_channels')
|
||||||
|
->selectRaw('
|
||||||
|
COUNT(*) as total_channels,
|
||||||
|
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_channels
|
||||||
|
')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$routeStats = DB::table('routes')
|
||||||
|
->selectRaw('
|
||||||
|
COUNT(*) as total_routes,
|
||||||
|
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_routes
|
||||||
|
')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_feeds' => $feedStats->total_feeds,
|
||||||
|
'active_feeds' => $feedStats->active_feeds,
|
||||||
|
'total_channels' => $channelStats->total_channels,
|
||||||
|
'active_channels' => $channelStats->active_channels,
|
||||||
|
'total_routes' => $routeStats->total_routes,
|
||||||
|
'active_routes' => $routeStats->active_routes,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,9 @@ class ArticleParserFactory
|
||||||
BelgaArticleParser::class,
|
BelgaArticleParser::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
public static function getParser(string $url): ArticleParserInterface
|
public static function getParser(string $url): ArticleParserInterface
|
||||||
{
|
{
|
||||||
foreach (self::$parsers as $parserClass) {
|
foreach (self::$parsers as $parserClass) {
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
|
|
||||||
class HttpFetcher
|
class HttpFetcher
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
public static function fetchHtml(string $url): string
|
public static function fetchHtml(string $url): string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -22,6 +25,7 @@ public static function fetchHtml(string $url): string
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,11 +44,9 @@ public static function fetchMultipleUrls(array $urls): array
|
||||||
});
|
});
|
||||||
|
|
||||||
return collect($responses)
|
return collect($responses)
|
||||||
|
->filter(fn($response, $index) => isset($urls[$index]))
|
||||||
|
->reject(fn($response, $index) => $response instanceof Exception)
|
||||||
->map(function ($response, $index) use ($urls) {
|
->map(function ($response, $index) use ($urls) {
|
||||||
if (!isset($urls[$index])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = $urls[$index];
|
$url = $urls[$index];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -75,6 +77,7 @@ public static function fetchMultipleUrls(array $urls): array
|
||||||
->toArray();
|
->toArray();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]);
|
logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Services\Log;
|
namespace App\Services\Log;
|
||||||
|
|
||||||
use App\LogLevelEnum;
|
use App\Enums\LogLevelEnum;
|
||||||
use App\Models\Log;
|
use App\Models\Log;
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue