Skip to main content
AI Skill

Create Orders Manager

Build a single-page Orders Manager application powered by the DataDoe REST API. Use when the user asks to create, scaffold, or build an orders manager, orders dashboard, orders table, or order management app using DataDoe.

Install this skill

Select your AI agent to see the installation instructions for this skill.

Claude Code

Execute this command in your project root:

bash
1npx skills@latest add Deltologic/datadoe-ai-skills --agent claude-code --skill create-orders-manager

Skill reference

SKILL.md

The full skill specification, rendered straight from the source repository.

Orders Manager

Build a complete, working single-page Orders Manager app using the DataDoe REST API. The app must be functional after a single execution.

API docs: https://api.datadoe.com/api/v1/docs Data scheme: https://api.datadoe.com/api/v1/spec/data-scheme

Agent Type Detection

Determine your agent type before starting:

  • Low-level agent (Cursor, Claude Code, Codex, Aider, etc.): follow the Default Tech Stack below.
  • High-level vibe coding assistant (Lovable, Base44, Bolt, v0, etc.): ignore the tech stack section entirely; use whatever the platform provides. Follow only the App Features, DataDoe API Integration, and Implementation Rules sections.

App Features

1. Seller/Vendor Picker

  • On load, fetch connected sellers and vendors from the API.
  • Show a dropdown to select one. Orders cannot be loaded without a selection.
  • Only sellers with sellerCentralConnection !== null are valid (Order Line Items source is SELLER_CENTRAL).

2. Orders Table

  • Display orders in a table with expandable nested rows for line items.
  • Parent row: amazon_order_id, order_date, order_status, fulfillment_channel, total item count, total price.
  • Child rows (expanded): sku, child_asin, product_name, quantity, item_price_value, item_price_currency, item_status.
  • Client-side pagination with configurable page size (10/25/50).

3. Filters

  • Date range: date-from / date-to inputs. Default: last 30 days.
  • Order status: multi-select checkboxes. Values: Pending, Unshipped, PartiallyShipped, Shipped, Canceled, Unfulfillable, InvoiceUnconfirmed, PendingAvailability.
  • Applying filters triggers a new export.

4. Order Tagging (Local)

  • Tags are NOT part of the DataDoe API — they are stored in localStorage.
  • Each order (amazon_order_id) can have zero or more text tags.
  • UI: inline tag chips with "add tag" button + text input.
  • Prefix all localStorage keys with datadoe-orders-manager:: for collision safety.
  • Enforce via TypeScript branded type:
typescript
1type StorageKey = `datadoe-orders-manager::${string}`;

5. Loading & Error States

  • Show a spinner/skeleton during export creation and polling.
  • Show user-friendly error messages for API failures (network errors, 429 rate limits, 4xx/5xx).
  • On 429: auto-retry after Retry-After header value.

DataDoe API Integration

Auth & Config

The app always targets the production DataDoe API at https://api.datadoe.com. During local development, Vite proxies /api requests to this domain to avoid CORS. There is no staging/production distinction — always use the same API target.

.env file structure (create .env.example with the placeholders below):

bash
1# Vite proxies /api → https://api.datadoe.com (CORS bypass for local dev)
2VITE_API_BASE_URL=/api
3VITE_PROXY_TARGET=https://api.datadoe.com
4VITE_DATADOE_API_KEY=<your-api-key>
  • VITE_API_BASE_URL must always be /api — the browser app never calls api.datadoe.com directly.
  • VITE_PROXY_TARGET must always be https://api.datadoe.com — this is the Vite proxy upstream.
  • VITE_DATADOE_API_KEY — the API key obtained from the DataDoe dashboard.

Header selection logic (mutually exclusive — never send both):

  • If VITE_DATADOE_API_KEY is set → attach datadoe-api-key header.
  • Else if VITE_DATADOE_ORGANIZATION_ID is set → attach datadoe-organization-id header.

Always attach Content-Type: application/json and Accept: application/json.

Rate limit: 2 req/s per org. On 429, read Retry-After header, wait, retry.

Anti-Rate-Limit Guardrails (Mandatory)

  1. Global client-side pacing: cap all DataDoe requests to max 1 request every 600ms (<= ~1.67 req/s), leaving headroom under 2 req/s.
  2. Single in-flight flow per seller: never run multiple export flows in parallel for the same seller.
  3. No parallel polling: only one active poll loop per export; cancel/replace stale loops before starting new ones.
  4. Debounce filter-triggered exports: wait 400-800ms after filter changes before creating a new export.
  5. Serialize bursty steps: when chaining create → poll → raw download, avoid extra background fetches in parallel.
  6. Respect Retry-After exactly on 429, then add incremental delay for subsequent retries.

If these constraints conflict with UI responsiveness, prioritize staying below rate limits.

Export Flow (5 steps)

1. Fetch sellers

text
1GET /api/v1/util/sellers-and-vendors
2→ { items: [{ id, name, sellerCentralConnection, vendorCentralConnection, amazonAdsConnection }] }

Filter to items where sellerCentralConnection is not null.

2. Fetch export sources (get source ID)

GET /api/v1/exports/sources?sellerOrVendorIds=<sellerId>

Find the source named Order Line Items (type SELLER_CENTRAL). Hardcode fallback ID: 89b27535d27c2a94db5ae39af4717f542624ff4df7802fd633e16c78674a1778

3. Create export

POST /api/v1/exports

Request body:

json
1{
2  "sellerOrVendorIds": ["<sellerId>"],
3  "sourceId": "89b27535d27c2a94db5ae39af4717f542624ff4df7802fd633e16c78674a1778",
4  "columns": [
5    "amazon_order_id",
6    "order_date",
7    "order_status",
8    "fulfillment_channel",
9    "sku",
10    "line_item_number",
11    "child_asin",
12    "product_name",
13    "item_status",
14    "quantity",
15    "item_price_value",
16    "item_price_currency"
17  ],
18  "from": "2025-01-01T00:00:00.000Z",
19  "to": "2025-01-31T23:59:59.999Z",
20  "outputType": "JSON",
21  "sendToAllOrganizationMembers": false
22}

To filter by status, add filters:

json
1{
2  "filters": {
3    "combinator": "or",
4    "rules": [
5      {
6        "field": "order_status",
7        "operator": "=",
8        "value": "Shipped",
9        "not": false
10      },
11      {
12        "field": "order_status",
13        "operator": "=",
14        "value": "Pending",
15        "not": false
16      }
17    ]
18  }
19}

Response: { id, status: "PENDING", ... }.

4. Poll status (every 5s, timeout 2min)

text
1GET /api/v1/exports/<exportId>
2→ { status: "PENDING" | "PROCESSING" | "COMPLETED" | "ERROR" }

5. Download data

When status is COMPLETED, download. If ERROR, show error.

GET /api/v1/exports/<exportId>/raw
  • 200: redirects (302) to a pre-signed S3 URL — axios must follow the redirect automatically (followRedirects: true in Vite proxy handles this on the server side; the browser axios instance follows it natively).
  • 204: still processing — retry after a few seconds.
  • 404: export not found.

Example row returned in the flat array:

json
1{
2  "seller_id": "54fa3cs2-b69f-4642-82c4-58fe731eed69",
3  "seller_name": "DataDoe UK",
4  "marketplace_name": "United Kingdom",
5  "amazon_order_id": "203-5492174-4518714",
6  "sku": "341_SDA12D_117_FBA",
7  "line_item_number": 0,
8  "order_date": null,
9  "order_status": "Shipped",
10  "fulfillment_channel": "Amazon",
11  "child_asin": "ABCDEFGHIJ",
12  "product_name": "xyz",
13  "item_status": "Shipped",
14  "quantity": 2,
15  "item_price_value": 14.76,
16  "item_price_currency": "GBP"
17}

For the full column reference, see: https://api.datadoe.com/api/v1/spec/data-scheme

Raw Data Shape & Client-Side Grouping Logic

The /raw endpoint returns a flat array of line items. Each element represents one SKU within one order. An order with N distinct SKUs produces N rows all sharing the same amazon_order_id.

CRITICAL — columns: [] (empty array) does NOT return all columns. It returns only implicit seller-context metadata columns (seller_id, seller_name, amazon_selling_partner_id, marketplace_name, etc.) and none of the Order Line Items fields (amazon_order_id, sku, order_status, etc.). Always specify the required columns explicitly.

Field roles:

FieldLevelNotes
amazon_order_idOrderGrouping key — unique per order
order_statusOrderConsistent across all rows for the same order
fulfillment_channelOrderConsistent across all rows for the same order
order_dateOrderAlways null in practice — do not use for display or sorting
skuLine itemUnique product identifier within the order
line_item_numberLine item0-based index of the line item within the order
child_asinLine itemAmazon ASIN
product_nameLine itemFull product name
item_statusLine itemCan differ from order_status
quantityLine itemUnits for this SKU only
item_price_valueLine itemUnit price (multiply by quantity for line total)
item_price_currencyLine itemConsistent across all rows for the same order

How to build the parent (order) row from the flat array:

typescript
1// Group by amazon_order_id — preserve API row order (order_date is always null)
2const ordersMap = new Map<string, OrderRow>();
3
4for (const item of lineItems) {
5  const existing = ordersMap.get(item.amazon_order_id);
6  if (existing) {
7    existing.lineItems.push(item);
8    existing.totalPrice += item.item_price_value * item.quantity;
9    existing.itemCount += item.quantity;
10  } else {
11    ordersMap.set(item.amazon_order_id, {
12      amazon_order_id: item.amazon_order_id,
13      order_status: item.order_status, // order-level, consistent
14      fulfillment_channel: item.fulfillment_channel, // order-level, consistent
15      order_date: item.order_date, // null — show "—" or omit
16      currency: item.item_price_currency, // consistent per order
17      totalPrice: item.item_price_value * item.quantity,
18      itemCount: item.quantity,
19      lineItems: [item],
20    });
21  }
22}
23
24const orders = Array.from(ordersMap.values());
25// Do NOT sort by order_date — it is always null.
26// Preserve insertion order (API natural order) or sort by amazon_order_id alphabetically.

Parent row columns to display:

ColumnSourceDisplay
Order IDamazon_order_idMonospace, full string
Order Dateorder_dateAlways null → show
Statusorder_status (first item)Coloured badge
Channelfulfillment_channel (first item)Badge
Itemssum(quantity) across all line itemsNumber
Totalsum(item_price_value × quantity)Formatted currency
TagslocalStorage onlyTag chips

Child (line item) row columns to display:

ColumnSource
SKUsku
ASINchild_asin
Productproduct_name
Qtyquantity
Priceitem_price_value + item_price_currency
Item Statusitem_status

CRITICAL — API parameter formats discovered through testing:

  • GET /exports/sources uses bare query param sellerOrVendorIds=<uuid>not sellerOrVendorIds[]=<uuid> and not sellerOrVendorIds[0]=<uuid>. The bracket syntax is rejected with 400.
  • POST /exports columns must be an explicit list of the field names you need — never pass [] (empty). An empty array returns only seller-context metadata with no order fields, causing the table to display empty rows.
  • Date range uses top-level from and to fields (ISO 8601 datetime strings like 2025-01-01T00:00:00.000Z) — not inside filters.
  • filters uses combinator with lowercase values: "and" or "or" (not "AND" / "OR").
  • Each filter rule must include the not field (boolean): { "field": "...", "operator": "...", "value": "...", "not": false }.
  • If no filter rules are needed, either omit filters entirely or pass { "combinator": "and", "rules": [] }.

Default Tech Stack (Low-Level Agents Only)

Skip this section if you are a high-level assistant.

ToolVersion / Notes
ViteLatest, React + TypeScript template (npm create vite@latest)
React18+ with TypeScript strict mode
axios1.13.6 only (security requirement)
TanStack Table@tanstack/react-table latest
day.jsLatest, for date formatting and manipulation
Lucide ReactLatest, for icons
CSS ModulesNative CSS, no Tailwind or component libraries
Google SansFont via Google Fonts CDN

Setup

Vite creates a nested directory. Move all files up to the workspace root so the project lives at the top level:

bash
1npm create vite@latest orders-manager -- --template react-ts
2shopt -s dotglob && mv orders-manager/* . && rmdir orders-manager
3npm install axios@1.13.6 @tanstack/react-table dayjs lucide-react

Required Execution Order

  1. Scaffold Vite app in orders-manager.
  2. Move generated files/folders to workspace root.
  3. Install dependencies.
  4. Add .env to .gitignore.
  5. Create .env.example.
  6. Verify API connectivity with curl.
  7. Implement app code using exact snippets from the Mandatory Code Snippets section.
  8. Run build/lint checks.

File Structure (Feature-Based)

Every component lives in its own directory containing exactly two files: the component file and its CSS Module. No component files sit loose in a parent folder.

text
1src/
2├── main.tsx
3├── App.tsx
4├── App.module.css
5├── shared/
6│   ├── styles/
7│   │   ├── global.css          # imports tokens, typography, palette
8│   │   ├── tokens.css          # CSS custom properties: colors, spacing
9│   │   ├── typography.css      # font families, sizes, weights
10│   │   ├── colors.css          # color palette utility classes
11│   │   └── spacing.css         # margin/padding utility classes
12│   ├── components/
13│   │   ├── DataTable/
14│   │   │   ├── DataTable.tsx        # generic TanStack Table wrapper
15│   │   │   └── DataTable.module.css
16│   │   ├── Spinner/
17│   │   │   ├── Spinner.tsx
18│   │   │   └── Spinner.module.css
19│   │   └── ErrorMessage/
20│   │       ├── ErrorMessage.tsx
21│   │       └── ErrorMessage.module.css
22│   ├── hooks/
23│   │   └── useLocalStorage.ts
24│   ├── utils/
25│   │   └── localStorage.ts  # branded StorageKey type, get/set helpers
26│   └── api/
27│       └── client.ts        # axios instance + 429 retry interceptor
28├── features/
29│   └── orders/
30│       ├── OrdersManager/
31│       │   ├── OrdersManager.tsx
32│       │   └── OrdersManager.module.css
33│       ├── OrdersTable/
34│       │   ├── OrdersTable.tsx
35│       │   └── OrdersTable.module.css
36│       ├── OrderFilters/
37│       │   ├── OrderFilters.tsx
38│       │   └── OrderFilters.module.css
39│       ├── OrderTags/
40│       │   ├── OrderTags.tsx
41│       │   └── OrderTags.module.css
42│       ├── SellerPicker/
43│       │   ├── SellerPicker.tsx
44│       │   └── SellerPicker.module.css
45│       ├── hooks/
46│       │   ├── useOrders.ts
47│       │   ├── useExport.ts
48│       │   └── useSellers.ts
49│       ├── types.ts
50│       └── utils.ts

Styling Rules

  • Define design tokens as CSS custom properties in tokens.css.
  • Import Google Sans in global.css: @import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap');
  • Each component gets its own .module.css file.
  • No inline styles. No Tailwind. No component libraries.

Generic DataTable Component

Create a generic DataTable<T> component that:

  • Accepts columns: ColumnDef<T>[], data: T[], and optional renderSubComponent.
  • Handles pagination (page size selector: 10/25/50).
  • Uses getExpandedRowModel() for nested row expansion.
  • Renders via CSS Modules — the orders feature passes its own column definitions.

localStorage Key Safety

typescript
1type StorageKey = `datadoe-orders-manager::${string}`;
2
3function getItem(key: StorageKey): string | null {
4  return localStorage.getItem(key);
5}
6
7function setItem(key: StorageKey, value: string): void {
8  localStorage.setItem(key, value);
9}

Mandatory API Connectivity Check (before UI)

Never edit .env automatically — treat it as a protected secrets file.

Step 1: Direct Target Connectivity

bash
1source .env
2curl -sS -o /tmp/datadoe-sellers.json -D /tmp/datadoe-sellers.headers \
3  "$VITE_PROXY_TARGET/api/v1/util/sellers-and-vendors" \
4  -H "datadoe-api-key: $VITE_DATADOE_API_KEY" \
5  -H "Accept: application/json" \
6  -H "Content-Type: application/json"
7cat /tmp/datadoe-sellers.headers | head -1

Expected: HTTP/2 200. Continue only when direct check returns HTTP 200. If 403: wrong API key for the target. If 502: DNS/network issue. Stop and fix before coding.

Step 2: Local Proxy Verification (after npm run dev)

After starting the dev server, read its output to find the actual port (may not be 5173 if that port is in use), then run:

bash
1source .env
2DEV_PORT=<actual-port-from-vite-output>
3curl -sS -o /tmp/datadoe-proxy-sellers.json -D /tmp/datadoe-proxy-sellers.headers \
4  "http://localhost:${DEV_PORT}/api/v1/util/sellers-and-vendors" \
5  -H "datadoe-api-key: $VITE_DATADOE_API_KEY" \
6  -H "Accept: application/json" \
7  -H "Content-Type: application/json"
8cat /tmp/datadoe-proxy-sellers.headers | head -1

Expected: HTTP/1.1 200 OK. Finish only when localhost proxy check also returns HTTP 200.

Debug Path for 403/502

If either check fails, follow this mandatory debug strategy:

  • 502 + ENOTFOUND: proxy target DNS/network mismatch. Verify VITE_PROXY_TARGET is exactly https://api.datadoe.com and that the process has network access. If running in a sandbox, restart with full network permissions.
  • 502 on wrong localhost port: check actual Vite port from terminal output and retry on the correct port.
  • 403: API key/target environment mismatch (e.g., staging key used against production target). Verify the API key matches the target.
  • After any fix, re-run both checks (direct target and localhost proxy).

Mandatory Code Snippets

The following files must be implemented exactly as shown. These are verified working against the production API. Do not modify the proxy configuration, base URL logic, header logic, or request parameter shapes.

vite.config.ts — Proxy Configuration

typescript
1import { defineConfig, loadEnv } from "vite";
2import react from "@vitejs/plugin-react";
3
4export default defineConfig(({ mode }) => {
5  const env = loadEnv(mode, process.cwd(), "");
6  const proxyTarget = env.VITE_PROXY_TARGET || "https://api.datadoe.com";
7
8  return {
9    plugins: [react()],
10    server: {
11      proxy: {
12        "/api": {
13          target: proxyTarget,
14          changeOrigin: true,
15          secure: true,
16          followRedirects: true,
17        },
18      },
19    },
20  };
21});

Why this works: The browser sends requests to /api/v1/... on localhost. Vite's dev server proxies them to https://api.datadoe.com/api/v1/.... changeOrigin: true sets the Host header to the target. secure: true enforces TLS verification. followRedirects: true handles any 3xx from the API.

src/api/client.ts — Axios Instance with Pacing & 429 Retry

typescript
1import axios from "axios";
2import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
3
4const BASE_URL = import.meta.env.VITE_API_BASE_URL || "/api";
5const API_KEY = import.meta.env.VITE_DATADOE_API_KEY;
6const ORG_ID = import.meta.env.VITE_DATADOE_ORGANIZATION_ID;
7
8const MIN_REQUEST_INTERVAL_MS = 600;
9let lastRequestTime = 0;
10
11async function waitForPacing(): Promise<void> {
12  const now = Date.now();
13  const elapsed = now - lastRequestTime;
14  if (elapsed < MIN_REQUEST_INTERVAL_MS) {
15    await new Promise((resolve) =>
16      setTimeout(resolve, MIN_REQUEST_INTERVAL_MS - elapsed)
17    );
18  }
19  lastRequestTime = Date.now();
20}
21
22function buildAuthHeaders(): Record<string, string> {
23  if (API_KEY) {
24    return { "datadoe-api-key": API_KEY };
25  }
26  if (ORG_ID) {
27    return { "datadoe-organization-id": ORG_ID };
28  }
29  return {};
30}
31
32const instance: AxiosInstance = axios.create({
33  baseURL: BASE_URL,
34  headers: {
35    "Content-Type": "application/json",
36    Accept: "application/json",
37    ...buildAuthHeaders(),
38  },
39});
40
41instance.interceptors.request.use(async (config) => {
42  await waitForPacing();
43  return config;
44});
45
46instance.interceptors.response.use(
47  (response) => response,
48  async (error) => {
49    const response = error.response as AxiosResponse | undefined;
50    if (response?.status === 429) {
51      const retryAfter = parseInt(response.headers["retry-after"] || "1", 10);
52      const delay = retryAfter * 1000 + 200;
53      await new Promise((resolve) => setTimeout(resolve, delay));
54      return instance.request(error.config as AxiosRequestConfig);
55    }
56    return Promise.reject(error);
57  }
58);
59
60export default instance;

Critical details:

  • BASE_URL is always /api (from .env), never a full URL — all requests go through the Vite proxy.
  • Auth headers use datadoe-api-key (not datadoe-organization-id) when both are available. Never send both.
  • The pacing interceptor enforces 600ms minimum between requests (< 2 req/s limit).
  • The 429 interceptor reads Retry-After, waits, and retries once.

src/api/datadoe.ts — API Functions with Correct Request Shapes

typescript
1import client from "./client";
2
3const ORDER_LINE_ITEMS_SOURCE_ID =
4  "89b27535d27c2a94db5ae39af4717f542624ff4df7802fd633e16c78674a1778";
5
6export async function fetchSellersAndVendors() {
7  const response = await client.get<{ items: any[] }>(
8    "/v1/util/sellers-and-vendors"
9  );
10  return response.data.items;
11}
12
13// IMPORTANT: query param is sellerOrVendorIds=<uuid> (no brackets)
14export async function fetchOrderLineItemsSourceId(
15  sellerId: string
16): Promise<string> {
17  try {
18    const response = await client.get<any[]>(
19      `/v1/exports/sources?sellerOrVendorIds=${sellerId}`
20    );
21    const orderSource = response.data.find(
22      (s: any) => s.name === "Order Line Items" && s.type === "SELLER_CENTRAL"
23    );
24    return orderSource?.id ?? ORDER_LINE_ITEMS_SOURCE_ID;
25  } catch {
26    return ORDER_LINE_ITEMS_SOURCE_ID;
27  }
28}
29
30// Required columns for the Order Line Items source.
31// NEVER pass columns: [] — an empty array returns only seller-context metadata
32// (seller_id, seller_name, marketplace_name, etc.) with NO order fields,
33// which causes the table to display empty rows.
34const ORDER_LINE_ITEMS_COLUMNS = [
35  "amazon_order_id",
36  "order_date",
37  "order_status",
38  "fulfillment_channel",
39  "sku",
40  "line_item_number",
41  "child_asin",
42  "product_name",
43  "item_status",
44  "quantity",
45  "item_price_value",
46  "item_price_currency",
47];
48
49// IMPORTANT: export creation request shape — all fields are mandatory.
50// - from/to are TOP-LEVEL ISO datetime strings (not inside filters).
51// - columns must be an EXPLICIT list of field names (never empty []).
52// - sendToAllOrganizationMembers is required (use false).
53// - filters.combinator is lowercase: "and" or "or".
54// - each filter rule must have a "not" boolean field.
55export async function createExport(
56  sellerId: string,
57  sourceId: string,
58  dateFrom: string,
59  dateTo: string,
60  orderStatusFilters: string[] = []
61) {
62  const body: Record<string, unknown> = {
63    sourceId,
64    sellerOrVendorIds: [sellerId],
65    outputType: "JSON",
66    columns: ORDER_LINE_ITEMS_COLUMNS,
67    sendToAllOrganizationMembers: false,
68    from: `${dateFrom}T00:00:00.000Z`,
69    to: `${dateTo}T23:59:59.999Z`,
70  };
71
72  if (orderStatusFilters.length > 0) {
73    body.filters = {
74      combinator: "or",
75      rules: orderStatusFilters.map((status) => ({
76        field: "order_status",
77        operator: "=",
78        value: status,
79        not: false,
80      })),
81    };
82  }
83
84  const response = await client.post<any>("/v1/exports", body);
85  return response.data;
86}
87
88export async function pollExport(exportId: string) {
89  const response = await client.get<any>(`/v1/exports/${exportId}`);
90  return response.data;
91}
92
93export async function fetchExportRaw(exportId: string) {
94  const response = await client.get(`/v1/exports/${exportId}/raw`, {
95    validateStatus: (status) => status === 200 || status === 204,
96  });
97  if (response.status === 204) return null;
98  return response.data;
99}

.env.example

bash
1# Vite proxies /api → https://api.datadoe.com (CORS bypass for local dev)
2VITE_API_BASE_URL=/api
3VITE_PROXY_TARGET=https://api.datadoe.com
4VITE_DATADOE_API_KEY=<your-api-key>

Implementation Rules

  1. Single run: the app must work after one execution. Do not leave TODOs or placeholders.
  2. Test API calls first: before coding the full UI, verify the export flow works by running the mandatory connectivity checks.
  3. Group line items by order: the API returns flat rows. Group by amazon_order_id before rendering.
  4. Handle empty states: no sellers, no orders, export still loading — all need UI.
  5. Date defaults: from = 30 days ago, to = today.
  6. Use outputType: "JSON" for exports.
  7. Poll interval: 5 seconds between status checks. Timeout after 2 minutes with an error message.
  8. Retry on 429: auto-retry with the Retry-After value, with incrementally increasing delays.
  9. Use the exact code snippets from the Mandatory Code Snippets section for vite.config.ts, src/api/client.ts, and src/api/datadoe.ts.
  10. Run build and lint before finalizing.
  11. Leave dev server running in background after implementation.
  12. Report local URL, proxy check result, and any residual risk explicitly.

Protected Files

Never modify .env.

  • Use .env.example as reference and documentation target.
  • Never print .env secrets in output.

Post-Implementation Startup

Always run dev server in background:

bash
1npm run dev

Verification Checklist

After implementation, verify:

  • App starts without errors (npm run dev).
  • Seller picker loads and shows sellers.
  • Selecting a seller and clicking "Load Orders" triggers the export flow.
  • Orders appear in the table grouped by amazon_order_id.
  • Expanding a row shows line items.
  • Date filter changes trigger new exports.
  • Status filter with checkboxes works.
  • Tags can be added, removed, and persist across page reloads.
  • Loading spinner shows during export polling.
  • Error messages display on API failure.
  • Pagination controls work (10/25/50 per page).