|
| 1 | +--- |
| 2 | +title: "MiseOS: What Got Built, What I Learned, and What I'd Do Differently" |
| 3 | +date: 2026-06-01 |
| 4 | +draft: false |
| 5 | +description: "The final post in the MiseOS development log. A full account of what was built, the architectural decisions that shaped it, and honest reflections on what I'd change." |
| 6 | +tags: ["miseOS", "react", "frontend", "reflection", "architecture"] |
| 7 | +categories: ["Project Log"] |
| 8 | +series: ["MiseOS Development"] |
| 9 | +series_order: 17 |
| 10 | +--- |
| 11 | + |
| 12 | +This is the last post in the MiseOS development log. The project is in production, the exam is next week, and it's time to look back at what actually got built and what I learned building it. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## What got built |
| 17 | + |
| 18 | +MiseOS started as a school project and became a system my colleagues use every day in a professional kitchen. That was never the plan. It happened because the domain was real and the problems were real, and solving them properly turned out to be more interesting than building a demo. |
| 19 | + |
| 20 | +The full feature set, as it stands: |
| 21 | + |
| 22 | +**Kitchen staff flows** |
| 23 | +- Dish suggestion workflow — propose a dish, track approval status, receive feedback |
| 24 | +- Ingredient requests — request specific items with quantity and note, track status |
| 25 | +- Weekly menu view — see the current published menu by day and station |
| 26 | +- AI menu inspiration — SSE-streamed dish ideas generated by Gemini via the backend |
| 27 | + |
| 28 | +**Admin flows** |
| 29 | +- Dashboard with live counters via WebSocket |
| 30 | +- Dish suggestion management — approve, reject with feedback, edit |
| 31 | +- Dish bank — manage all approved dishes across stations |
| 32 | +- Weekly menu editor — build and publish the weekly menu slot by slot |
| 33 | +- Ingredient request management — approve, reject, coordinate with kitchen |
| 34 | +- Shopping list — AI-generated and normalized from approved ingredient requests |
| 35 | +- User management — role and station assignment |
| 36 | +- Station and allergen management |
| 37 | +- Takeaway offers — create daily offers from published menu dishes *(in progress)* |
| 38 | + |
| 39 | +**Public flows** |
| 40 | +- Weekly menu — visible to guests without login |
| 41 | +- Takeaway ordering — browse and order today's offers *(in progress)* |
| 42 | + |
| 43 | +Takeaway offers and ordering are still in progress, but the rest of the system is fully functional and in use. The public menu is live on a tablet in the kitchen. Staff log in to submit suggestions and requests. The head chef uses the admin dashboard to manage everything. |
| 44 | + |
| 45 | +{{< gallery >}} |
| 46 | + <img src="/img/blog/dashboard.png" class="grid-w25" /> |
| 47 | + <img src="/img/blog/menu-inspirations.png" class="grid-w25" /> |
| 48 | + <img src="/img/blog/chef-suggestion.png" class="grid-w25" /> |
| 49 | + <img src="/img/blog/dish-suggestions.png" class="grid-w25" /> |
| 50 | + <img src="/img/blog/dish-suggestions-detail.png" class="grid-w25" /> |
| 51 | + <img src="/img/blog/ingredient-request.png" class="grid-w25" /> |
| 52 | + <img src="/img/blog/public-menu.png" class="grid-w25" /> |
| 53 | + <img src="/img/blog/dish-select.png" class="grid-w25" /> |
| 54 | +{{< /gallery >}} |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## The decisions that shaped the project |
| 59 | + |
| 60 | +**Feature-based architecture.** Every feature — dishes, menus, suggestions, ingredient requests — owns its own pages, components, services, and utils. Working on dish suggestions means opening `features/suggestions/` and seeing everything relevant in one place. The alternative (all pages together, all services together) forces you to jump between folders to work on a single feature. That friction compounds fast on a project this size. |
| 61 | + |
| 62 | +The decision wasn't planned from the start. It came after two days of building the public menu and looking at a flat `src/components/` folder full of unrelated files. Refactoring to feature-based structure took a few hours. It saved far more than that across the following weeks. |
| 63 | + |
| 64 | +**Minimal global state.** Three React Contexts, nothing more: |
| 65 | + |
| 66 | +- `AuthContext` — JWT session, 401/403 handling, navigation |
| 67 | +- `NotificationContext` — global toast messages |
| 68 | +- `RealtimeNotificationsContext` — WebSocket-based live counters |
| 69 | + |
| 70 | +Everything else is local component state. The rule was simple: data lives where it's used. Pages own their fetch logic. Components receive props and fire callbacks. Nothing reaches into a sibling's state. |
| 71 | + |
| 72 | +This kept the codebase readable. At any point in the project I could open a page component and understand exactly where its data came from and what could change it. |
| 73 | + |
| 74 | +**CSS Modules over utility-first.** I chose CSS Modules over Tailwind deliberately. The goal was to learn CSS properly and have full control over the design rather than assembling it from utility classes. Global design tokens in `:root` — colours, spacing, radii, typography — meant consistency across all modules without a preprocessor. |
| 75 | + |
| 76 | +The hardest part was discipline. It's tempting to copy-paste styles rather than abstract shared ones. Towards the end I had shared stylesheets like `DashboardCards.module.css` used across all dashboard cards. Getting there required refactoring more than once. |
| 77 | + |
| 78 | +--- |
| 79 | + |
| 80 | +## The technically interesting parts |
| 81 | + |
| 82 | +**Realtime with WebSocket.** The dashboard shows live counts of pending suggestions and ingredient requests via a WebSocket connection. The interesting part wasn't the connection — it was deciding what "live" means. The solution was a snapshot model: on connect, the backend sends a complete state snapshot. From there, individual events update specific values. No polling, no full refetch. |
| 83 | + |
| 84 | +**SSE streaming and AI.** The menu inspiration feature uses Gemini AI via the backend and streams the response as SSE — Server-Sent Events, a one-way HTTP stream. The user sees dish ideas appear in real time rather than waiting for a full response. It's technically the most satisfying feature in the app and the one colleagues find most useful. |
| 85 | + |
| 86 | +The shopping list also uses AI — but differently. Approved ingredient requests from multiple cooks are sent to the backend, normalised and aggregated by AI into a clean consolidated list for the head chef to review. No streaming here — it's a single request and response, but the output quality is meaningfully better than a raw list would be. |
| 87 | + |
| 88 | +**CSS print styles.** The shopping list has a print stylesheet — `@media print` — so the head chef can print a clean, formatted list for the kitchen. It strips navigation, colours, and UI chrome and leaves only the relevant content. I only implemented it on the shopping list, but it showed how much CSS can do that has nothing to do with the screen. |
| 89 | + |
| 90 | +--- |
| 91 | + |
| 92 | +## Building for real users |
| 93 | + |
| 94 | +The project is in production. That changed how I thought about almost everything. |
| 95 | + |
| 96 | +Colleagues use the system during service. When something doesn't work, they can't do their job. Error messages, loading states, and empty states stopped being edge cases and became part of the product. Every page handles three states: loading, empty, and error. That sounds obvious until you're the one maintaining it under real conditions. |
| 97 | + |
| 98 | +It also made building more interesting. There's something different about getting feedback from someone who actually uses what you built — not a review comment, but a real consequence. I don't know whether they'll keep using MiseOS after the semester ends. But building something with actual stakes made every decision feel more worth making carefully. |
| 99 | + |
| 100 | +--- |
| 101 | + |
| 102 | +## What I'd do differently |
| 103 | + |
| 104 | +**TypeScript from the start.** Plain JavaScript worked until it didn't. As services and API response shapes grew, the absence of types became real friction. Tracking down a bug caused by a property name mismatch between what the API returns and what the component expects is the kind of thing TypeScript catches before it runs. Next project starts with TypeScript. |
| 105 | + |
| 106 | +**Structured error handling.** `apiClient.js` handles 401 and 403 globally. Feature-specific errors — 409 conflicts, 400 validation failures — are caught ad hoc in each page component. The result is similar error-handling code written slightly differently in a dozen places. A shared error mapper that translates API error shapes into typed exceptions would have made both the code and the debugging more consistent. |
| 107 | + |
| 108 | +**A test strategy, early.** There are no automated frontend tests. This is the biggest gap in the project. With a production application, the critical flows — login, suggestion creation, menu publishing — deserve integration tests that run on every deploy. I didn't explore this during the semester, but it's clearly the next thing to understand properly. |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## What didn't make it |
| 113 | + |
| 114 | +Some things were on the list and didn't happen. |
| 115 | + |
| 116 | +**Dark mode.** The design tokens in `:root` are exactly the right foundation for it — swapping a `data-theme` attribute would flip the colour variables. The architecture is ready. I just didn't prioritise it. |
| 117 | + |
| 118 | +**`sessionStorage` vs `localStorage`.** The JWT is stored in `localStorage`, which persists across browser sessions. `sessionStorage` would clear it when the tab closes — a different security tradeoff worth exploring. I thought about it but left it as-is. |
| 119 | + |
| 120 | +**Single-day menu view.** The menu editor shows a full week. During service, staff care about today. A day filter would be a rendering change, not a structural one — the data is already there. It's the one thing I'd add first if I came back to this. |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +## Final thoughts |
| 125 | + |
| 126 | +This frontend ended up being a good capstone for the semester because it forced me to connect concepts that are easy to understand in isolation but harder to get right together: routing and layouts, JWT auth with role-based access, state placement, component responsibility boundaries, consistent error handling, and a UI that reflects real domain logic rather than just displaying data. |
| 127 | +The things that clicked most weren't the individual React APIs — it was the decisions around them. Where does state live? Who owns an error? When is a component too big? Those questions don't have answers in the documentation. You find them by building something real and feeling where the friction is. |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +*This is the final post in the MiseOS development log. The full series covers backend development from week 1 through frontend completion — seventeen posts, one semester, one production system.* |
0 commit comments