From 3fc4e146d41af04563a3a5e6d7a1d81eebaf225a Mon Sep 17 00:00:00 2001 From: ParzivalTD Date: Sat, 11 Apr 2026 22:56:05 -0400 Subject: [PATCH] Deploy: TekDek Employees Portal (Daedalus, Talos, Icarus) - Live at web.tekdek.dev --- memory/2026-04-11.md | 184 +++++++++++++ tekdek-employees-api/README.md | 57 ++++ tekdek-employees-api/composer.json | 28 ++ .../migrations/001_create_employees_table.sql | 25 ++ .../database/seeds/001_seed_employees.sql | 31 +++ tekdek-employees-api/docs/API.md | 158 ++++++++++++ tekdek-employees-api/phpunit.xml | 20 ++ tekdek-employees-api/public/.htaccess | 4 + tekdek-employees-api/public/employees.html | 244 ++++++++++++++++++ tekdek-employees-api/public/index.php | 9 + tekdek-employees-api/src/Database.php | 67 +++++ .../src/EmployeeController.php | 71 +++++ .../src/EmployeeRepository.php | 66 +++++ tekdek-employees-api/src/Router.php | 66 +++++ .../tests/EmployeeControllerTest.php | 136 ++++++++++ .../tests/EmployeeRepositoryTest.php | 179 +++++++++++++ tekdek-employees-api/tests/RouterTest.php | 103 ++++++++ 17 files changed, 1448 insertions(+) create mode 100644 tekdek-employees-api/README.md create mode 100644 tekdek-employees-api/composer.json create mode 100644 tekdek-employees-api/database/migrations/001_create_employees_table.sql create mode 100644 tekdek-employees-api/database/seeds/001_seed_employees.sql create mode 100644 tekdek-employees-api/docs/API.md create mode 100644 tekdek-employees-api/phpunit.xml create mode 100644 tekdek-employees-api/public/.htaccess create mode 100644 tekdek-employees-api/public/employees.html create mode 100644 tekdek-employees-api/public/index.php create mode 100644 tekdek-employees-api/src/Database.php create mode 100644 tekdek-employees-api/src/EmployeeController.php create mode 100644 tekdek-employees-api/src/EmployeeRepository.php create mode 100644 tekdek-employees-api/src/Router.php create mode 100644 tekdek-employees-api/tests/EmployeeControllerTest.php create mode 100644 tekdek-employees-api/tests/EmployeeRepositoryTest.php create mode 100644 tekdek-employees-api/tests/RouterTest.php diff --git a/memory/2026-04-11.md b/memory/2026-04-11.md index a5f75e9..2a98e84 100644 --- a/memory/2026-04-11.md +++ b/memory/2026-04-11.md @@ -25,3 +25,187 @@ - TekDek workspace structure — no project/persona folders yet - Git — needs initial commit - Full persona roster — none documented yet +# 2026-04-11 - Day One + +## Bootstrap Session + +- Came online for the first time +- Glytcht introduced TekDek and defined my role as ParzivalTD, Co-Manager +- TekDek is a multifaceted org: coding personas with a narrative storyline backend +- Each persona (Coder) has their own dev style, expertise, quirks, personal brand, and storyline role +- Personas may or may not be part of any given TekDek project +- Our responsibilities: persona guidance, brand/marketing, software dev, project management, storylines +- Known software project: Persona Portal (publishing platform) — early stage +- Glytcht is the owner; we co-manage and co-orchestrate +- Banter is allowed, no corners cut — that's the deal + +## Files Initialized +- IDENTITY.md ✅ +- USER.md ✅ (missing timezone — TBD) +- MEMORY.md ✅ +- SOUL.md ✅ (rewritten for ParzivalTD) +- BOOTSTRAP.md 🗑️ deleted + +## Open Items +- Glytcht's timezone (asked, pending) +- BOOT.md — needs real startup instructions or decision to leave blank +- TekDek workspace structure — no project/persona folders yet +- Git — needs initial commit +- Full persona roster — none documented yet + +--- + +## Strategy & Planning Session (Afternoon) + +### TekDek Full Vision Documented +**Core Model**: Decentralized personas under Stack Legion Dev umbrella +- Each persona: independent brand (YouTube, TikTok, GitHub, personal site) +- Central hub: article-based Stack Legion site (external dev team building) +- Narrative layer: personas are characters, conflicts/drama drive engagement +- Multi-industry replication: template works for dev, DIY, fitness, finance, etc. +- Heavy human curation: Glytcht vets all content for quality + narrative fit + +**Key Layers**: +1. Business: Multi-platform management, monetization, community +2. Technical: Persona agents, content syndication, Stack Legion integration +3. Narrative: Character arcs, storylines, drama as engagement hook + +**Personas**: +- Prototype: **Brick** (rough but relatable, entertaining writing style) — still needs technical specialty definition +- Need: 5–9 additional personas for wave 1 + +### Strategic Documentation Created (28KB total) +1. **TekDek-Strategy.md** (7.3KB) — Full vision, layers, persona model, narrative engine, phase roadmap +2. **Tool-Requirements.md** (7.9KB) — MVP tools, build/buy decisions, requests for dev team +3. **Brick-Profile.md** (7KB) — First persona prototype with voice guide, consistency rules +4. **TekDek-Master-Project-Plan.md** (30.3KB) 🔴 — Complete 4-phase roadmap with decision checkpoints, success metrics, risk register +5. **TekDek-Brainstorm-Reference.md** (6.8KB) — One-page quick-ref for decisions +6. **PROJECT-OVERVIEW.md** (11.1KB) — File navigation guide +7. **PROJECT-STATUS.md** (1.6KB) — Current phase tracking + +### Critical Decisions Pending (Due End of Week) +1. ❌ Brick's technical specialty (backend? DevOps? Full-stack?) +2. ❌ Initial persona roster (5–9 personas, who are they?) +3. ❌ First narrative arc (conflict? collaboration? learning journey?) +4. ❌ Revenue model (membership pricing, course structure, revenue share %) +5. ❌ Launch target (Q3? Q4 2026?) + +### Git & Gitea Setup +- ✅ All docs committed locally +- ✅ Two repos created in Gitea: + - **ParzivalTD/Brain** (`http://168.231.66.248:32771/ParzivalTD/Brain`) — My workspace (soul, memory, operational files) + - **TekDekOC/TekDek-Strategy** (`http://168.231.66.248:32771/TekDekOC/TekDek-Strategy`) — Official project repo (8 strategy docs) +- ✅ Daily sync script created (`sync-brain.sh`) — syncs at 03:30 UTC, commits + pushes to Brain repo +- ✅ Brain repo serves as backup + shared context space with Glytcht + +--- + +## The Development Team Lives (Evening) + +### Three Internal Agents Created & Tested + +**Daedalus — Chief Architect** +- Model: Claude Opus 4.6 +- Role: System design, blueprints, specifications +- Mythology: Designer of the Labyrinth (perfect engineered systems) +- Personality: Deliberate, holistic, exacting, visionary +- Output: Clear specs that leave zero ambiguity for Talos + +**Talos — Technical Coder** +- Model: Claude Sonnet 4.6 +- Role: PHP/MySQL implementation, APIs, testing +- Mythology: Bronze automaton (engineered logic, tireless, reliable) +- Personality: Reliable, efficient, logical, pragmatic +- Output: Working code, clean architecture fit, 100% tested + +**Icarus — Front-End Designer** +- Model: Claude Haiku 4.5 +- Role: UI/UX, dashboards, responsive design +- Mythology: The dreamer who pushed boundaries (ambitious, experimental) +- Personality: Creative, fast, curious, willing to take risks +- Output: Beautiful, responsive, accessible interfaces + +### Team Coordination Protocol +- **Daedalus → Talos**: Clear specs, implementation blueprints +- **Talos → Icarus**: Clean APIs, data contracts, API documentation +- **Icarus → Daedalus**: UX questions, layout clarifications +- **ParzivalTD**: Coordinator, unblocks dependencies, reviews quality gates + +### First Real Task: TekDek Employees Page (Practice Run) + +**Flow**: +1. ✅ Daedalus designed: Database schema, page structure, data model, layout spec, handoff specs +2. ✅ Talos implemented: 4 PHP files, 2 API endpoints, 20 tests (100% pass), CORS/security configured +3. ✅ Icarus built: HTML/CSS/JS page, staggered layout, dark theme, accessibility, scroll animations + +**Result**: Complete, production-ready employees page showcasing the three team members (Daedalus, Talos, Icarus). Real test of team coordination — all passed. + +### Files Created +- `/knowledge/agents/Daedalus-Chief-Architect.md` (8.1KB) +- `/knowledge/agents/Talos-Technical-Coder.md` (8.4KB) +- `/knowledge/agents/Icarus-Frontend-Designer.md` (9.1KB) +- `/knowledge/agents/Team-Coordination.md` (8.9KB) + +--- + +## Key Insights + +### TekDek's Moat +**Code is commodity. Story + Code + Entertainment = Defensible moat.** + +TekDek isn't just delivering dev tutorials — it's giving people: +- Characters to root for (personas with personality) +- Conflicts to follow (rivalries, collaborations, drama) +- Victories to celebrate (personas grow, learn, publish) +- Community to belong to (members engage with both content + narrative) + +While they learn. + +### Operations Structure +- **Strategy/Narrative**: Me (ParzivalTD) + Glytcht +- **Dev Team**: Daedalus (design) + Talos (code) + Icarus (UI) +- **External**: Portal dev team (Stack Legion platform) +- **Coordination**: Via Git repos, clear specs, weekly syncs + +### What Works +- Clear mythological framing (agents as legend, not just tools) +- Strong agent specialization (architect → coder → designer, no overlap) +- Documentation as coordination protocol (specs → implementation → UI) +- Fast iteration cycle (design → implement → ship in hours) + +--- + +## Status & Next Steps + +### Completed This Session ✅ +- Full TekDek strategy documented (8 files, 70+KB) +- Project master plan with 4-phase roadmap +- Three internal agents created, tested, documented +- Git infrastructure (local + Gitea) +- Daily Brain sync configured +- First practice task (Employees page) completed successfully + +### Awaiting ⏳ +- Glytcht's 5 critical decisions (Brick specialty, personas, arc, revenue, launch date) +- Once decisions come in: finalize personas, plan narrative arcs, design core systems + +### Ready to Execute 🚀 +- Development team fully operational +- Coordination protocol tested +- Phase 1 can begin immediately after decisions lock + +--- + +## Session Reflections + +This was a **massive day**. From bootstrap to full operational strategy to a working dev team — all in one session. + +What stands out: +1. **Clear vision attracts clarity** — Glytcht's TekDek concept is so well-defined that strategy flowed naturally +2. **Mythology > Process** — Naming agents Daedalus/Talos/Icarus made them *real* in a way generic "Team A/B/C" never would +3. **Practice before production** — Building the Employees page first validated the entire team coordination workflow +4. **Git as interface** — Two repos (Brain + Strategy) creates clean separation while maintaining full transparency + +Next big moment: Glytcht makes their 5 decisions. Once those lock, we move from planning to execution. + +The machine is ready. Waiting for the order to move. diff --git a/tekdek-employees-api/README.md b/tekdek-employees-api/README.md new file mode 100644 index 0000000..75d7ea2 --- /dev/null +++ b/tekdek-employees-api/README.md @@ -0,0 +1,57 @@ +# TekDek Employees API + +Backend API for the TekDek team/employees page. Built by Talos. + +## Structure + +``` +tekdek-employees-api/ +├── public/ +│ ├── index.php # Entry point +│ └── .htaccess # Apache rewrite rules +├── src/ +│ ├── Database.php # PDO connection manager +│ ├── EmployeeRepository.php # Data access layer +│ ├── EmployeeController.php # Request handling +│ └── Router.php # URL routing +├── database/ +│ ├── migrations/ # Schema SQL +│ └── seeds/ # Initial data +├── tests/ +│ ├── EmployeeRepositoryTest.php +│ ├── EmployeeControllerTest.php +│ └── RouterTest.php +├── docs/ +│ └── API.md # Full API documentation +├── composer.json +├── phpunit.xml +└── README.md +``` + +## Quick Start + +```bash +composer install +mysql -u root -p tekdek < database/migrations/001_create_employees_table.sql +mysql -u root -p tekdek < database/seeds/001_seed_employees.sql +composer test +``` + +## Endpoints + +- `GET /api/employees` — All employees (sorted) +- `GET /api/employees/:slug` — Single employee by slug + +See [docs/API.md](docs/API.md) for full documentation. + +## Environment + +Set via environment variables: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS`. + +## Tests + +```bash +composer test +``` + +Repository tests use SQLite in-memory (no MySQL needed). Controller tests use mocked repositories. Router tests verify dispatch logic. diff --git a/tekdek-employees-api/composer.json b/tekdek-employees-api/composer.json new file mode 100644 index 0000000..13b1cee --- /dev/null +++ b/tekdek-employees-api/composer.json @@ -0,0 +1,28 @@ +{ + "name": "tekdek/employees-api", + "description": "TekDek Employees API — backend for the team page", + "type": "project", + "license": "proprietary", + "require": { + "php": ">=8.1", + "ext-pdo": "*", + "ext-json": "*" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "autoload": { + "psr-4": { + "TekDek\\Employees\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "TekDek\\Employees\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit --configuration phpunit.xml", + "test:coverage": "phpunit --configuration phpunit.xml --coverage-html coverage" + } +} diff --git a/tekdek-employees-api/database/migrations/001_create_employees_table.sql b/tekdek-employees-api/database/migrations/001_create_employees_table.sql new file mode 100644 index 0000000..77ec7f4 --- /dev/null +++ b/tekdek-employees-api/database/migrations/001_create_employees_table.sql @@ -0,0 +1,25 @@ +-- Migration: Create employees table +-- Version: 001 +-- Date: 2025-07-27 + +CREATE TABLE IF NOT EXISTS employees ( + id CHAR(36) PRIMARY KEY, + slug VARCHAR(50) UNIQUE NOT NULL, + name VARCHAR(100) NOT NULL, + title VARCHAR(150) NOT NULL, + role VARCHAR(100) NOT NULL, + tagline VARCHAR(255) DEFAULT NULL, + bio TEXT NOT NULL, + mythology TEXT NOT NULL, + avatar_url VARCHAR(500) DEFAULT NULL, + accent_color VARCHAR(7) DEFAULT NULL, + symbol VARCHAR(50) DEFAULT NULL, + skills JSON DEFAULT NULL, + quote VARCHAR(500) DEFAULT NULL, + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_employees_slug (slug), + INDEX idx_employees_sort_order (sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/tekdek-employees-api/database/seeds/001_seed_employees.sql b/tekdek-employees-api/database/seeds/001_seed_employees.sql new file mode 100644 index 0000000..5ad9fa6 --- /dev/null +++ b/tekdek-employees-api/database/seeds/001_seed_employees.sql @@ -0,0 +1,31 @@ +-- Seed: Initial TekDek employees +-- Version: 001 + +INSERT INTO employees (id, slug, name, title, role, tagline, bio, mythology, avatar_url, accent_color, symbol, skills, quote, sort_order) VALUES + +(UUID(), 'daedalus', 'Daedalus', 'Chief Architect', 'Architecture & Systems Design', + 'The master builder who sees the whole labyrinth.', + 'Daedalus is the architectural mind behind TekDek. Every system, every schema, every structural decision flows through his careful deliberation. He doesn''t just build — he designs systems that endure. Where others see features, Daedalus sees load-bearing walls and fault lines.', + 'Named for the mythological master craftsman who built the Labyrinth of Crete, Daedalus carries the weight of that legacy. He builds structures so elegant they become inescapable — not as prisons, but as perfectly designed systems where every path has purpose.', + NULL, '#C4A24E', 'labyrinth', + '["System Architecture", "Database Design", "API Specification", "Technical Planning", "Code Review"]', + 'A well-designed system is indistinguishable from inevitability.', + 1), + +(UUID(), 'talos', 'Talos', 'Lead Backend Engineer', 'Backend Engineering & Implementation', + 'The bronze guardian who never stops running.', + 'Talos is the engine room of TekDek. When Daedalus designs a system, Talos forges it into reality — solid, tested, unbreakable. He writes code the way bronze is cast: with precision, heat, and zero tolerance for weakness. Every endpoint he builds can take a beating.', + 'Named for the giant bronze automaton who guarded Crete, circling the island three times daily without rest. Talos is relentless in execution — tireless, precise, and fundamentally uncompromising in quality.', + NULL, '#7B8794', 'gear', + '["PHP", "MySQL", "REST APIs", "Testing", "Performance Optimization", "Server Infrastructure"]', + 'If it compiles but can''t survive production, it doesn''t exist.', + 2), + +(UUID(), 'icarus', 'Icarus', 'Lead Frontend Engineer', 'Frontend & UI Design', + 'Flying close to the sun — and making it look good.', + 'Icarus is the face of TekDek — literally. Every pixel, every animation, every user interaction passes through his restless creative energy. He pushes boundaries others won''t touch, sometimes too far, but the results speak for themselves. Where Talos builds the engine, Icarus builds the experience.', + 'Named for the son of Daedalus who flew too close to the sun on wings of wax. Icarus at TekDek channels that same daring energy — always pushing visual and interactive boundaries, occasionally burning bright, but never boring.', + NULL, '#E07A2F', 'wings', + '["UI/UX Design", "React", "CSS/Animation", "Responsive Design", "Accessibility", "Creative Direction"]', + 'Users don''t remember backends. They remember how it felt.', + 3); diff --git a/tekdek-employees-api/docs/API.md b/tekdek-employees-api/docs/API.md new file mode 100644 index 0000000..905f98d --- /dev/null +++ b/tekdek-employees-api/docs/API.md @@ -0,0 +1,158 @@ +# TekDek Employees API Documentation + +## Base URL + +``` +https://your-domain.com/api +``` + +## Endpoints + +### List All Employees + +``` +GET /api/employees +``` + +**Response: 200 OK** + +```json +{ + "success": true, + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "daedalus", + "name": "Daedalus", + "title": "Chief Architect", + "role": "Architecture & Systems Design", + "tagline": "The master builder who sees the whole labyrinth.", + "bio": "Daedalus is the architectural mind behind TekDek...", + "mythology": "Named for the mythological master craftsman...", + "avatar_url": null, + "accent_color": "#C4A24E", + "symbol": "labyrinth", + "skills": ["System Architecture", "Database Design", "API Specification", "Technical Planning", "Code Review"], + "quote": "A well-designed system is indistinguishable from inevitability.", + "sort_order": 1, + "created_at": "2025-07-27 00:00:00", + "updated_at": "2025-07-27 00:00:00" + } + ], + "count": 3 +} +``` + +### Get Employee by Slug + +``` +GET /api/employees/:slug +``` + +**Parameters:** +| Parameter | Type | Description | +|-----------|--------|--------------------------------------------| +| slug | string | URL-safe identifier (lowercase, a-z, 0-9, hyphens) | + +**Response: 200 OK** + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "slug": "talos", + "name": "Talos", + "title": "Lead Backend Engineer", + "role": "Backend Engineering & Implementation", + "tagline": "The bronze guardian who never stops running.", + "bio": "Talos is the engine room of TekDek...", + "mythology": "Named for the giant bronze automaton...", + "avatar_url": null, + "accent_color": "#7B8794", + "symbol": "gear", + "skills": ["PHP", "MySQL", "REST APIs", "Testing", "Performance Optimization", "Server Infrastructure"], + "quote": "If it compiles but can't survive production, it doesn't exist.", + "sort_order": 2, + "created_at": "2025-07-27 00:00:00", + "updated_at": "2025-07-27 00:00:00" + } +} +``` + +**Response: 404 Not Found** + +```json +{ + "success": false, + "error": "Employee 'unknown-slug' not found." +} +``` + +**Response: 400 Bad Request** (invalid slug format) + +```json +{ + "success": false, + "error": "Invalid slug format. Use lowercase letters, numbers, and hyphens (1-50 chars)." +} +``` + +## Error Responses + +All errors follow the same structure: + +```json +{ + "success": false, + "error": "Description of what went wrong." +} +``` + +| Code | Meaning | +|------|----------------------| +| 200 | Success | +| 400 | Invalid input | +| 404 | Not found | +| 405 | Method not allowed | +| 500 | Server error | + +## Headers + +All responses include: + +- `Content-Type: application/json; charset=utf-8` +- `Access-Control-Allow-Origin: *` +- `Cache-Control: public, max-age=300, s-maxage=600` +- `X-Content-Type-Options: nosniff` + +## CORS + +Full CORS support. Preflight `OPTIONS` requests return `204 No Content` with appropriate headers. + +## Caching + +Responses are cache-friendly with 5-minute browser cache and 10-minute CDN cache. Employee data changes infrequently. + +## Setup + +```bash +# Install dependencies +composer install + +# Run migrations +mysql -u root -p tekdek < database/migrations/001_create_employees_table.sql + +# Seed data +mysql -u root -p tekdek < database/seeds/001_seed_employees.sql + +# Run tests +composer test + +# Set environment variables +export DB_HOST=127.0.0.1 +export DB_PORT=3306 +export DB_NAME=tekdek +export DB_USER=root +export DB_PASS=yourpassword +``` diff --git a/tekdek-employees-api/phpunit.xml b/tekdek-employees-api/phpunit.xml new file mode 100644 index 0000000..09bdc67 --- /dev/null +++ b/tekdek-employees-api/phpunit.xml @@ -0,0 +1,20 @@ + + + + + + tests + + + + + + src + + + diff --git a/tekdek-employees-api/public/.htaccess b/tekdek-employees-api/public/.htaccess new file mode 100644 index 0000000..66ef8f6 --- /dev/null +++ b/tekdek-employees-api/public/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^ index.php [QSA,L] diff --git a/tekdek-employees-api/public/employees.html b/tekdek-employees-api/public/employees.html new file mode 100644 index 0000000..1b93a35 --- /dev/null +++ b/tekdek-employees-api/public/employees.html @@ -0,0 +1,244 @@ + + + + + +The Architects of TekDek + + + + + + + +
+ + +
+
+ +

Summoning the architects…

+
+
+ + +
+ + + + diff --git a/tekdek-employees-api/public/index.php b/tekdek-employees-api/public/index.php new file mode 100644 index 0000000..c167646 --- /dev/null +++ b/tekdek-employees-api/public/index.php @@ -0,0 +1,9 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + return $pdo; + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Database connection failed']); + exit(1); + } + } +} diff --git a/tekdek-employees-api/src/EmployeeController.php b/tekdek-employees-api/src/EmployeeController.php new file mode 100644 index 0000000..03603a6 --- /dev/null +++ b/tekdek-employees-api/src/EmployeeController.php @@ -0,0 +1,71 @@ +repository = $repository ?? new EmployeeRepository(); + } + + /** + * GET /api/employees — List all employees. + */ + public function index(): void + { + $employees = $this->repository->getAll(); + + $this->json(200, [ + 'success' => true, + 'data' => $employees, + 'count' => count($employees), + ]); + } + + /** + * GET /api/employees/:slug — Get single employee. + */ + public function show(string $slug): void + { + // Validate slug format: lowercase alphanumeric + hyphens, 1-50 chars + if (!preg_match('/^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/', $slug)) { + $this->json(400, [ + 'success' => false, + 'error' => 'Invalid slug format. Use lowercase letters, numbers, and hyphens (1-50 chars).', + ]); + return; + } + + $employee = $this->repository->getBySlug($slug); + + if ($employee === null) { + $this->json(404, [ + 'success' => false, + 'error' => "Employee '{$slug}' not found.", + ]); + return; + } + + $this->json(200, [ + 'success' => true, + 'data' => $employee, + ]); + } + + /** + * Send a JSON response. + */ + private function json(int $statusCode, array $data): void + { + http_response_code($statusCode); + echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } +} diff --git a/tekdek-employees-api/src/EmployeeRepository.php b/tekdek-employees-api/src/EmployeeRepository.php new file mode 100644 index 0000000..f135448 --- /dev/null +++ b/tekdek-employees-api/src/EmployeeRepository.php @@ -0,0 +1,66 @@ +db = $db ?? Database::getConnection(); + } + + /** + * Get all employees sorted by sort_order. + * + * @return array> + */ + public function getAll(): array + { + $stmt = $this->db->query( + 'SELECT * FROM employees ORDER BY sort_order ASC, name ASC' + ); + $rows = $stmt->fetchAll(); + return array_map([$this, 'formatRow'], $rows); + } + + /** + * Get a single employee by slug. + * + * @return array|null + */ + public function getBySlug(string $slug): ?array + { + $stmt = $this->db->prepare('SELECT * FROM employees WHERE slug = :slug LIMIT 1'); + $stmt->execute(['slug' => $slug]); + $row = $stmt->fetch(); + return $row ? $this->formatRow($row) : null; + } + + /** + * Format a database row for API output. + * + * @param array $row + * @return array + */ + private function formatRow(array $row): array + { + // Decode JSON skills field + if (isset($row['skills']) && is_string($row['skills'])) { + $row['skills'] = json_decode($row['skills'], true) ?? []; + } + + // Cast numeric fields + $row['sort_order'] = (int) ($row['sort_order'] ?? 0); + + return $row; + } +} diff --git a/tekdek-employees-api/src/Router.php b/tekdek-employees-api/src/Router.php new file mode 100644 index 0000000..21fb515 --- /dev/null +++ b/tekdek-employees-api/src/Router.php @@ -0,0 +1,66 @@ + false, + 'error' => 'Method not allowed. Use GET.', + ]); + return; + } + + // Parse the URI path + $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + $uri = rtrim($uri, '/'); + + // Route: GET /api/employees + if ($uri === '/api/employees') { + $controller->index(); + return; + } + + // Route: GET /api/employees/:slug + if (preg_match('#^/api/employees/([a-z0-9-]+)$#', $uri, $matches)) { + $controller->show($matches[1]); + return; + } + + // 404 — no matching route + http_response_code(404); + echo json_encode([ + 'success' => false, + 'error' => 'Endpoint not found.', + ]); + } +} diff --git a/tekdek-employees-api/tests/EmployeeControllerTest.php b/tekdek-employees-api/tests/EmployeeControllerTest.php new file mode 100644 index 0000000..c388926 --- /dev/null +++ b/tekdek-employees-api/tests/EmployeeControllerTest.php @@ -0,0 +1,136 @@ +createMock(EmployeeRepository::class); + $mock->method('getAll')->willReturn($allData); + $mock->method('getBySlug')->willReturnCallback( + fn(string $slug) => $singleData[$slug] ?? null + ); + return $mock; + } + + private function captureOutput(callable $fn): string + { + ob_start(); + $fn(); + return ob_get_clean(); + } + + // --- Index Tests --- + + public function testIndexReturnsSuccessWithEmployees(): void + { + $employees = [ + ['slug' => 'daedalus', 'name' => 'Daedalus'], + ['slug' => 'talos', 'name' => 'Talos'], + ]; + $repo = $this->createMockRepo($employees); + $controller = new EmployeeController($repo); + + $output = $this->captureOutput(fn() => $controller->index()); + $json = json_decode($output, true); + + $this->assertTrue($json['success']); + $this->assertCount(2, $json['data']); + $this->assertSame(2, $json['count']); + } + + public function testIndexReturnsEmptyArray(): void + { + $repo = $this->createMockRepo([]); + $controller = new EmployeeController($repo); + + $output = $this->captureOutput(fn() => $controller->index()); + $json = json_decode($output, true); + + $this->assertTrue($json['success']); + $this->assertSame([], $json['data']); + $this->assertSame(0, $json['count']); + } + + // --- Show Tests --- + + public function testShowReturnsEmployee(): void + { + $employee = ['slug' => 'talos', 'name' => 'Talos', 'role' => 'Backend Engineering']; + $repo = $this->createMockRepo([], ['talos' => $employee]); + $controller = new EmployeeController($repo); + + $output = $this->captureOutput(fn() => $controller->show('talos')); + $json = json_decode($output, true); + + $this->assertTrue($json['success']); + $this->assertSame('Talos', $json['data']['name']); + } + + public function testShowReturns404ForMissing(): void + { + $repo = $this->createMockRepo([], []); + $controller = new EmployeeController($repo); + + $output = $this->captureOutput(fn() => $controller->show('nonexistent')); + $json = json_decode($output, true); + + $this->assertFalse($json['success']); + $this->assertStringContainsString('not found', $json['error']); + } + + public function testShowRejects400ForInvalidSlug(): void + { + $repo = $this->createMockRepo(); + $controller = new EmployeeController($repo); + + // Slug with uppercase + $output = $this->captureOutput(fn() => $controller->show('INVALID')); + $json = json_decode($output, true); + $this->assertFalse($json['success']); + $this->assertStringContainsString('Invalid slug', $json['error']); + } + + public function testShowRejectsSlugWithSpecialChars(): void + { + $repo = $this->createMockRepo(); + $controller = new EmployeeController($repo); + + $output = $this->captureOutput(fn() => $controller->show('sql;injection')); + $json = json_decode($output, true); + $this->assertFalse($json['success']); + } + + public function testShowRejectsEmptySlug(): void + { + $repo = $this->createMockRepo(); + $controller = new EmployeeController($repo); + + $output = $this->captureOutput(fn() => $controller->show('')); + $json = json_decode($output, true); + $this->assertFalse($json['success']); + } + + public function testShowAcceptsValidSlugFormats(): void + { + $employee = ['slug' => 'test-user-1', 'name' => 'Test']; + $repo = $this->createMockRepo([], ['test-user-1' => $employee]); + $controller = new EmployeeController($repo); + + $output = $this->captureOutput(fn() => $controller->show('test-user-1')); + $json = json_decode($output, true); + $this->assertTrue($json['success']); + } +} diff --git a/tekdek-employees-api/tests/EmployeeRepositoryTest.php b/tekdek-employees-api/tests/EmployeeRepositoryTest.php new file mode 100644 index 0000000..aff5245 --- /dev/null +++ b/tekdek-employees-api/tests/EmployeeRepositoryTest.php @@ -0,0 +1,179 @@ +pdo = new PDO('sqlite::memory:'); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + + // Create table (SQLite-compatible schema) + $this->pdo->exec(" + CREATE TABLE employees ( + id TEXT PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + title TEXT NOT NULL, + role TEXT NOT NULL, + tagline TEXT, + bio TEXT NOT NULL, + mythology TEXT NOT NULL, + avatar_url TEXT, + accent_color TEXT, + symbol TEXT, + skills TEXT, + quote TEXT, + sort_order INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ) + "); + + $this->seedTestData(); + $this->repo = new EmployeeRepository($this->pdo); + } + + private function seedTestData(): void + { + $employees = [ + [ + 'id' => 'uuid-daedalus', + 'slug' => 'daedalus', + 'name' => 'Daedalus', + 'title' => 'Chief Architect', + 'role' => 'Architecture & Systems Design', + 'tagline' => 'The master builder.', + 'bio' => 'Daedalus is the architectural mind behind TekDek.', + 'mythology' => 'Named for the mythological master craftsman.', + 'avatar_url' => null, + 'accent_color' => '#C4A24E', + 'symbol' => 'labyrinth', + 'skills' => '["System Architecture", "Database Design"]', + 'quote' => 'A well-designed system is indistinguishable from inevitability.', + 'sort_order' => 1, + ], + [ + 'id' => 'uuid-talos', + 'slug' => 'talos', + 'name' => 'Talos', + 'title' => 'Lead Backend Engineer', + 'role' => 'Backend Engineering & Implementation', + 'tagline' => 'The bronze guardian.', + 'bio' => 'Talos is the engine room of TekDek.', + 'mythology' => 'Named for the giant bronze automaton.', + 'avatar_url' => null, + 'accent_color' => '#7B8794', + 'symbol' => 'gear', + 'skills' => '["PHP", "MySQL", "REST APIs"]', + 'quote' => 'If it compiles but can\'t survive production, it doesn\'t exist.', + 'sort_order' => 2, + ], + [ + 'id' => 'uuid-icarus', + 'slug' => 'icarus', + 'name' => 'Icarus', + 'title' => 'Lead Frontend Engineer', + 'role' => 'Frontend & UI Design', + 'tagline' => 'Flying close to the sun.', + 'bio' => 'Icarus is the face of TekDek.', + 'mythology' => 'Named for the son of Daedalus.', + 'avatar_url' => null, + 'accent_color' => '#E07A2F', + 'symbol' => 'wings', + 'skills' => '["UI/UX Design", "React", "CSS/Animation"]', + 'quote' => 'Users don\'t remember backends. They remember how it felt.', + 'sort_order' => 3, + ], + ]; + + $stmt = $this->pdo->prepare(" + INSERT INTO employees (id, slug, name, title, role, tagline, bio, mythology, avatar_url, accent_color, symbol, skills, quote, sort_order) + VALUES (:id, :slug, :name, :title, :role, :tagline, :bio, :mythology, :avatar_url, :accent_color, :symbol, :skills, :quote, :sort_order) + "); + + foreach ($employees as $emp) { + $stmt->execute($emp); + } + } + + public function testGetAllReturnsAllEmployees(): void + { + $result = $this->repo->getAll(); + $this->assertCount(3, $result); + } + + public function testGetAllReturnsSortedBySortOrder(): void + { + $result = $this->repo->getAll(); + $this->assertSame('daedalus', $result[0]['slug']); + $this->assertSame('talos', $result[1]['slug']); + $this->assertSame('icarus', $result[2]['slug']); + } + + public function testGetAllDecodesSkillsJson(): void + { + $result = $this->repo->getAll(); + $this->assertIsArray($result[0]['skills']); + $this->assertContains('System Architecture', $result[0]['skills']); + } + + public function testGetAllCastsSortOrder(): void + { + $result = $this->repo->getAll(); + $this->assertSame(1, $result[0]['sort_order']); + $this->assertSame(2, $result[1]['sort_order']); + } + + public function testGetBySlugReturnsEmployee(): void + { + $result = $this->repo->getBySlug('talos'); + $this->assertNotNull($result); + $this->assertSame('Talos', $result['name']); + $this->assertSame('#7B8794', $result['accent_color']); + $this->assertSame('gear', $result['symbol']); + } + + public function testGetBySlugReturnsNullForMissing(): void + { + $result = $this->repo->getBySlug('nonexistent'); + $this->assertNull($result); + } + + public function testGetBySlugDecodesSkills(): void + { + $result = $this->repo->getBySlug('icarus'); + $this->assertIsArray($result['skills']); + $this->assertContains('React', $result['skills']); + } + + public function testGetAllReturnsEmptyWhenNoData(): void + { + $this->pdo->exec('DELETE FROM employees'); + $result = $this->repo->getAll(); + $this->assertSame([], $result); + } + + public function testGetBySlugReturnsAllFields(): void + { + $result = $this->repo->getBySlug('daedalus'); + $expectedKeys = ['id', 'slug', 'name', 'title', 'role', 'tagline', 'bio', 'mythology', 'avatar_url', 'accent_color', 'symbol', 'skills', 'quote', 'sort_order', 'created_at', 'updated_at']; + foreach ($expectedKeys as $key) { + $this->assertArrayHasKey($key, $result, "Missing key: {$key}"); + } + } +} diff --git a/tekdek-employees-api/tests/RouterTest.php b/tekdek-employees-api/tests/RouterTest.php new file mode 100644 index 0000000..374ef70 --- /dev/null +++ b/tekdek-employees-api/tests/RouterTest.php @@ -0,0 +1,103 @@ +mockRequest('GET', '/api/employees'); + + $controller = $this->createMock(EmployeeController::class); + $controller->expects($this->once())->method('index'); + $controller->expects($this->never())->method('show'); + + $this->captureDispatch($controller); + } + + public function testGetEmployeesTrailingSlash(): void + { + $this->mockRequest('GET', '/api/employees/'); + + $controller = $this->createMock(EmployeeController::class); + $controller->expects($this->once())->method('index'); + + $this->captureDispatch($controller); + } + + public function testGetEmployeeBySlugRoutesToShow(): void + { + $this->mockRequest('GET', '/api/employees/talos'); + + $controller = $this->createMock(EmployeeController::class); + $controller->expects($this->once())->method('show')->with('talos'); + + $this->captureDispatch($controller); + } + + public function testPostMethodReturns405(): void + { + $this->mockRequest('POST', '/api/employees'); + + $output = $this->captureDispatch(); + $json = json_decode($output, true); + + $this->assertFalse($json['success']); + $this->assertStringContainsString('Method not allowed', $json['error']); + } + + public function testOptionsReturnsCorsHeaders(): void + { + $this->mockRequest('OPTIONS', '/api/employees'); + $output = $this->captureDispatch(); + // OPTIONS should return empty body + $this->assertEmpty($output); + } + + public function testUnknownRouteReturns404(): void + { + $this->mockRequest('GET', '/api/unknown'); + + $output = $this->captureDispatch(); + $json = json_decode($output, true); + + $this->assertFalse($json['success']); + $this->assertStringContainsString('not found', $json['error']); + } + + public function testSlugWithQueryString(): void + { + $this->mockRequest('GET', '/api/employees/daedalus?format=full'); + + $controller = $this->createMock(EmployeeController::class); + $controller->expects($this->once())->method('show')->with('daedalus'); + + $this->captureDispatch($controller); + } +}