u
This commit is contained in:
53
.cursor/rules/no-assumed-api.mdc
Normal file
53
.cursor/rules/no-assumed-api.mdc
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
description: Nie API-Verträge raten — immer Backend-Doku lesen
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Keine angenommenen APIs
|
||||
|
||||
API-Shapes, Endpoints und Response-Felder **niemals erraten**, **niemals** aus ähnlichen Endpoints ableiten, **niemals** im Chat „vorschlagen“ und dann direkt implementieren.
|
||||
|
||||
## Quelle der Wahrheit (in dieser Reihenfolge)
|
||||
|
||||
1. **Laufendes Backend** — `dashback/docs/api/` (Markdown pro Route) und `dashback/docs/api/openapi.json`
|
||||
2. **`src/api/index.js`** — nur Spiegel des Backend-Vertrags, keine Erfindung
|
||||
3. **Explizite User-Vorgabe** — nur wenn der User den Vertrag wörtlich nennt
|
||||
|
||||
Fehlt (1): **nicht implementieren**. Stattdessen Doku lesen oder User fragen.
|
||||
|
||||
## Verboten
|
||||
|
||||
```js
|
||||
// ❌ „PATCH liefert { article }, also sicher auch POST …/image“
|
||||
requireObject(res.article, 'article');
|
||||
|
||||
// ❌ Endpoint oder Feldname erfinden
|
||||
return httpMultipart(`/api/article/${sku}/photos`, …);
|
||||
|
||||
// ❌ Response-Shape aus Konvention ableiten
|
||||
onArticleUpdated?.(res.article ?? res.data);
|
||||
```
|
||||
|
||||
```js
|
||||
// ✅ Vertrag aus dashback übernommen, in index.js dokumentiert
|
||||
// POST /api/article/:sku/image → 201 { image: { picId, sort, full, thumb, hash, thumbHash } }
|
||||
requireObject(res.image, 'image');
|
||||
```
|
||||
|
||||
## Pflicht-Workflow bei neuem oder geändertem Endpoint
|
||||
|
||||
1. Route in `dashback/docs/api` oder `openapi.json` finden und lesen
|
||||
2. Method, Path, Request-Body, Status-Codes, Response-JSON exakt notieren
|
||||
3. `src/api/index.js` kommentieren und Client-Methode schreiben
|
||||
4. Response mit `requireObject` / `requireArray` / `requireDefined` aus `apiShape.js` validieren
|
||||
5. Kein Frontend-Code, der Felder nutzt, die nicht im Vertrag stehen (z. B. `photo.url` wenn nur `thumbHash` dokumentiert ist)
|
||||
|
||||
## Erlaubt
|
||||
|
||||
- **Backend-Doku aktualisieren**, wenn der User den Vertrag bewusst ändert — danach Frontend anpassen
|
||||
- **Blockieren mit Frage**, wenn Vertrag unklar oder widersprüchlich ist
|
||||
|
||||
## Zusammenspiel mit `no-silent-fallbacks`
|
||||
|
||||
- Keine Fallbacks auf alternative Feldnamen oder Endpoints
|
||||
- Kein „vielleicht heißt es `article` oder `image`“ — der Vertrag legt es fest
|
||||
@@ -180,7 +180,7 @@ export const article = {
|
||||
/**
|
||||
* POST /api/article/:sku/image
|
||||
* Body: raw AVIF bytes (Content-Type: image/avif, max 1000×1000, prepared client-side via WASM)
|
||||
* → 201 { article }
|
||||
* → 201 { image: { picId, sort, full, thumb, hash, thumbHash } }
|
||||
*/
|
||||
uploadImage(sku, file) {
|
||||
return httpBinary(`/api/article/${encodeURIComponent(sku)}/image`, {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { notifyError } from '../utils/notify';
|
||||
import { bootstrapSession as bootstrapSessionThunk } from '../store/authSlice';
|
||||
|
||||
let sessionBootstrapped = false;
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import { connect } from 'react-redux';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import GoogleIcon from '@mui/icons-material/Google';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { notifyErrorMessage } from '../utils/notify';
|
||||
import { loginWithGoogle, selectAuthStatus, selectGoogleClientId } from '../store/authSlice';
|
||||
import {
|
||||
abortGoogleSignIn,
|
||||
FEDCM_BLOCKED_MESSAGE,
|
||||
@@ -61,7 +63,7 @@ class LoginPage extends Component {
|
||||
const detail = [err?.message || String(err), err?.code, err?.status]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
enqueueSnackbar(`Login failed: ${detail}`, { variant: 'error' });
|
||||
notifyErrorMessage(`Login failed: ${detail}`);
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { requireObject } from '../../utils/apiShape';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
|
||||
import { pasteImageOrFallback } from '../../utils/pasteImage';
|
||||
import { primaryPhoto } from '../../utils/articleDetail';
|
||||
import { articleWithUploadedPhoto, primaryPhoto } from '../../utils/articleDetail';
|
||||
import ArticlePhoto from './ArticlePhoto';
|
||||
|
||||
export default function ArticlePhotoTile({
|
||||
@@ -28,8 +28,8 @@ export default function ArticlePhotoTile({
|
||||
const file = await prepareArticlePhoto(rawBlob);
|
||||
const res = await article.uploadImage(articleData.sku, file);
|
||||
requireObject(res, 'POST /api/article/:sku/image');
|
||||
requireObject(res.article, 'article');
|
||||
onArticleUpdated?.(res.article);
|
||||
requireObject(res.image, 'image');
|
||||
onArticleUpdated?.(articleWithUploadedPhoto(articleData, res.image));
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
} finally {
|
||||
|
||||
@@ -10,6 +10,24 @@ export function primaryPhoto(article) {
|
||||
return photos[0] ?? null;
|
||||
}
|
||||
|
||||
/** Map POST /api/article/:sku/image → photos[] entry shape. */
|
||||
export function photoFromUploadedImage(image) {
|
||||
return {
|
||||
sort: image.sort,
|
||||
thumbW: image.thumb.w,
|
||||
thumbH: image.thumb.h,
|
||||
thumbHash: image.thumbHash,
|
||||
};
|
||||
}
|
||||
|
||||
export function articleWithUploadedPhoto(article, image) {
|
||||
const photo = photoFromUploadedImage(image);
|
||||
const photos = sortedPhotos(article).filter((row) => row.sort !== photo.sort);
|
||||
photos.push(photo);
|
||||
photos.sort((a, b) => a.sort - b.sort);
|
||||
return { ...article, photos };
|
||||
}
|
||||
|
||||
export function primaryPrice(article) {
|
||||
if (!Array.isArray(article?.prices) || article.prices.length === 0) return null;
|
||||
return article.prices.find((row) => row.minmultiplier === 1) ?? article.prices[0];
|
||||
|
||||
Reference in New Issue
Block a user