Kod konsoli¶
Architektura to backend. To jest druga połowa: aplikacja
Next.js w frontend/, dla kogoś, kto zaraz ją zmieni.
Stack. Next.js 15 (App Router) · React 19 · TypeScript strict · Tailwind ·
next-intl · TanStack Query · Zustand · vitest i Testing Library ·
Playwright. Menedżer pakietów i runner: bun.
Gdzie co leży¶
| Ścieżka | |
|---|---|
src/app/[locale]/(dashboard)/… |
Produkt. Ścieżki mają prefiks lokalizacji |
src/app/api/… |
Route handlery proxujące do backendu |
src/lib/ |
Typowani klienci API, query-keys.ts i rejestry opisane niżej |
src/hooks/ |
Jeden na zasób — use-agents, use-permissions, … |
src/stores/ |
Zustand, jeden na zagadnienie |
src/components/<domain>/ |
UI według domeny; prymitywy w ui/, stany puste i błędu w states/ |
Server Components są domyślne. "use client" jest od stanu, efektów i
handlerów, a nie z przyzwyczajenia.
Przeglądarka nigdy nie woła backendu¶
Każde żądanie idzie do /api/* w tej aplikacji, która przekazuje je do FastAPI
z tokenem dostępu pobranym z ciasteczka HttpOnly. To właśnie trzyma token
poza JavaScriptem, a URL backendu poza bundlem klienta.
Robi to jeden przekaźnik — src/lib/platform-proxy.ts — a nie ręcznie napisany
plik route na każdy endpoint, powtarzający te same dwanaście linijek.
Odpowiedź bez Cache-Control to nie odpowiedź, której nikt nie cache'uje
Proxy stempluje no-store na wszystkim, czego backend nie oznaczył. Każda
odpowiedź tutaj zależy od ciasteczka, zestawu uprawnień i nagłówka
organizacji, a lista pobrana ponownie po zapisie musi dotrzeć do serwera.
Ręcznie napisany plik route jest winien ten sam nagłówek — proxy jest jedynym miejscem, które nakłada go za Ciebie.
Dane i to, gdzie mieszka stan¶
Cały dostęp do API idzie przez klienta w src/lib/, konsumowanego przez
hooka. Żadnego fetch w komponencie.
Dane serwerowe mieszkają w warstwie zapytań; store'y trzymają wyłącznie stan
UI i stan ulotny. Zarejestruj każdy klucz zapytania w query-keys.ts, żeby
unieważnianie po zapisie pozostawało spójne.
Uprawnienia to decyzja o renderowaniu¶
use-permissions.ts daje efektywny zestaw uprawnień dla aktywnej organizacji.
Kontrolka, której wołający nie może użyć, nie jest renderowana — nie
renderowana, a nie renderowana i dopiero potem 403.
Dwie pułapki, obie już tutaj wydane:
- To, które role oferuje picker, jest arytmetyką, a nie listą.
assignableRolesodzwierciedla regułę serwera na katalogu uprawnień: rola jest oferowana tylko wtedy, gdy własna rola wołającego ściśle ją przewyższa. Zahardkodowane „każda rola poza owner” to to, co zaoferowało Adminowi opcję Admin i zwróciło 403 po wpisaniu adresu e-mail. - Strona, która nazywa organizację w swoim URL-u, jest tą organizacją.
Klient API stempluje
X-Organization-Idz aktywnej organizacji, więc strona działająca na organizacji ze swojej ścieżki, a czytająca uprawnienia dla aktywnej, rozstrzyga o członkach Acme na podstawie Twojej roli w Globeksie. Przyjęcie organizacji mieszka wActiveOrgGuard, raz.
Każdy widoczny dla użytkownika napis idzie przez next-intl¶
make lint egzekwuje to w obie strony: czytelny napis siedzący w komponencie
oblewa i tak samo oblewa klucz, który trzyma katalog, a którego żaden komponent
nie czyta.
Trzy reguły, na których ludzie się przewracają:
- Liczebnik to ICU
plural, nigdy operator warunkowy.{n} file{n === 1 ? "" : "s"}to zdanie, które tak buduje tylko angielski. - Rzeczownik, z którym zdanie się zgadza, nie jest parametrem.
{matched} of {total} {noun}renderuje po polsku3 of 40 skills. Rzeczownik wchodzi do środkapluralalboselect. - Katalog trzyma copy i tylko copy. Fałszywy alarm bierze
i18n-exemptz uzasadnieniem; nigdy nie bierze klucza. Odpowiedzenie na jeden z nich przez przeniesienie listy klas Tailwinda doen.jsonto sposób, w jaki przetłumaczenie jednego napisu pozbawiło kiedyś komponent jego stylowania.
Angielski jest językiem źródłowym i jest podkładany pod każdą lokalizację, więc brakujące tłumaczenie renderuje angielski, a nie klucz.
Własne rzeczowniki produktu zostają po angielsku w każdej lokalizacji
agent, spec, capability, skill, embed, budget, run, prompt, provider, token, vault, workspace, sandbox, MCP. Nazywają rzeczy, które klient spotyka też w dokumentacji, w API i w wyeksportowanym YAML-u — tłumaczenie ich w UI i nigdzie indziej robi dwa słowniki dla jednego produktu. Odmieniaj je, nie zastępuj.
Cztery rejestry i żadnego drugiego źródła¶
Każdy z nich to jedna tabela, którą czyta kilka części UI. Dopisanie do tabeli jest całą zmianą; dodanie drugiego źródła jest błędem.
| Trzyma | Dodajesz przez | |
|---|---|---|
lib/tool-catalog.ts |
Ikonę, podpis w trakcie działania, nazwę po zakończeniu i renderer dla każdego narzędzia, które rejestruje backend | Wiersz kluczowany na id narzędzia z capability. Test backendu porównuje jedno z drugim w obie strony |
lib/brand-glyphs.generated.ts |
Każdy znak usługi, konektora i providera, jako surowe dane ścieżek | Wiersz w scripts/gen-brand-icons.ts, a potem bun run gen:brand-icons. Nigdy import z paczki z ikonami |
lib/dashboard/registry.ts |
Widgety dashboardu i uprawnienie, którym każdy z nich jest bramkowany | Pięć edycji, wymienionych na stronie poniżej |
lib/dialog-sizes.ts |
Jeden token szerokości i jeden token kształtu na dialog | Wybór tokenu, nigdy wysokość szytą na miarę |
Dwie rzeczy, które nowa powierzchnia jest winna¶
Oba są rejestrami o tym samym trybie awarii: strona dodana gdziekolwiek indziej jest po prostu nieobecna, nic nie oblewa, a funkcja wychodzi niewidoczna.
Przystanek w przewodniku. lib/onboarding/tour.ts to bierny spacer, który
odtwarza „?” danej strony, a flows.ts to prowadzone tworzenie, które spacer
oferuje na końcu. Strona bez przystanku nie renderuje żadnego „?” — więc
nowa strona, której nagłówek nie ma przycisku pomocy, nie została
zarejestrowana. Zabramkuj krok uprawnieniem, które niesie jego kontrolka, oznacz
go jako optional, gdy kontrolka wymaga istnienia danych, i zakotwicz go na
czymś ograniczonym.
Widget dashboardu, jeśli funkcja wytwarza stan, który ktoś chciałby widzieć
na pierwszy rzut oka. Pięć edycji: id i definicja w dashboard/registry.ts,
komponent w components/dashboard/widgets/, umiejscowienie w layouts.ts, id
odzwierciedlone w backend/app/schemas/dashboard_layout.py — test oblewa, gdy
te dwa się rozjadą — oraz copy zarówno w en.json, jak i w pl.json.
Weryfikacja¶
Z katalogu frontend/. W korzeniu repozytorium vitest nie znajduje konfiguracji,
zgłasza około 164 widmowych porażek i zostawia niepotrzebny katalog cache.
Raz, przed pushem — z korzenia repozytorium:
test:coverage, a nie test:run
Zadanie, które uruchamia CI, mierzy pokrycie i oblewa poniżej 100% linii, instrukcji i funkcji albo 97,5% gałęzi. Zestaw, w którym każdy test przechodzi, nadal może być czerwony — i już bywał.
Martwą gałąź łatwiej usunąć niż pokryć: ?? "" za sprawdzeniem, które już
dowiodło wartości, jest właśnie tym, co bramka słusznie zauważa.
Spec, który przekracza czas, to zwykle maszyna. testTimeout wynosi 15 s, a
asyncUtilTimeout 5 s, oba zmierzone, a nie zgadnięte. Żaden z nich nie jest
powodem, by trzymać spec montujący więcej, niż czytają jego asercje.
Podsumowanie¶
- Przeglądarka rozmawia z
/api/*w tej aplikacji, nigdy z FastAPI — jeden przekaźnik, i to on stemplujeno-store. - Dane serwerowe mieszkają w warstwie zapytań; store'y trzymają wyłącznie stan UI.
- Kontrolka, której wołający nie może użyć, nie jest renderowana, a pickery ról są wyliczane, a nie wypisane.
- Copy idzie przez
next-intl, liczebniki są liczbami mnogimi ICU, a katalog trzyma copy i tylko copy. - Nowa powierzchnia jest winna przystanek w przewodniku, a jeśli ma stan widoczny na pierwszy rzut oka — także widget; oba rejestry milczą.