Recipe Collection — CakePHP 5 + Angular 20 (dark/light Cockpit UI)#6
Open
Psheikomaniac wants to merge 96 commits into
Open
Recipe Collection — CakePHP 5 + Angular 20 (dark/light Cockpit UI)#6Psheikomaniac wants to merge 96 commits into
Psheikomaniac wants to merge 96 commits into
Conversation
added 30 commits
June 17, 2026 16:51
Break the AVENDIS recipe collection task into 15 self-contained issues across setup, data model, frontend, advanced features and submission, plus an overview README with critical path and effort estimates. Each issue documents the rationale behind its key technical decisions.
Add explicit Tests sections to the implementation issues and introduce a dedicated lightweight GET /recipes/{id}/preview endpoint plus a debounce for the hover preview to keep the payload small and avoid request spam.
Provision nginx, PHP-FPM 8.3 and MySQL 8.0 as containers matching the required stack and the Ubuntu deployment target. PHP waits for a MySQL healthcheck before starting, and a temporary placeholder front controller verifies the nginx -> PHP-FPM -> MySQL chain until the CakePHP skeleton lands in issue openITCOCKPIT#2.
Document the Docker Compose quick start and the native Ubuntu 26.04 install steps, record the initial architecture decisions (CakePHP as a JSON API, Docker for reproducibility), and stub the browser-support matrix. Keep the original challenge brief for reference.
Environment verified: HTTP 200 with PHP 8.3.31 and MySQL 8.0.46, database reachable from PHP.
Add an implementation log separate from the issue specs, recording the decisions made while building the dev environment (version pinning, healthcheck gating, the placeholder DB check, the 3306 -> 3307 port deviation) and the reasoning behind each.
Add the CakePHP 5.3.6 application skeleton as the JSON API foundation and adapt it for API use: a StatusController serving GET /status, a CorsMiddleware that sets the Access-Control-Allow-* headers and answers OPTIONS preflight, and removal of the cookie-based CSRF middleware (and its now-obsolete skeleton tests) since this is a stateless cross-origin API. The datasource reads DATABASE_URL, so no credentials are committed.
Pass DATABASE_URL, SECURITY_SALT and CORS_ALLOW_ORIGIN to the php service so CakePHP connects to the mysql container with no hardcoded credentials. Document the variables in .env.example.
Add the implementation log for the CakePHP skeleton (DATABASE_URL wiring, CSRF removal, CORS) and update the issue status.
The CakePHP skeleton's GitHub workflows lived under api/.github/, which GitHub Actions never loads (only the repo root is read), and inlined a SECURITY_SALT. Replace them with a root .github/workflows/ci.yml scoped to the api/ app that runs PHPUnit and CodeSniffer on PHP 8.3/8.4; the salt is generated by composer post-install in CI, so nothing secret is committed. Drop the skeleton stale-issue bot.
Scaffold the standalone-component Angular 20 app, integrate Bootstrap 5 (CSS + JS bundle) and wire HttpClient. A StatusService calls the CakePHP /status endpoint and the root component renders the result, proving the cross-origin (CORS) frontend-to-backend slice end to end. apiBaseUrl is configured through Angular environment files.
Add the implementation log for the Angular project (version pinning to 20, standalone over NgModules, host dev server vs Dockerised API) and update the issue status.
Two reversible migrations create the recipes and 1:n ingredients tables. amount is DECIMAL(8,2) (exact fractional amounts, no float rounding) and unit is a free-text VARCHAR (new units need no migration); the foreign key cascades on delete. A seeder loads the chocolate-cake example from the brief.
Point DATABASE_TEST_URL at a separate test_recipes database (auto-created by a MySQL init script) so PHPUnit exercises the same engine and data types as production rather than SQLite. SchemaTest verifies the amount column is decimal(8,2), that 1.50 round-trips exactly, and that deleting a recipe cascades to its ingredients.
Add the implementation log for the schema work (DECIMAL/VARCHAR/cascade rationale, MySQL test database, the seed-tracking gotcha) and update the issue status.
Bake the Table and Entity classes. RecipesTable has a dependent hasMany to Ingredients and a Timestamp behaviour configured for the created column only (there is no modified column).
GET /recipes and GET /recipes/{id} return JSON with eager-loaded ingredients; an unknown id returns a hand-built JSON 404 (reliable even in debug mode, where the exception renderer would emit HTML). Records-only fixtures and three controller tests cover the list, detail and not-found paths against the MySQL test database.
Add the implementation log for the read API (eager loading, response shape, the JSON-404 deviation from the spec) and update the issue status.
Make created (recipe) and recipe_id/recipe (ingredient) non-mass-assignable so a client cannot set the server-owned timestamp or re-parent an ingredient through the request body.
POST /recipes creates a recipe and its ingredients in a single transaction, returning 201 on success or a 422 JSON error envelope on validation failure. Validation requires a title and at least one ingredient, and bounds amount to the DECIMAL(8,2) range (0 < amount <= 999999.99) so an out-of-range value returns a clean 422 instead of a 500 database error. Six controller tests cover the success, validation and no-orphan-on-failure paths.
Add the implementation log for the write API, including the amount-overflow bug found by manual adversarial probing (the review workflow hit the session limit) and its fix.
The required-ingredient guard checked the raw request, but CakePHP's marshaller silently drops malformed ingredient entries — so ingredients sent as a bare object or a list of non-objects passed the guard and persisted a recipe with zero ingredients. Check the marshalled entity instead; both cases now return 422. Found by adversarial review; covered by two regression tests.
Configure ErrorController to render uncaught exceptions as JSON (message/url/code) regardless of the Accept header, so a malformed JSON body (400) and any exception (500) no longer return an HTML error page that the Angular client cannot parse. With JSON errors guaranteed, RecipesController::view() throws NotFoundException again instead of hand-building the 404. Updates two skeleton PagesController tests that asserted HTML error pages. Found by adversarial review.
Record the three defects found by the adversarial review (amount overflow, bypassable ingredient guard, HTML error responses) and their fixes in the issue and implementation logs.
RecipeService is the single typed HTTP layer (getRecipes/getRecipe/createRecipe), unwrapping the JSON envelope into domain types. RecipeList renders the recipes as responsive Bootstrap cards with explicit loading, empty and error states and a dd.MM.yyyy date. The root component becomes an app shell (navbar + router-outlet) and the now-unused status smoke-test service is removed. Eight component/service specs cover the URL mapping, query params, and the three list states.
Add the implementation log for the recipe list (service-as-HTTP-layer, signals over async pipe, the three load states) and update the issue status. MVP milestone reached.
CorsMiddleware sat inside ErrorHandlerMiddleware, so responses generated for thrown exceptions (404/400/500) never received the Access-Control-* headers — cross-origin the browser blocked them and the SPA saw a network error instead of the status. Move CORS to be the outermost middleware so every response, including error responses, is decorated. Regression test asserts a 404 carries the CORS header; middleware-order test updated.
RecipeDetail renders a recipe at /recipes/:id with its ingredients (formatted as '100g sugar'), description and date. It loads via route.paramMap + switchMap so navigating between detail pages reloads, distinguishes a 404 ("No recipe found") from a generic error, and ties its subscription to the component lifetime. Three specs cover the loaded, not-found and id-change paths.
Add the implementation log for the detail view, including the CORS-on-error backend bug found during browser testing and its fix.
RecipeForm is a ReactiveForm with a FormArray of ingredient rows (add/remove, always at least one). Client validation mirrors the server (title required/max 255, amount > 0, name/unit required); a 422 response maps field errors back onto the matching controls. The submit button is disabled with a spinner while the POST is in flight, and on success the user is routed to the list where the new recipe appears. The recipes/new route precedes recipes/:id so "new" is not matched as an id.
added 30 commits
June 18, 2026 11:22
Serve uploaded images at /uploads from the frontend nginx (same origin in the container; the backend serves them in dev). Add a RecipeService upload/delete, an image-url helper, and UI: the detail page uploads / replaces / removes the photo and shows it, and the photo appears on list cards and in the hover preview, with a placeholder when absent. Service and upload-flow specs added.
Add the issue spec and implementation log for the hero-image feature, including the disk-storage/single-origin serving design and the ingredients-wiped bug found in the browser.
Add a notes table (1:n to recipes, FK ON DELETE CASCADE) and a NotesController with GET/POST /api/recipes/{id}/notes (newest first; 422 on empty body; 404 for an unknown recipe) and DELETE /api/notes/{id}. recipe_id is taken from the route, not the request body (non-mass-assignable). Nine controller tests cover listing, add, validation, the route-vs-body recipe_id, delete and cascade on recipe deletion.
The detail page loads a recipe's notes (newest first), shows each with its author (or Anonymous) and date, lets the user add a note (body required, author optional) and delete one. RecipeService gains getNotes/addNote/deleteNote. Service and add/delete specs added.
Add the issue spec and implementation log for personal notes (the no-accounts reframing of 'comments', dedicated NotesController, route-derived recipe_id, cascade).
Add a 'Photo (optional)' field to the recipe form with a local preview. On submit the form saves the recipe and then uploads the chosen image to its id (a switchMap second step over the existing upload endpoint, since the upload needs an existing recipe), so a photo can be attached during creation; in edit mode the current photo can be replaced or removed. An image-upload failure does not block navigation. Covered by a new form spec.
The 'Back to list' link on the detail page and the Cancel actions in the share dialog and the create/edit form were rendered as text links (btn-link). Make them btn-outline-secondary so they read as buttons, consistent with the neutral buttons already in those views (e.g. Edit).
Search was an always-visible field in the list toolbar. Replace it with a magnifier icon in the global navbar that expands into a text field on click and collapses again when left empty. The term is reflected in the URL (?search=): the navbar debounces keystrokes and navigates to the list with the query param, and the list now reads the term from queryParamMap and reloads. This gives a single source of truth, makes a filtered list shareable, and lets a search from any page land on the filtered list. The list no longer owns a search input or its own debounce.
Only the card title navigated to the recipe. Add Bootstrap's stretched-link to the title anchor so its ::after covers the whole card, making a click anywhere open the recipe — one semantic link, no extra handler. Show the pointer cursor across the card. As a deliberate side effect the hover preview now triggers over the whole card, which is more consistent.
When a recipe has a hero image, attach it to the share e-mail as an inline (cid) part and render it in the HTML body. Inline embedding shows reliably in Mailpit and real clients without needing a publicly reachable URL, and keeps the mail self-contained. The mime type is derived from the stored extension via the existing upload allow-list. Covered by a test that shares a recipe with an image and asserts the cid reference and the attachment.
Add implementation log #21 covering the button styling, clickable card, navbar search and inline e-mail image, with the reasoning behind each.
Replace the outlined '×' button on each note with a borderless trash-can icon (btn-link, no border or background) so only the red icon shows. Clearer delete affordance, lighter visual weight in the notes list.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A full-stack Recipe Collection solving the challenge brief, with a modern
openITCOCKPIT-inspired dark/light UI.
Stack: CakePHP 5 (JSON REST API) · Angular 20 + TypeScript · Bootstrap 5.3 ·
MySQL 8.0 · PHP 8.3 · nginx + PHP-FPM · Docker · Mailpit.
Run it
Brief features (all implemented)
FormArray, validationLIKE,debounceTime(300), reflected in the URLswitchMap+ per-id cache + dedicated/previewendpointBeyond the brief
PUT, ingredients replaced), photo upload (replace/remove, ≤5 MB),personal notes per recipe, temperature field.
prefers-color-scheme) built purely via Bootstrap--bs-*CSS-variable theming;a filter sidebar (duration + ingredient-count + sort); WCAG 2.1 AA contrast verified.
Quality
/api→ no CORS).READMEand underdocs/.