ElementAPI'de demir sayfası neden veritabanına hiç uğramıyor
Saga yazısının devamı. Bilimsel katalog, açılışta belleğe aldığı bir JSON'dan cevap veriyor; v2 API'si de alan seçimi ve ETag'le herkese açık. Sonra JWT'yi doğrulayan tek servis olan identity-service'e, tarayıcıda duran misafir defterine, üç compose profiline ve Jenkins ile Playwright'a geçiyorum.
ElementAPI'de /element/fe adresini açarsan Demir sayfasını görürsün. Bu sayfa iki kaynaktan besleniyor: uygulamaya gömülü bir JSON kopyası ve GET /api/v2/elements/fe. Gömülü kopya yüklenince sayfayı onunla çiziyorum, API cevabı gelince de onun yerini alıyor. API'ye ulaşılamazsa ekranda gömülü kopya kalıyor.
İsteğin arkasında bir veritabanı da yok. İstek tam platformda gateway üzerinden catalog-service'e, bağımsız atlasta ise doğrudan science-service'e gidiyor. Karşılayan servis, açılışta belleğe aldığı bir JSON dosyasından istenen alanları kopyalıyor, gövdenin SHA-256 özetinden bir ETag üretiyor ve cevabı bir saatlik önbellek başlığıyla dönüyor. Sipariş saga'sını ilk yazıda anlatmıştım; bu yazıda platformun öbür yarısına bakıyorum.
Veri repoda, iki JSON dosyasında
Bilimsel verinin tamamı repodaki iki dosyada duruyor: catalog-service altındaki scientific-elements.json (118 element) ve compound-service altındaki scientific-compounds.json (214 bileşik). İkisini de deploy/scripts/refresh-scientific-catalog.mjs üretiyor. Elementlerde üç kaynağı birleştiriyorum: PubChem'in periyodik tablo JSON'u, RSC'nin element sayfaları ve NIST'in izotop bileşimleri. Script indirdiklerini artifacts/science-cache/ altında saklıyor, --force vermedikçe de onları yeniden kullanıyor. PubChem'den tam 118 satır gelmezse ya da NIST sayfasından 300'den az izotop kaydı çıkarsa da duruyor. API'nin okuma yolunda dış kaynağa hiç istek gitmiyor; yeni bir snapshot ancak servis yeniden derlenince devreye giriyor.
Her kaydın provenance bölümünde alanların nereden geldiği, ne zaman alındığı, hangi birim dönüşümlerinden geçtiği ve null'ın ne anlama geldiği yazıyor. Elementlerde bu anlamı "sıfır değil, yokluğun kanıtı da değil" diye yazdım. Sayıya çevrilemeyen kaynak değerlerini atmıyorum, metin olarak saklıyorum: demirin discovered_year alanı null, ama discovery_reported alanında kaynaktaki "Ancient" duruyor. Bileşiklerin 51'i tam bir PubChem görüntüsü. Kalan 163 kayıt için PubChem'den yalnız CID ve formülü aldım, onların fiziksel ve GHS alanları null.
Tam platformu açmadan atlası gösterebilmek için ayrı bir host yazdım. science-service/Element.Science.csproj kendi denetleyicisini içermiyor; catalog ve compound'taki iki v2 denetleyicisini ve shared-lib/Science/ScientificCatalog.cs dosyasını Compile Include ile derliyor. Yani aynı v2 kodu üç yerde çalışıyor ama kaynakta tek bir kopyası var. Açılış davranışı science-service/Program.cs'te:
// Load before becoming ready; broken or empty snapshots must fail startup.
var elements = LoadSnapshot("scientific-elements.json");
var compounds = LoadSnapshot("scientific-compounds.json");
if (elements.Count != ExpectedElementCount || compounds.Count == 0)
throw new InvalidDataException("Scientific snapshot is incomplete.");
var unavailableElementSections = FindSectionsEmptyInEveryRecord(elements);
// ...
app.MapGet("/api/v2/coverage", (HttpResponse response) =>
{
response.Headers.CacheControl = "public, max-age=3600"; // snapshot-derived, same policy as the v2 catalog
return Results.Json(new
{
schemaVersion = "2.0",
elements = elements.Count,
compounds = compounds.Count,
retrievedAt = elements[0]!["provenance"]!["retrieved_at"]!.ToString(),
unavailableElementSections,
nullMeaning = "Not available in this snapshot; not zero.",
});
});
// API misses must stay JSON 404s; they must never fall through to the SPA.
app.MapFallback("/api/{**path}", () => Results.Problem(statusCode: 404, title: "Endpoint not available in the scientific profile"));/api/v2/coverage yalnız bu hostta var, gateway'de karşılığı yok. Bugünkü dosyalarla üç bölüm döndürüyor: electromagnetic_and_optical, crystallography ve abundance. Bu bölümler şemada duruyor ama hiçbir elementte değerleri yok; uç da bunu açık açık söylüyor. Host yalnız /api yolunu sınırlıyor: IP başına dakikada 300 istek.
Statik dosyalarda hash'li /assets bir yıllık immutable, index.html ise no-cache alıyor. Sebebini koddaki yoruma yazdım: öyle yapmasam, yeniden yayına aldıktan sonra eski index.html silinmiş varlıklara işaret edebilirdi. science-service/Dockerfile web-app'i VITE_ACCOUNTS_ENABLED=false ile derleyip wwwroot'a koyuyor. docker-compose.science.yml de konteyneri read_only: true ve /tmp için tmpfs ile, yalnız 127.0.0.1:5080'de açıyor. deploy/scripts/present-local.ps1 ise bu dosyayı ayağa kaldırmaktan başka bir şey yapmıyor.
v2'den yalnız istediğin alanlar geliyor
v2'nin bütün mantığı ScientificCatalog sınıfında. Listeler varsayılan olarak view=summary, tek kayıt ise view=full dönüyor. include özete bölüm ekliyor; fields ikisini de ezip yalnız istenen noktalı yolları döndürüyor. Bu yolları ilk kayda karşı doğruluyorum. Bilinmeyen bir alan, dizinin içine girmeye çalışan bir yol (isotopes.mass_number gibi), 32'den fazla yol ya da altı seviyeden derin bir yol 400 alıyor. Birim testi, sonuç boşken de böyle olduğunu ?fields=unknown&q=no-match ile sınıyor.
Element kimliği olarak sembol, atom numarası ya da fe-26 verebilirsin; bileşikte slug ya da PubChem CID. q metni küçük harfe çevirip ı'yı i yapıyor ve aksanları atıyor, yani "bakır", "bakir" ya da "copper" yazsan da aynı kaydı bulursun. category, block, group, period filtreleri yalnız elementlerde geçerli. Sayfa boyutu varsayılan olarak 30, en fazla 100. info.next bağlantısı da filtreleri ve alan seçimini koruyor.
Önbellek davranışı shared-lib/Science/ScientificCatalog.cs'teki CachedJson metodunda:
// Sets ETag + Cache-Control and returns 304 when If-None-Match already names this exact body.
private static IActionResult CachedJson(HttpRequest request, HttpResponse response, string json)
{
var bodyHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json)))[..ETagHashLength];
var etag = "W/\"" + bodyHash + "\"";
var quotedHash = etag[2..];
response.Headers.ETag = etag;
response.Headers.CacheControl = "public, max-age=3600";
var clientTags = request.Headers.IfNoneMatch.ToString().Split(',');
var clientHasThisVersion = clientTags.Any(tag =>
tag.Trim() == "*" || tag.Trim().Replace("W/", "", StringComparison.Ordinal) == quotedHash);
if (clientHasThisVersion)
return new StatusCodeResult(304);
// ...
}ETag, gövdenin SHA-256 özetinin ilk 24 hex karakterinden üretilen zayıf bir etiket. Farklı bir fields seçimi farklı bir temsil sayılıyor ve tam gövde dönüyor; EtagRevalidatesOnlySameRepresentation testi bunu doğruluyor. 304 kararını gövdeyi serileştirdikten sonra verdiğim için sunucu tarafında bir tasarruf yok, kazanç yalnızca ağda. Bu akışı deploy/scripts/test-scientific-api.mjs de sınıyor. science-service yerelde çalışırken şöyle görünüyor:
GET /api/v2/elements/fe?fields=symbol,names.tr,history
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: W/"FDD4E73CB3904E7C82D88813"
{"symbol":"Fe","history":{"discovered_year":null,"discovery_reported":"Ancient","discoverers":null,"etymology":null},"names":{"tr":"Demir"}}
GET /api/v2/elements/fe?fields=symbol,names.tr,history
If-None-Match: W/"FDD4E73CB3904E7C82D88813"
HTTP/1.1 304 Not ModifiedCevaptaki alan sırası da koddan geliyor: Project sığ yolları önce kopyaladığı için names.tr sona düşüyor.
CORS'u iki politikaya ayırdım. gateway-service/Program.cs'teki PublicScience her origin'e yalnız GET ve OPTIONS izni veriyor ve ETag başlığını tarayıcıya açıyor. Bu politikayı yalnız iki v2 rotasına bağladım; diğer rotalar yapılandırılmış origin listesiyle ve PUBLIC_WEB_ORIGIN ile sınırlı. Kontrat script'i, yabancı bir origin'den gelen /api/v1/me/wallet cevabında * olmadığını da kontrol ediyor. v2'nin hiçbir ucu anahtar istemiyor; anahtarlı v1 rotalarını ilk yazıda anlatmıştım.
Uygulamadaki /docs sayfasında bu sözleşmeyi canlı deneyebilirsin. web-app/src/components/developer/endpoints.ts'teki istek listesinde demir ve su örneklerinin yanına bilerek bozulmuş iki istek koydum: ?fields=not_a_field (400) ve /api/v2/compounds/yok (404). "Aynı ETag ile sor" düğmesi son cevabın ETag'ini If-None-Match olarak geri gönderiyor. İstek cache: "no-store" ile atıldığı için ekranda gördüğün 304 tarayıcı önbelleğinden gelmiyor, doğrudan sunucudan geliyor. v1 uçlarında tezgâh, oturumdaki anahtarı X-API-Key olarak ekliyor; kopyalanabilen curl, JavaScript ve Python örnekleri ise anahtarı ortam değişkeninden okuyor.
Hesaplar ve JWT yalnız identity-service'te
Kayıt ve giriş uçları, ASP.NET Core Identity üzerine kurulu AuthController'da. İkisi de önce Turnstile doğrulaması yapıyor; CAPTCHA_SECRET_KEY boşsa bu kontrol kapalı. Şifre kuralını koddaki yorum tek cümlede özetliyor: karmaşıklık yerine uzunluk, yani en az 10 karakter. Beş kez hatalı şifre girersen hesap 15 dakika kilitleniyor. Başarılı giriş, HMAC-SHA256 ile imzalanmış bir JWT döndürüyor. İçinde kullanıcı kimliği, e-posta, ad, soyad ve güvenlik damgası var; süresi appsettings.json'da 120 dakika.
JWT'yi platformda yalnız identity-service doğruluyor, AddJwtBearer başka hiçbir serviste yok. Doğrulama imzayla da bitmiyor (identity-service/Element.Services.Identity.API/Program.cs):
// Changing or resetting the password rotates the security stamp, so every JWT issued before
// that moment is rejected here. A deleted account is rejected the same way.
static async Task RejectTokenWithStaleSecurityStampAsync(TokenValidatedContext context)
{
var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var user = userId is null ? null : await userManager.FindByIdAsync(userId);
var tokenSecurityStamp = context.Principal?.FindFirst(TokenService.SecurityStampClaimType)?.Value;
if (user is null || tokenSecurityStamp != user.SecurityStamp)
{
context.Fail("Session expired. Sign in again.");
}
}Bu yüzden şifre değişince eski oturumlar süresinin dolmasını beklemeden düşüyor. Bedeli de var tabii: identity'ye gelen her JWT'li istek bir kullanıcı sorgusu yapıyor. Cüzdan ve sipariş uçları ise JWT beklemiyor, API anahtarı bekliyor. Web uygulamasında bu iki yolu web-app/src/services/api.ts'te birleştirdim:
/** Wallet, holdings, desk and order endpoints need the dashboard API key besides the JWT. */
const DASHBOARD_PATH = /^\/(me|orders|desk)(\/|$)/;
// ...
const needsDashboardKey = DASHBOARD_PATH.test(path);
if (needsDashboardKey && readStorage("token") && !readStorage("apiKey"))
await apiKeyService.ensureDashboardKey();Bu yollardan birine ilk gittiğinde uygulama JWT ile POST /api/v1/api-keys/generate çağırıyor ve "Web Dashboard Key" açıklamalı bir anahtar alıyor. Anahtar ele_live_ ile başlayan 32 hex karakter; veritabanında yalnız SHA-256 özeti duruyor. Hesap başına 20 etkin anahtar sınırı dolunca identity 409 dönüyor; web de en eski dashboard anahtarını iptal edip bir kez daha deniyor. Her web girişi yeni bir anahtar ürettiği için iptal edilmiş anahtar geçmişini en yeni 20 satıra budadım. Gerekçesi ApiKeyController'daki yorumda.
identity-service öğrenme ilerlemesini de tutuyor. LearningController, PUT /api/v1/auth/learning ile gelen keşifleri ve rotaları Identity'nin AspNetUserTokens tablosuna ElementLearning.v1 sağlayıcı adıyla yazıyor ve her başarıya ayrı, benzersiz bir satır açıyor. Bu yüzden iki cihaz birbirinin üzerine yazmıyor, ikisi de ilerlemeye ekleme yapıyor. Ekleme ON CONFLICT DO NOTHING ile yapıldığı için aynı ilerlemeyi yeniden göndermek zararsız; uç zaten hiçbir şeyi silmiyor. Kabul edilen kimlikler web-app'teki known-compounds.json ve lessons.json'dan geliyor. csproj bu dosyaları identity imajına da kopyalıyor ki istemci uydurma bir başarı kaydedemesin. Bir rota, gerektirdiği keşiflerin hepsi kayıtlıysa tamamlanmış sayılıyor.
Arayüzün çoğu sunucuya hiç gitmiyor
Arayüzü React 19, React Router 7, Vite 8 ve TypeScript ile tek sayfa uygulaması olarak yazdım. Stiller Tailwind CSS 4 ile; diyalog ve menüler için radix-ui, geçişler için framer-motion, ikonlar için lucide-react kullandım. Sunucu tarafında render yok. Aynı derlemeyi Docker imajında nginx, bağımsız atlasta da science-service statik dosya olarak sunuyor. App.tsx'te giriş, piyasa, mağaza ve hesap rotaları AccountsOnly altında duruyor. Derleme VITE_ACCOUNTS_ENABLED=false ile yapıldıysa bu rotalar "Bu kurulumda hesap kapalı" sayfasına düşüyor.
Kayıt sayfalarını useScience kancası besliyor. Aynı iki snapshot scienceCatalog.ts'te import ediliyor ve ilk bilimsel görünümde yaklaşık 1,4 MB'lık ayrı bir parça olarak yükleniyor. Bu yerel kopya elementlerde tam kayıt, bileşiklerde ise laboratuvar kataloğundan üretilmiş kısa bir kayıt. Kayıt ne API'de ne yerel kopyada varsa kanca bu 404'ü ulaşılamayan bir servisten ayırıyor; e2e testinde /element/zz "Kayıt bulunamadı" başlığını gösteriyor. null alanlar varsayılan olarak gizli. "Eksik alanları göster"i işaretlersen "Veri yok" etiketiyle listeleniyorlar.
/lab'ın kimya kuralları tamamen tarayıcıda çalışıyor. Bir karışımı denerken sunucuya istek gitmiyor, cüzdana da dokunulmuyor. Sürükle-bırakı kütüphane kullanmadan, pointer olaylarıyla yazdım. Bastıktan sonra 6 pikselden fazla hareket edersen basış sürüklemeye dönüyor, bırakma hedefini de document.elementFromPoint buluyor; fare, kalem ve dokunma aynı kodla çalışıyor. Palet karoları aynı zamanda birer düğme, yani klavyeyle de atom ekleyebilirsin. "Dene"ye bastığında formCompound atom sayılarını 214 kayıtlık katalogla karşılaştırıyor ve başarısızlığı sınıflandırıyor. Ekrandaki tonu web-app/src/services/lab.ts belirliyor:
/** Feedback for a failed mix: wrong ratio, chemically impossible, not in the catalogue, or nothing on the bench. */
export type MissTone = "almost" | "impossible" | "unknown" | "empty";
/** Maps a failed formCompound result to its miss tone. */
export function missTone(result: Extract<FormResult, { ok: false }>): MissTone {
if (result.code === "wrong_ratio") return "almost";
if (result.code === "empty") return "empty";
if (result.code === "noble" || result.code === "unstable") return "impossible";
return "unknown";
}Doğru elementleri yanlış oranda koyarsan sana aynı elementlerden oluşan katalog kayıtları öneriliyor. İşin içinde soy gaz varsa ya da sayılar yük ve değerlik dengesine uymuyorsa sonuç "impossible". Mümkün görünen ama katalogda olmayan bir karışımda ise "hayali bileşik uydurulmaz" mesajını görürsün. /lab/formula'da katalogdan bir bileşiğin atom sayılarını sen kuruyorsun, /lab/detective'te ipuçlarından elementi tahmin ediyorsun.
Başarılı bir karışım deftere yazılıyor. Defterin üstünde lessons.json'daki altı öğrenme rotası duruyor; her birinde üç keşif ve bir ya da iki soru var. İlerlemenin anahtarı web-app/src/services/useLearning.ts'te:
function storageKey(user: string | null): string {
return `elementapi:learning:${user ?? "guest"}:v1`;
}
// ...
/** Progress recorded on this device before signing in. */
guest: parseRaw(readRaw(null)),
/** Adds this device's guest progress to the signed-in account. */
importGuest() {
persist(user, mergeLearning(progress, parseRaw(readRaw(null))));
},Anahtarda misafir için guest, girişli kullanıcı için JWT'deki sub kullanılıyor. localStorage hata verirse (gizli pencere, dolu kota) ilerleme sekme kapanana kadar bellekte duruyor ve arayüz bunu sana söylüyor. Giriş yaptıysan her değişiklik PUT /auth/learning ile gidiyor, sunucunun döndürdüğü küme de yereldekiyle birleştiriliyor. Misafir keşifleri hesaba kendiliğinden geçmiyor. NotebookSync.tsx sana "Misafir keşiflerimi hesabıma ekle" düğmesini gösteriyor; importGuest iki kümeyi birleştiriyor ve misafir kopyasını silmiyor. Hesap açmadan taşımak istersen defteri sürüm numaralı bir JSON dosyası (elementapi-koleksiyon.json, en fazla 32 KB) olarak indirip başka bir tarayıcıdaki koleksiyona ekleyebilirsin.
Aynı repo, üç ayrı açılış
| Profil | Compose dosyaları | Konteynerler | Host'a açılan portlar |
|---|---|---|---|
| Yalnız atlas | docker-compose.science.yml | science (web derlemesi içinde) | 127.0.0.1:5080 |
| Tam yerel platform | docker-compose.yml | PostgreSQL, Redis, RabbitMQ, dokuz arka servis, web-app | altyapı ve servisler 127.0.0.1'de; gateway 5000, web 6241 |
| Public | docker-compose.yml + docker-compose.public.yml | yukarıdakiler ve Caddy | yalnız Caddy: 80, 443, 443/udp |
Tam platformu deploy/scripts/present-platform.ps1 açıyor. İlk çalıştırmada docker/.env.example'ı docker/.env'e kopyalıyor, sonra docker compose up -d --build çağırıyor. PostgreSQL 16 tek bir konteynerde çalışıyor ve docker/init-scripts/init.sql orada yedi veritabanı açıyor: element_identity_db, element_market_db, element_compound_db, element_order_db, element_wallet_db, element_inventory_db ve element_shipment_db. Uygulama konteynerlerine bellek sınırı koymamın sebebi docker-compose.yml'daki yorumda yazılı: sınır yoksa JVM, .NET ve Node heap'lerini host RAM'ine göre boyutluyor ve OOM killer bir JVM'i öldürüyor. Varsayılan sınır 512 MB ve bir CPU, Java servislerinde 640 MB.
Caddy başta host'a ayrı kurulan bir parçaydı, 19 Eylül'de onu public overlay'in içine aldım. Overlay'in işi docker-compose.public.yml'ın başında yazılı:
# Public single-host stack (dev / test / prod).
# Infra + app ports closed on the host (!override []); only Caddy publishes HTTP(S).
# Do not re-publish postgres/redis/gateway/web ports on a public server.
# ...
services:
# No dev-default credentials on a public host (every service reads the same variables).
postgres:
container_name: element-${ELEMENT_ENV:-prod}-postgres
ports: !override []
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set a unique POSTGRES_PASSWORD}
# ...
caddy:
image: caddy:2.9-alpine
ports:
- "${CADDY_HTTP_PORT:-80}:80"
- "${CADDY_HTTPS_PORT:-443}:443"
- "${CADDY_HTTPS_PORT:-443}:443/udp"Overlay, Caddy dışındaki her servisin (gateway ve web-app dahil) port listesini boşaltıyor. deploy/Caddyfile.elements-api /api* ve /swagger* isteklerini gateway'e, geri kalanını web-app'e gönderiyor. Caddy istek gövdesini 1 MB ile sınırlıyor; HSTS, X-Frame-Options: DENY ve Turnstile'a izin veren bir CSP ekliyor. style-src'de 'unsafe-inline''ın neden kaldığını CSP'nin yanındaki yoruma yazdım: Radix'in Sheet ve Dialog bileşenleri çalışma anında <style> ekliyor. present-public.ps1 aynı overlay'i Dev, Test ve Prod için :8080, :8081, :8082 portlarında yan yana da açabiliyor.
Production'da geliştirme sırrıyla açılmamak
science-service'in korunacak bir sırrı yok. Onun dışındaki bütün .NET servisleri, loglama kurulumu sırasında shared-lib/Extensions/ProductionConfiguration.cs'i çağırıyor. Kontrolün kendisi kısa:
private static readonly string[] GuardedSecretKeys = ["INTERNAL_API_KEY", "JwtSettings:Secret"];
// ...
private static bool IsDevelopmentSecret(string secretValue) =>
secretValue.Length < MinimumSecretLength
|| secretValue.Contains(PlaceholderMarker, StringComparison.OrdinalIgnoreCase)
|| KnownDevelopmentSecrets.Contains(secretValue);Production ortamında bu sırlardan biri 32 karakterden kısaysa, "ChangeMe" içeriyorsa ya da bilinen geliştirme değerlerinden biriyse servis açılmıyor. order-service NODE_ENV=production, wallet-service de ELEMENT_ENV=prod gördüğünde aynı kontrolün kendi sürümünü çalıştırıyor. İç anahtar kullanmayan inventory-service'te bu kontrol yok. Public compose da ${VAR:?} sözdizimi sayesinde veritabanı ve broker şifreleri, JWT sırrı ve iç anahtar verilmeden başlamıyor. present-public.ps1 -Server de yer tutucu ya da kısa sırlarla 80 ve 443'te açılmayı reddediyor.
Jenkins neyi ne zaman çalıştırıyor
CI için kökte bir Jenkinsfile var. Ajan Windows olduğu için adımları bat ile yazdım. İlk aşama, değişen dosyaları PR'ın hedef dalına göre git diff ile buluyor. Bu olmazsa önceki commit'e bakıyor, o da olmazsa her aşamayı açıyor. Bayrakları Jenkinsfile şöyle kuruyor:
env.RUN_DOCS_ONLY = isDocsOnly(files) ? '1' : '0'
env.RUN_ORDER = touchesAny(files, 'order-service/', 'shared-lib/', 'docker/', 'docker-compose') ? '1' : '0'
env.RUN_WEB = touchesAny(files, 'web-app/') ? '1' : '0'
// ...
env.RUN_WALLET = touchesAny(files, 'wallet-service/', 'docker/', 'docker-compose') ? '1' : '0'
env.RUN_INVENTORY = touchesAny(files, 'inventory-service/', 'docker/', 'docker-compose') ? '1' : '0'
env.RUN_E2E = (env.RUN_WEB == '1' || touchesAny(files, 'science-service/')) ? '1' : '0'
env.RUN_SMOKE = touchesAny(files, 'docker/', 'docker-compose', 'deploy/Caddyfile', 'deploy/scripts/') ? '1' : '0'Bayraklar order-service, web-app, .NET birim testleri, wallet ve inventory için mvn test, Playwright ve compose sağlık kontrolü aşamalarını açıp kapatıyor. Yalnız dokümana dokunan bir değişiklik ağır aşamaların hepsini atlıyor. Compose kontrolü ayrıca RUN_COMPOSE_SMOKE=1 de bekliyor.
Playwright aşamasında bağımsız atlası test ortamı olarak kullanıyorum. web-app/playwright.config.ts'teki webServer önce dotnet run ile science-service'i, sonra Vite'ı VITE_ACCOUNTS_ENABLED=false ile açıyor; veritabanı ya da broker gerekmiyor. Her test masaüstü (1365x900) ve mobil (390x844) projelerinde ayrı ayrı koşuyor. e2e/atlas-lab-notebook.spec.ts, /periodic, /lab ve /collection sayfalarının açıldığını başlıklarına bakarak doğrulayan kısa bir duman testi. Asıl akış e2e/lab.spec.ts'te ve bence en keyifli test de bu: yalnız klavyeyle hidrojen karosunda iki kez Enter'a, oksijende bir kez boşluk tuşuna basıyor, "Dene" ile suyu keşfediyor ve /collection'da /compound/h2o'ya bağlanan kartı arıyor. accounts-off.spec.ts dört sayfada 390 piksel genişlikte yatay taşma olmadığını ölçüyor.
Ertelediklerim ve eksikler
Ertelediğim işlerden biri oturumun nerede saklandığı. JWT ve API anahtarı localStorage'da duruyor. docs/runbooks/AUTH-XSS.md bunun sonucunu açıkça yazıyor: origin'deki herhangi bir XSS ikisini de okuyabilir. Verdiğim karar docs/runbooks/AUTH-COOKIE-BACKLOG.md'de:
Keep JWT + API key in `localStorage` for the current demo/beta. Plan migration to:
1. Short-lived access token (memory or short cookie)
2. Refresh token in **httpOnly** + `Secure` + `SameSite=Lax|Strict` cookie
3. CSRF strategy if cookie auth is used for state-changing POSTsAynı dosya neden beklediğimi de anlatıyor: SPA Authorization ve X-API-Key başlıklarına bağlı, çerezli oturum ise identity, web ve CORS tarafının birlikte değişmesini gerektiriyor. Konuya 2026-12-31'de yeniden bakacağım. O zamana kadar ara önlem olarak Caddy'nin CSP ve güvenlik başlıkları, auth hız sınırı ve isteğe bağlı Turnstile var.
Veri tarafındaki boşluklar da kodda görünüyor. Üç element bölümü hiçbir kayıtta dolu değil, 163 bileşik kısa kayıt olarak kalıyor ve 118 elementin yalnızca 75'inde fotoğraf var. docs/adr/SIMPLIFICATION-ROADMAP.md kalan boşlukların lisansı uygun olmayan görsellerle doldurulmamasını istiyor.
Laboratuvarın akla yatkınlık kontrolü de sezgisel: isPlausible'ın başındaki yorum onu yükseltgenme basamakları ve ders kitabı değerliği olarak tanımlıyor, bir yapı üreteci olmadığını da belirtiyor. Metrik ucu yok; Prometheus ve Grafana'nın ADR 0001 ile kaldırılmasını ilk yazıda anlatmıştım. TODO.md'nin 2026-09-26 tarihli operatör listesinde Jenkins sunucusunun kurulumu ve Turnstile anahtarları hâlâ işaretsiz duruyor. Yani Jenkinsfile hazır, ama sunucu tarafındaki kurulum o tarihte bitmemişti.
Atlas element-api.ahmetfuzunkaya.com adresinde, kaynak kodu GitHub'da. Sipariş saga'sı, outbox ve gateway ayrıntıları için ilk yazıya, projenin özeti için proje sayfasına bakabilirsin.
