Toolvana: production-ready media tools platform (CodeIgniter 4)

- 134-tool registry with programmatic SEO (unique titles/H1/descriptions,
  JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter)
- DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD),
  Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG)
- Security: SSRF guard, MIME validation, rate limits, API-key auth,
  bcrypt admin login, security headers
- Admin panel: dashboard, tools/categories/guides CRUD, SEO audit,
  analytics, job inspector with retry, system health, feature flags
- Docker deployment (nginx + web/api FPM pools + scalable workers),
  PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
This commit is contained in:
deepseek
2026-08-23 07:10:30 +00:00
commit beaf0e1f37
217 changed files with 19619 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Controllers\Admin;
use CodeIgniter\HTTP\ResponseInterface;
/** Tools CRUD: list, create (with generated SEO content), edit, delete. */
final class ToolsAdmin extends AdminBase
{
public function index(): string
{
$q = trim((string) $this->request->getGet('q'));
$query = $this->toolsModel()->orderBy('name', 'ASC')->limit(300);
if ($q !== '') {
$query->like('name', $q)->orLike('slug', $q);
}
return $this->render('tools_index', [
'tools' => $query->findAll(),
'categories' => $this->categories(),
'q' => $q,
]);
}
public function create(): string
{
return $this->render('tools_form', [
'tool' => null,
'categories' => $this->categories(),
]);
}
public function store(): ResponseInterface
{
return $this->save(null);
}
public function edit(int $id): string
{
$tool = $this->toolsModel()->find($id) ?? throw new \CodeIgniter\Exceptions\PageNotFoundException();
return $this->render('tools_form', [
'tool' => $tool,
'categories' => $this->categories(),
]);
}
public function update(int $id): ResponseInterface
{
return $this->save($id);
}
private function save(?int $id): ResponseInterface
{
$post = $this->request->getPost();
$catId = (int) ($post['category_id'] ?? 0);
try {
$row = $this->toolFromInput((array) $post, $catId);
} catch (\Throwable $e) {
session()->setFlashdata('error', 'Could not build the tool: ' . $e->getMessage());
return redirect()->back()->withInput();
}
// pre-publish SEO validation (critical checks only)
$problems = [];
if (trim((string) $row['seo_title']) === '') {
$problems[] = 'SEO title missing';
}
if (mb_strlen((string) $row['seo_description']) < 50) {
$problems[] = 'SEO description too short (<50 chars)';
}
if (trim((string) $row['h1']) === '') {
$problems[] = 'H1 missing';
}
if ($problems !== [] && ($row['status'] ?? '') === 'active') {
session()->setFlashdata('error', 'Blocked from publishing: ' . implode('; ', $problems));
session()->setFlashdata('form_data', $post);
return redirect()->to($id ? '/admin/tools/' . $id . '/edit' : '/admin/tools/new')->withInput();
}
if ($id !== null) {
$this->toolsModel()->update($id, $row);
session()->setFlashdata('success', 'Tool updated.');
} else {
$this->toolsModel()->insert($row);
session()->setFlashdata('success', 'Tool created.');
}
return redirect()->to('/admin/tools');
}
public function delete(int $id): ResponseInterface
{
$this->toolsModel()->delete($id);
service('cache')->clear();
return redirect()->to('/admin/tools');
}
}