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

# [Przykład: aplikacja Pogoda krok po kroku](https://intum.pl/pomoc/noe/api/przyklad-aplikacja-pogoda.md)

Kompletny, sprawdzony przepis na aplikację wg wzorca "pobieraj z zewnętrznego API (ręcznie
i cyklicznie), zapisuj każde pobranie do bazy, pokazuj historię w apce". Składniki: custom
connector (Open-Meteo), baza Noe, konektor zapisu, **jeden custom flow wspólny dla crona
i przycisku w apce**, cykliczna akcja konta (cron) i aplikacja Noe (Svelte) z historią.
Każdy rekord ma źródło: `manual` (kto i z jakiego IP kliknął) albo `cron`.

**Autoryzacja:** `Authorization: Bearer TOKEN`
**Content-Type:** `application/json; charset=utf-8`

## Krok 1 - custom connector do zewnętrznego API

```
POST /connect/connectors.json
```

```json
{
  "connector": {
    "name": "Open-Meteo API (example1)",
    "code": "open-meteo-example1",
    "kind": "noe/custom_connector",
    "url": "https://api.open-meteo.com",
    "active": true,
    "methods": [
      {
        "method": "get_weather",
        "object_in": "json_object",
        "object_out": "json_object",
        "http_method": "GET",
        "path": "/v1/forecast?latitude={latitude}&longitude={longitude}&current_weather=true",
        "description": "Pobiera aktualną pogodę dla współrzędnych"
      }
    ]
  }
}
```

Pułapki:

- `methods` to **top-level pole konektora** (kolumna jsonb), NIE `fields.methods`
- bazowy `url` jest normalizowany do samego hosta - **ścieżkę (np. `/v1`) podawaj w `path` metody**
- klucz API zewnętrznego serwisu (jeśli wymagany) idzie w `secret_token` (wysyłany jako Bearer)

## Krok 2 - baza Noe na historię pobrań

```
POST /noe/dbs.json
```

```json
{
  "db": {
    "name": "Pogoda - historia (example1)",
    "code": "weather-example1",
    "schema": {
      "fields": {
        "temperature": { "type": "number" },
        "windspeed": { "type": "number" },
        "alert": { "type": "string" },
        "source": { "type": "string" },
        "fetched_by": { "type": "string" },
        "ip": { "type": "string" }
      },
      "indexes": { "idx_str_1": "source" }
    },
    "public_methods": { "list": true }
  }
}
```

- `list: true` - frontend czyta historię bez autoryzacji: `POST /noe/db/weather-example1/list`
  (rekordy `{id, data, created_at}`, najnowsze pierwsze; czas pobrania = `created_at`)
- zapisy robi wyłącznie flow (niżej), więc publiczne `create` nie jest wystawiane

## Krok 3 - konektor zapisu do bazy (prywatne API + token)

Program flow nie ma wbudowanej akcji "zapisz do bazy Noe" - zapis to krok `connector` na endpoint
bazy. Utwórz token API (`/account/api_tokens` -> "Nowy") i wstaw go w `secret_token`:

```json
{
  "connector": {
    "name": "Zapis pogody do bazy Noe (example1)",
    "code": "weather-db-example1",
    "kind": "noe/custom_connector",
    "url": "http://intum1.intum.test",
    "secret_token": "TOKEN_API",
    "active": true,
    "methods": [
      { "method": "save_weather", "object_in": "json_object", "object_out": "json_object",
        "http_method": "POST", "path": "/noe/dbs/weather-example1/records.json",
        "description": "Zapisuje rekord pogody przez prywatne API rekordów (Bearer = secret_token)" }
    ]
  }
}
```

`url` = adres konta (na produkcji domena konta, na dev np. `http://intum1.intum.test`).
Endpoint prywatnego API czyta z body `record.data` - nadmiarowe klucze obiektu ignoruje.

## Krok 4 - JEDEN custom flow dla crona i przycisku

```
POST /connect/flows.json
```

```json
{
  "flow": {
    "name": "Get Weather (example1)",
    "code": "get-weather-example1",
    "kind": "noe/custom_flow",
    "active": true,
    "fields": {
      "env": { "latitude": "52.23", "longitude": "21.01" },
      "program": [
        {
          "action": "mutation",
          "in": "json_object",
          "mutation": {
            "json_object.latitude": "{env.latitude}",
            "json_object.longitude": "{env.longitude}"
          },
          "out": "json_object"
        },
        {
          "action": "connector",
          "in": "json_object",
          "kind": "noe/custom_connector",
          "code": "open-meteo-example1",
          "method": "get_weather",
          "result_key": "weather",
          "out": "json_object"
        },
        {
          "action": "mutation",
          "in": "json_object",
          "mutation": {
            "json_object.record.data.temperature": "{json_object.weather.current_weather.temperature}",
            "json_object.record.data.windspeed": "{json_object.weather.current_weather.windspeed}"
          },
          "out": "json_object"
        },
        {
          "in": "json_object",
          "out": "json_object",
          "if": {
            "{json_object.record.data.temperature} > 30": {
              "then": [
                { "action": "mutation", "in": "json_object",
                  "mutation": { "json_object.record.data.alert": "'true'" }, "out": "json_object" }
              ],
              "else": [
                { "action": "mutation", "in": "json_object",
                  "mutation": { "json_object.record.data.alert": "'false'" }, "out": "json_object" }
              ]
            }
          }
        },
        {
          "in": "json_object",
          "out": "json_object",
          "if": {
            "{json_object.kind} == 'cron'": {
              "then": [
                { "action": "mutation", "in": "json_object",
                  "mutation": {
                    "json_object.record.data.source": "'cron'",
                    "json_object.record.data.fetched_by": "'cron'",
                    "json_object.record.data.ip": "'-'"
                  }, "out": "json_object" }
              ],
              "else": [
                { "action": "mutation", "in": "json_object",
                  "mutation": {
                    "json_object.record.data.source": "'manual'",
                    "json_object.record.data.fetched_by": "{json_object._user_name}",
                    "json_object.record.data.ip": "{json_object._ip}"
                  }, "out": "json_object" }
              ]
            }
          }
        },
        {
          "action": "connector",
          "in": "json_object",
          "kind": "noe/custom_connector",
          "code": "weather-db-example1",
          "method": "save_weather",
          "out": "json_object"
        }
      ]
    }
  }
}
```

Kluczowe elementy:

- **`result_key: "weather"`** na kroku Open-Meteo - bez tego odpowiedź API ZASTĄPIŁABY cały obiekt
  i kontekst wywołania (`kind`, `_user_name`, `_ip`) przepadłby przed krokiem rozpoznającym źródło
- **skąd flow wie, kto go odpalił**: cron przekazuje na wejściu `{ "kind": "cron", "cron_id": ... }`,
  a akcja aplikacji Noe dokłada `_action`, `_app_id` i kontekst wywołującego: `_user_name`,
  `_user_id`, `_ip` - warunek `{json_object.kind} == 'cron'` rozróżnia źródła
- **współrzędne w `fields.env`** - program czyta je jako `{env.latitude}`/`{env.longitude}` przy
  każdym uruchomieniu (cron i akcja); zmiana lokalizacji = edycja `fields.env` flow, bez dotykania
  programu (w apce robi to sekcja "Lokalizacja" PATCH-em na flow)
- payload zapisu budowany pod zagnieżdżonym kluczem **`record.data`** (mutation umie tworzyć
  zagnieżdżone ścieżki) - dokładnie w formacie prywatnego API rekordów
- konektory muszą istnieć PRZED flow (walidacja programu sprawdza metody)
- test (dev): `POST /connect/flows/get-weather-example1/start.json?perform_now=1` z body `{}`

## Krok 5 - cykliczna akcja konta (cron)

```
POST /account/crons.json
```

```json
{ "cron": { "kind": "flow", "target_code": "get-weather-example1", "schedule": "0 6 */3 * *", "name": "Pogoda - odświeżanie", "active": true } }
```

Ręczne odpalenie: `POST /account/crons/:id/run.json` albo "Uruchom teraz" w `/account/crons`.

## Krok 6 - aplikacja Noe (Svelte) z historią pobrań

```
POST /noe/apps.json
```

```json
{
  "app": {
    "name": "Pogoda (example1)",
    "url_code": "pogoda-example1",
    "description": "Przykładowa apka: custom connector + custom flow + cron + baza Noe z historią pobrań",
    "app_engine": "svelte",
    "css_engine": "tailwind_all",
    "public": true,
    "db_code": "weather-example1",
    "actions": [
      { "action": "get_weather", "flow_code": "get-weather-example1", "description": "Pobiera pogodę i zapisuje do bazy", "public": true }
    ],
    "source_code": "...kod Svelte (opis niżej)..."
  }
}
```

Frontend (Svelte 5): karta z gradientem pokazuje najnowszy pomiar (duża temperatura, wiatr,
badge alertu), przycisk "Pobierz pogodę", a pod nim **historia pobrań** - każdy wiersz z datą
i czasem, temperaturą, wiatrem i badge źródła (fioletowy `cron`, niebieski `ręcznie · kto (ip)`).

Pod historią sekcja **Lokalizacja** (widoczna dla userów z dostępem do flows): dwa pola lat/lng,
przycisk "Użyj mojej lokalizacji" (`navigator.geolocation` - wymaga HTTPS, na dev po HTTP
przeglądarka może odmówić) oraz "Zapisz" - GET flow, merge `fields.env` i PATCH
`/connect/flows/<code>.json` (z nagłówkiem `X-CSRF-Token` z meta). Domyślnie Warszawa;
zmiana obowiązuje od następnego pobrania.

Przepływ po kliknięciu (frontend niczego nie zapisuje - zapis robi flow):

1. `POST /noe/apps/APP_ID/action/get_weather.json` (POST akcji **musi mieć `.json`**)
2. przy `status: "pending"` polling `GET /noe/apps/APP_ID/action/get_weather/status?flow_process_id=...`
3. po zakończeniu przeładowuje listę `POST /noe/db/weather-example1/list`

Po utworzeniu appki:

- podmień placeholder `APP_ID` w kodzie: `PATCH /noe/apps/:id/replace.json`
  `{ "field": "source_code", "old_string": "APP_ID", "new_string": "PRAWDZIWE_ID", "replace_all": true }`
- powiąż bazę z appką: `PATCH /noe/dbs/weather-example1.json` z `{ "db": { "app_id": "PRAWDZIWE_ID" } }` -
  baza będzie widoczna w "Powiązaniach" na show appki

Pułapka przy wgrywaniu kodu przez curl: NIE wklejaj source_code w heredocu bez cudzysłowów -
shell zje backticki i `${...}` z template literals oraz runy `$state`/`$derived`. Zapisz kod do
pliku i buduj JSON np. ruby/pythonem z odczytu pliku.

## Krok 7 - weryfikacja

- **lokalnie musi działać worker SolidQueue** (`./solid_queue_local_sugester2`) - akcje appki i cron
  wykonują flow asynchronicznie; bez workera przycisk wisi na "Pobieram...", a joby czekają w kolejce
- ręczne pobranie: klik w appce -> rekord `source: manual` z nazwą użytkownika i jego IP
- cron: `POST /account/crons/:id/run.json` (albo czekaj na harmonogram) -> rekord `source: cron`
- historia: `POST /noe/db/weather-example1/list`, widok appki `/noe/pogoda-example1`
  albo rekordy bazy w `/noe/dbs/weather-example1/records`
- po zmianie kodu appki zrób w przeglądarce twarde odświeżenie (stary bundle potrafi zostać w cache)
- `perform_now=1` wykonuje flow synchronicznie w środku requestu - krok zapisu strzela wtedy HTTP
  do tego samego serwera dev i może dostać timeout (408), mimo że rekord się zapisze; normalna
  ścieżka (worker) nie ma tego problemu
- **publiczny dostęp** (`public: true`, bez logowania) działa dopiero na koncie zweryfikowanym -
  na niezweryfikowanym system blokuje publiczne appki (403, aktywność `noe_app_blocked_unverified`)
- powiązania (appka -> baza/flow -> cron/konektory) widać na show appki w `/noe/apps` i na `/connect/flows/:id`

## Powiązane

- [noe_api](https://intum.intum.pl/kb/intum-kb/noe/api/aplikacje) - pełne API aplikacji Noe
- [noe_db_api](https://intum.intum.pl/kb/intum-kb/noe/api/bazy-danych) - API baz danych Noe (schema, rekordy, metody publiczne)
- [connector_api](https://app.intum.pl/noe/prompt/connector_api.md) - konektory, w tym noe/custom_connector
- [connector_flow_api](https://app.intum.pl/noe/prompt/connector_flow_api.md) - program flow (connector/mutation/if/case, result_key)
- [account_crons_api](https://app.intum.pl/noe/prompt/account_crons_api.md) - cykliczne akcje konta
- [common_api](https://app.intum.pl/noe/prompt/common_api.md) - wspólne zasady API