[Intum Pomoc](https://intum.pl/pomoc.md) / [API](https://intum.pl/pomoc/noe/api.md)

# [Aplikacje Noe](https://intum.pl/pomoc/noe/api/aplikacje.md)

Tworzenie, aktualizacja i uruchamianie aplikacji webowych Noe przez API.

## Jak robimy aplikacje Noe (skrót)

- Aplikacja to rekord z polem `source_code` w wybranym silniku: `svelte` (domyślny, Svelte 5 z `$state`), `vue`, `react`, `js`, `html`; CSS przez `css_engine: "tailwind_all"`. Serwer sam kompiluje kod
- `url_code` daje przyjazny adres (np. "pogoda" → `/a/pogoda`), `public: true` udostępnia apkę bez logowania
- Dane trzymamy w bazie Noe (`db_code` + publiczne metody bazy) albo pobieramy z zewnętrznych API przez konektory
- Logika backendowa idzie przez **akcje aplikacji**: wpis w `actions` wskazuje flow (`flow_code`), frontend woła `POST /noe/apps/:id/action/:nazwa`, a flow wykonuje resztę (konektory, transformacje, warunki). Frontend nigdy nie woła zewnętrznych API bezpośrednio
- **Bezpieczeństwo:** NIGDY nie wpisuj `api_token` ani sekretów w `source_code` — kod aplikacji jest widoczny dla użytkowników. Do danych publicznych używaj `public_methods` bazy Noe albo konektora z metodami `public: true`
- Poprawki kodu rób przez `PATCH /noe/apps/:id/replace.json` (`field: "source_code"`, `old_string`/`new_string`) zamiast przesyłać cały kod od nowa

## Przykład: prosta apka z custom connectorem, custom flow i cronem

Wzorzec "pobieraj cyklicznie z zewnętrznego API, przy danej wartości zareaguj, pokazuj wynik w apce":

1. **Custom connector** (`kind: "noe/custom_connector"`) do zewnętrznego API - metody HTTP deklarujesz w `fields.methods` (path z interpolacją `{nazwa}`), bazowy adres w `url`, klucz API w `secret_token`
2. **Custom flow** (`kind: "noe/custom_flow"`) z programem w `fields.program`: krok `connector` (pobranie danych) → `mutation` (wyciągnięcie pól) → `if` z warunkiem na wyniku (np. `"{json_object.temperature} > 30"`), a w gałęzi `then` kolejny krok `connector` odpalający inne API (alert/powiadomienie)
3. **Cykliczna akcja konta** budząca flow według harmonogramu:

   ```json
   POST /account/crons.json
   { "cron": { "kind": "flow", "target_code": "KOD_FLOW", "schedule": "*/30 * * * *", "active": true } }
   ```

   Flow musi mieć ustawiony `code`. Cron przekazuje na wejściu `{ "kind": "cron", "cron_id": ... }` - parametry startowe (np. współrzędne) ustaw pierwszym krokiem `mutation`. Przebiegi programu sprawdzisz w `/connect/flow_processes`, cron ręcznie odpalisz przyciskiem "Uruchom teraz" w `/account/crons`
4. **Aplikacja Noe** z akcją `{ "action": "get_data", "flow_code": "KOD_FLOW", "public": true }` - frontend woła akcję (`POST /noe/apps/:id/action/get_data`) i renderuje dane; cron w tle dba o cykliczne odświeżanie/alerty


**Autoryzacja:** `Authorization: Bearer TOKEN`
**Content-Type:** `application/json`

## API Endpoints

| Metoda | Sciezka | Opis |
|--------|---------|------|
| GET | `/noe/apps.json` | Lista aplikacji |
| GET | `/noe/apps/:id.json` | Szczegoly aplikacji |
| POST | `/noe/apps.json` | Utworzenie aplikacji |
| PATCH | `/noe/apps/:id.json` | Aktualizacja |
| DELETE | `/noe/apps/:id.json` | Usuniecie |
| GET | `/a/:url_code` | Uruchomienie aplikacji (publiczny URL) |
| PATCH | `/noe/apps/:id/replace.json` | Czesciowa edycja pola |

## Pola aplikacji

| Pole | Typ | Wymagane | Opis |
|------|-----|----------|------|
| `name` | string | tak | Nazwa aplikacji |
| `url_code` | string | nie | Przyjazny URL slug (np. "kalkulator" -> `/a/kalkulator`) |
| `description` | string | nie | Opis aplikacji |
| `app_engine` | string | tak | Silnik: `svelte`, `vue`, `react`, `js`, `html`, `kaboom`, `phaser` |
| `css_engine` | string | tak | CSS: `tailwind_all` (CDN), `tailwind` (kompilowany), `none` |
| `source_code` | string | tak | Kod zrodlowy (max 200 000 znakow) |
| `public` | boolean | nie | `true` = dostepna bez logowania (domyslnie `false`) |
| `active` | boolean | nie | Czy aplikacja jest aktywna (domyslnie `true`) |
| `db_code` | string | nie | Kod bazy Noe::Db (wiele aplikacji moze wspoldzielic jedna baze) |
| `show_in_module` | string | nie | Osadza aplikacje w module (np. `organize`, `mail`, `crm`) |
| `dashboard_widget` | boolean | nie | Apka na liscie "Dodaj kafelki" pulpitow Toolkit (zapis w `fields.dashboard_widget`, reszta `fields` zostaje). Bez tego apki nie da sie dodac jako kafelek |
| `prompt` | string | nie | Prompt systemowy dla AI |
| `actions` | array | nie | Definicje akcji do integracji z Flow |
| `fields` | object | nie | Pola uzytkownika (JSON) |

## Silniki aplikacji

| Silnik | Opis | Kompilacja | CSS |
|--------|------|------------|-----|
| `svelte` | Svelte 5 z `$state`/`$derived` | tak (serwer kompiluje) | tak |
| `vue` | Vue 3 SFC (Composition API) | tak | tak |
| `react` | React 19 z JSX | tak | tak |
| `js` | Czysty JavaScript | nie (source_code = app_code) | nie |
| `html` | Czysty HTML | nie (source_code = app_code) | nie |
| `kaboom` | Kaboom.js (gry 2D) | tak | nie |
| `phaser` | Phaser (gry 2D/3D) | tak | nie |

## Silniki CSS

| Silnik | Opis | Kiedy uzywac |
|--------|------|-------------|
| `tailwind_all` | Tailwind CSS przez CDN (`<script src="cdn.tailwindcss.com">`) | Domyslny — zawsze dziala, wiekszy rozmiar strony |
| `tailwind` | Tailwind kompilowany na serwerze — mniejszy rozmiar | Dla produkcyjnych apek (tylko uzywane klasy) |
| `none` | Bez CSS frameworka | Dla apek bez stylowania lub z wlasnym CSS |

## Utworzenie aplikacji

```
POST /noe/apps.jsonContent-Type: application/json

{
  "app": {
    "name": "Nazwa aplikacji",
    "url_code": "kod-url",
    "description": "Opis",
    "app_engine": "svelte",
    "css_engine": "tailwind_all",
    "public": true,
    "source_code": "KOD_ZRODLOWY"
  }
}
```

### Sukces — POST (201 Created)

```json
{
  "id": "019cb0d4-47be-7ea0-b3e1-ea8bec016f89",
  "name": "Nazwa aplikacji",
  "url_code": "kod-url",
  "description": "Opis",
  "app_engine": "svelte",
  "css_engine": "tailwind_all",
  "source_code": "...",
  "public": true,
  "active": true,
  "app_build_time": 79,
  "css_build_time": 0,
  "fields": {},
  "config": {},
  "actions": [],
  "db_code": null,
  "show_in_module": null,
  "created_by_id": 1,
  "updated_by_id": null,
  "created_at": "2026-03-03T00:13:47.964+01:00",
  "updated_at": "2026-03-03T00:13:47.964+01:00"
}
```

**Uwaga:** Odpowiedz NIE zawiera: `account_id`, `app_code`, `css_code`, `token`.

### Blad walidacji (422 Unprocessable Content)

```json
{
  "name": ["can't be blank"],
  "app_engine": ["is not included in the list"],
  "source_code": ["blad kompilacji: ..."]
}
```

## Aktualizacja aplikacji

```
PATCH /noe/apps/:id.jsonContent-Type: application/json

{
  "app": {
    "source_code": "NOWY_KOD"
  }
}
```

Mozna wyslac tylko zmienione pola — reszta pozostaje bez zmian.

### Replace API (czesciowa edycja pola)

Pozwala zmienic fragment `source_code`, `fields` lub `prompt` bez przesylania calego pola:

```
PATCH /noe/apps/:id/replace.jsonContent-Type: application/json

{
  "field": "source_code",
  "old": "let count = $state(0)",
  "new": "let count = $state(10)"
}
```

## URL aplikacji

Aplikacja jest dostepna pod adresem:
- **`/a/:url_code`** — publiczny URL (np. `/a/kalkulator`)
- **`/a/:id`** — URL po UUID (np. `/a/019cb0d4-47be-7ea0-b3e1-ea8bec016f89`)

Pelny URL: `https://{host}/a/{url_code}`

**Wazne:** URL to `/a/` (nie `/noe/`).

## Svelte 5 — skladnia

Svelte 5 uzywa "runes" — nowy system reaktywnosci:

```svelte
<script>
  // Stan reaktywny
  let count = $state(0);
  let items = $state([]);
  let name = $state("");

  // Wartosc wyliczana (automatycznie aktualizowana)
  let doubled = $derived(count * 2);
  let total = $derived(items.length);

  // Efekt (uruchamiany przy zmianie zaleznosci)
  $effect(() => {
    console.log("Count zmieniony:", count);
  });

  // Funkcje
  function increment() {
    count++;
  }

  function addItem() {
    items = [...items, { name }];
    name = "";
  }
</script>

<!-- Template -->
<button onclick={increment}>Count: {count} (doubled: {doubled})</button>

<input bind:value={name} placeholder="Nowy element" />
<button onclick={addItem}>Dodaj</button>

{#each items as item, i}
  <div>{i + 1}. {item.name}</div>
{/each}

{#if count > 10}
  <p>Duzo!</p>
{:else}
  <p>Malo</p>
{/if}
```

### Kluczowe roznice Svelte 5 vs Svelte 4

| Svelte 4 | Svelte 5 |
|----------|----------|
| `let count = 0;` | `let count = $state(0);` |
| `$: doubled = count * 2;` | `let doubled = $derived(count * 2);` |
| `$: { console.log(count); }` | `$effect(() => { console.log(count); });` |
| `on:click={handler}` | `onclick={handler}` |
| `export let prop;` | `let { prop } = $props();` |

## Vue 3 — skladnia

```vue
<script setup>
import { ref, computed } from 'vue';

const count = ref(0);
const doubled = computed(() => count.value * 2);

function increment() {
  count.value++;
}
</script>

<template>
  <button @click="increment">Count: {{ count }} ({{ doubled }})</button>
</template>
```

## React 19 — skladnia

```jsx
import { useState } from 'react';
import { createRoot } from 'react-dom/client';

function App() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
  );
}

createRoot(document.getElementById("noe-app")).render(<App />);
```

## JS — skladnia

```javascript
// Czysty JS — kod uruchamiany bezposrednio w przegladarce
// Kontener: document.getElementById("noe-app")
(function() {
  const container = document.getElementById("noe-app");
  container.innerHTML = '<h1>Hello World</h1>';
})();
```

## Kontekst aplikacji (window.NoeAppContext)

Aplikacje osadzone w modulach maja dostep do danych uzytkownika:

```javascript
{
  user: {
    login: "jan.kowalski",
    email: "jan@example.com",
    name: "Jan Kowalski",
    role: "admin",              // admin, manager, user, guest
    team_ids: [1, 2],
    team_names: ["Sprzedaz", "IT"],
    group_ids: [5],
    group_names: ["Menedzerowie"],
    department_ids: [3],
    department_names: ["Dzial IT"]
  }
}
```

## Osadzanie w modulach (show_in_module)

Pole `show_in_module` osadza aplikacje w module systemu (menu boczne, breadcrumbs).

**Dostepne moduly:** `organize`, `mail`, `crm`, `kb`, `cms`, `connect`, `voip`, `form`, `insight`, `billing`, `drive`, `fiskator`, `commerce`, `account`

**Wstrzykiwanie na strony:** `show_in_module` moze zawierac sciezke do strony (np. `tasks/show`). Wtedy apka jest wstrzykiwana bezposrednio w widok.

| Wartosc | Zachowanie |
|---------|------------|
| `organize` | Aplikacja w menu bocznym modulu |
| `tasks/show` | Wstrzyknieta na stronie szczegulow zadania |
| `contacts/index` | Wstrzyknieta na liscie kontaktow |

## Akcje (integracja z Flow)

Akcje pozwalaja wywolywac Flow z frontendu:

```json
{
  "app": {
    "actions": [
      {
        "action": "get_data",
        "flow_code": "my-data-flow",
        "description": "Pobiera dane z API",
        "public": true
      }
    ]
  }
}
```

**Wywolanie z frontendu:**

```javascript
const response = await fetch('/noe/apps/APP_ID/action/get_data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ param1: "value1" })
});
const result = await response.json();
// result = { flow_process_id: "...", status: "pending"|"finished", data: {...} }
```

**Polling statusu (dla dlugich operacji):**

```javascript
async function waitForResult(appId, actionName, flowProcessId) {
  while (true) {
    const response = await fetch(`/noe/apps/${appId}/action/${actionName}/status?flow_process_id=${flowProcessId}`);
    const result = await response.json();
    if (result.status === "finished") return result.data;
    if (result.status === "error") throw new Error(result.error);
    await new Promise(r => setTimeout(r, 1000));
  }
}
```

## Bazy danych Noe (Noe::Db) — podsumowanie

Aplikacje moga korzystac z prostych baz danych Noe. Pełne API baz: [noe_db_api](https://intum.intum.pl/kb/intum-kb/noe/api/bazy-danych).

### Utworzenie bazy

```
POST /noe/dbs.json
{
  "db": {
    "name": "Todo",
    "code": "todo",
    "schema": {
      "fields": {
        "title": { "type": "string", "required": true },
        "done": { "type": "integer" }
      },
      "indexes": {
        "idx_str_1": "title",
        "idx_int_1": "done"
      }
    }
  }
}
```

### Dostepne indeksy

| Indeks | Typ |
|--------|-----|
| `idx_str_1`, `idx_str_2`, `idx_str_3` | string (max 255 znakow) |
| `idx_int_1`, `idx_int_2` | bigint |
| `idx_time_1`, `idx_time_2` | datetime |

### Typy pol w schema

`string`, `text`, `integer`, `number`, `boolean`, `datetime`, `date`, `time`

### Baza wielotabelowa

Jedna baza moze trzymac kilka tabel — zamiast `fields` na gornym poziomie podaj `tables`,
a kazda tabela dostaje wlasne `fields` i wlasne `indexed_fields` (pelny limit indeksow per tabela):

```
{
  "schema": {
    "tables": {
      "users":  { "fields": { "name": { "type": "string", "required": true }, "email": "string" },
                  "indexed_fields": ["name", "email"] },
      "orders": { "fields": { "order_number": "string", "user_id": "number" },
                  "indexed_fields": ["order_number", "user_id"] }
    }
  }
}
```

Rekordy takiej bazy przekazuja `table_name` (`{ "record": { "table_name": "orders", "data": {...} } }`),
a wyszukiwanie wymaga `?table_name=orders` — bez niego endpoint zwraca `400` z lista dostepnych tabel.
Frontendy appek korzystajace z metod publicznych: `table_name` dziala tylko w `search`, wiec do zapisu
do konkretnej tabeli uzyj prywatnego API rekordow albo osobnej bazy jednotabelowej.
Szczegoly: [noe_db_api](https://intum.intum.pl/kb/intum-kb/noe/api/bazy-danych).

### Operacje na rekordach (prywatne — wymaga autoryzacji)

```javascript
const DB = "todo";

// Lista
const records = await fetch(`/noe/dbs/${DB}/records.json`).then(r => r.json());

// Utworzenie
await fetch(`/noe/dbs/${DB}/records.json`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ record: { data: { title: "Zadanie", done: 0 } } })
});

// Aktualizacja
await fetch(`/noe/dbs/${DB}/records/${id}.json`, {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ record: { data: { done: 1 } } })
});

// Usuniecie
await fetch(`/noe/dbs/${DB}/records/${id}.json`, { method: 'DELETE' });

// Wyszukiwanie
const results = await fetch(`/noe/dbs/${DB}/search.json?q=zadanie&sort=-created_at`).then(r => r.json());
```

### Publiczne metody (bez autoryzacji)

Pole `public_methods` na bazie udostepnia operacje bez logowania:

```json
{
  "public_methods": {
    "list": { "scope_by": "user_ip" },
    "create": {
      "allowed_fields": ["title", "done"],
      "required_fields": ["title"],
      "default_values": { "done": 0 },
      "auto_fields": { "user_ip": "{ip}" }
    },
    "update": { "allowed_fields": ["title", "done"], "scope_by": "user_ip" },
    "delete": { "scope_by": "user_ip" }
  }
}
```

**URL publiczny:** `POST /noe/db/:db_code/:method_name`

**Zmienne w auto_fields:** `{ip}` (adres IP), `{cookie_key}` (unikalny token per przegladarka), `{user_id}` / `{user_name}` (zalogowany uzytkownik; puste bez sesji)

### Roznica: Records API vs Search API

| API | Struktura danych |
|-----|------------------|
| Records (`/records.json`) | Zagniezdzona: `record.data.field` |
| Search (`/search.json`) | Plaska: `record.field` |

## Kompletny przyklad — Kalkulator (Svelte 5)

```
POST /noe/apps.jsonContent-Type: application/json
```

```json
{
  "app": {
    "name": "Kalkulator",
    "url_code": "kalkulator",
    "description": "Prosty kalkulator",
    "app_engine": "svelte",
    "css_engine": "tailwind_all",
    "public": true,
    "source_code": "<script>\n  let display = $state(\"0\");\n  let firstOperand = $state(null);\n  let operator = $state(null);\n  let waitingForSecondOperand = $state(false);\n\n  function inputDigit(digit) {\n    if (waitingForSecondOperand) {\n      display = String(digit);\n      waitingForSecondOperand = false;\n    } else {\n      display = display === \"0\" ? String(digit) : display + digit;\n    }\n  }\n\n  function inputDecimal() {\n    if (waitingForSecondOperand) { display = \"0.\"; waitingForSecondOperand = false; return; }\n    if (!display.includes(\".\")) display = display + \".\";\n  }\n\n  function clear() {\n    display = \"0\"; firstOperand = null; operator = null; waitingForSecondOperand = false;\n  }\n\n  function toggleSign() { display = String(parseFloat(display) * -1); }\n  function inputPercent() { display = String(parseFloat(display) / 100); }\n\n  function calculate(first, op, second) {\n    switch (op) {\n      case \"+\": return first + second;\n      case \"-\": return first - second;\n      case \"*\": return first * second;\n      case \"/\": return second !== 0 ? first / second : \"Error\";\n      default: return second;\n    }\n  }\n\n  function handleOperator(nextOp) {\n    const val = parseFloat(display);\n    if (operator && !waitingForSecondOperand) {\n      const result = calculate(firstOperand, operator, val);\n      display = String(result); firstOperand = result;\n    } else { firstOperand = val; }\n    waitingForSecondOperand = true; operator = nextOp;\n  }\n\n  function handleEquals() {\n    if (operator == null || waitingForSecondOperand) return;\n    const result = calculate(firstOperand, operator, parseFloat(display));\n    display = String(result); firstOperand = null; operator = null; waitingForSecondOperand = false;\n  }\n</script>\n\n<div class=\"min-h-screen bg-gray-900 flex items-center justify-center p-4\">\n  <div class=\"w-full max-w-xs bg-gray-800 rounded-2xl shadow-2xl overflow-hidden\">\n    <div class=\"p-6 pb-2\">\n      <div class=\"text-right text-white text-5xl font-light tracking-tight truncate min-h-[60px] flex items-end justify-end\">\n        {display.length > 9 ? parseFloat(display).toExponential(4) : display}\n      </div>\n    </div>\n    <div class=\"grid grid-cols-4 gap-[1px] p-3\">\n      <button onclick={clear} class=\"bg-gray-500 hover:bg-gray-400 text-black rounded-full h-16 text-2xl font-medium\">C</button>\n      <button onclick={toggleSign} class=\"bg-gray-500 hover:bg-gray-400 text-black rounded-full h-16 text-2xl font-medium\">+/-</button>\n      <button onclick={inputPercent} class=\"bg-gray-500 hover:bg-gray-400 text-black rounded-full h-16 text-2xl font-medium\">%</button>\n      <button onclick={() => handleOperator(\"/\")} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">/</button>\n      {#each [7,8,9] as d}<button onclick={() => inputDigit(d)} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium\">{d}</button>{/each}\n      <button onclick={() => handleOperator(\"*\")} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">x</button>\n      {#each [4,5,6] as d}<button onclick={() => inputDigit(d)} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium\">{d}</button>{/each}\n      <button onclick={() => handleOperator(\"-\")} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">-</button>\n      {#each [1,2,3] as d}<button onclick={() => inputDigit(d)} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium\">{d}</button>{/each}\n      <button onclick={() => handleOperator(\"+\")} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">+</button>\n      <button onclick={() => inputDigit(0)} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium col-span-2 text-left pl-6\">0</button>\n      <button onclick={inputDecimal} class=\"bg-gray-700 hover:bg-gray-600 text-white rounded-full h-16 text-2xl font-medium\">.</button>\n      <button onclick={handleEquals} class=\"bg-amber-500 hover:bg-amber-400 text-white rounded-full h-16 text-2xl font-medium\">=</button>\n    </div>\n  </div>\n</div>"
  }
}
```

**Wynik:** Aplikacja dostepna pod `https://{host}/a/kalkulator`

## Kompletny przyklad — Todo z baza (Svelte 5)

### Krok 1: Utworz baze

```
POST /noe/dbs.json
{
  "db": {
    "name": "Todo",
    "code": "todo",
    "schema": {
      "fields": {
        "title": { "type": "string", "required": true },
        "done": { "type": "integer" },
        "user_ip": { "type": "string" }
      },
      "indexes": { "idx_str_1": "title", "idx_str_2": "user_ip", "idx_int_1": "done" }
    },
    "public_methods": {
      "list": { "scope_by": "user_ip" },
      "create": {
        "allowed_fields": ["title", "done"],
        "required_fields": ["title"],
        "default_values": { "done": 0 },
        "auto_fields": { "user_ip": "{ip}" }
      },
      "update": { "allowed_fields": ["title", "done"], "scope_by": "user_ip" },
      "delete": { "scope_by": "user_ip" }
    }
  }
}
```

### Krok 2: Utworz aplikacje

```
POST /noe/apps.json
{
  "app": {
    "name": "Todo",
    "url_code": "todo",
    "app_engine": "svelte",
    "css_engine": "tailwind_all",
    "public": true,
    "db_code": "todo",
    "source_code": "<script>\n  let tasks = $state([]);\n  let newTitle = $state(\"\");\n  const DB = \"todo\";\n\n  async function load() {\n    const res = await fetch(`/noe/db/${DB}/list`, { method: \"POST\" });\n    tasks = await res.json();\n  }\n\n  async function add() {\n    if (!newTitle.trim()) return;\n    await fetch(`/noe/db/${DB}/create`, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ data: { title: newTitle } })\n    });\n    newTitle = \"\"; await load();\n  }\n\n  async function toggle(task) {\n    await fetch(`/noe/db/${DB}/update`, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ id: task.id, data: { done: task.done ? 0 : 1 } })\n    });\n    await load();\n  }\n\n  async function remove(id) {\n    await fetch(`/noe/db/${DB}/delete`, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ id })\n    });\n    await load();\n  }\n\n  $effect(() => { load(); });\n</script>\n\n<div class=\"max-w-md mx-auto p-6\">\n  <h1 class=\"text-2xl font-bold mb-4\">Todo</h1>\n  <div class=\"flex gap-2 mb-4\">\n    <input bind:value={newTitle} placeholder=\"Nowe zadanie...\" class=\"border p-2 rounded flex-1\" onkeydown={(e) => e.key === 'Enter' && add()} />\n    <button onclick={add} class=\"bg-blue-500 text-white px-4 py-2 rounded\">Dodaj</button>\n  </div>\n  {#each tasks as task}\n    <div class=\"flex items-center gap-2 p-2 border-b\">\n      <input type=\"checkbox\" checked={task.done === 1} onchange={() => toggle(task)} />\n      <span class={task.done === 1 ? 'line-through text-gray-400' : ''}>{task.title}</span>\n      <button onclick={() => remove(task.id)} class=\"ml-auto text-red-500\">x</button>\n    </div>\n  {/each}\n</div>"
  }
}
```

## Wskazowki

- **Znaki specjalne w JSON** — przy wysylaniu przez curl unikaj em dash i innych znakow UTF-8. Uzywaj ASCII lub przesylaj z pliku (`-d @plik.json`)
- **Duzy source_code** — przygotuj JSON w pliku i uzyj `curl -d @payload.json`
- **Bledy kompilacji** — przy bledzie w source_code serwer zwroci 422 z opisem bledu w polu `source_code`
- **url_code** — musi byc unikalny na koncie. Serwer automatycznie oczyci go z niedozwolonych znakow
- **Publiczna apka** — ustaw `"public": true` aby byla dostepna bez logowania
- **Tailwind** — uzywaj `tailwind_all` (CDN) dla prostoty lub `tailwind` dla mniejszego rozmiaru
- **Testowanie** — po utworzeniu odwiedz `https://{host}/a/{url_code}` w przegladarce

## Powiązane

- [noe_db_api](https://intum.intum.pl/kb/intum-kb/noe/api/bazy-danych) — pełne API baz danych Noe (schema, rekordy, metody publiczne, auto_fields)
- [noe_app_example_weather](https://intum.intum.pl/kb/intum-kb/noe/api/przyklad-aplikacja-pogoda) — kompletny przykład krok po kroku: apka z custom connectorem, custom flow i cronem (pogoda)
- [common_api](https://app.intum.pl/noe/prompt/common_api.md) — wspólne zasady API (format, autoryzacja, odpowiedzi)