# Moduły portalu redAi: dokumentacja dla programistów

Wersja dokumentu: 25.09.2026, portal od 1.3.20. Strona z nawigacją: https://redai.pl/dev/

Ten dokument opisuje, jak zbudować własny moduł portalu redAi, co moduł może, a czego nie wolno mu robić, jak go wysłać do katalogu redAi i jak trafia na instancje. Jest napisany dla ludzi i dla czatów AI: wklejcie ten adres do swojego asystenta kodowania, a dostanie komplet reguł w jednym pliku.

## Czym jest moduł

Moduł to jeden folder `narzedzia/<klucz>/` w portalu instancji. Portal wykrywa go sam: wystarczy, że folder leży w `narzedzia/` i ma poprawny manifest `narzedzie.php`. Moduł pojawia się wtedy w **Administracja › Moduły** jako narzędzie do włączenia.

- Nie dopisujecie nic w `routes/`, `app/`, `config/`, `bootstrap/` ani w innych plikach portalu. Moduł rozmawia z portalem tylko przez manifest, haki klasy `Narzedzie` i wspólne elementy jądra opisane niżej.
- Moduł może być samodzielnym narzędziem (własne ekrany, tabele, rekordy) albo tylko rozszerzać portal: dodawać skille czatu, tekst do promptu czatu, zadania cykliczne, krótkie adresy publiczne.
- Portal to aplikacja Laravel (PHP 8.3). Moduł pisze się jak kawałek aplikacji Laravela: kontrolery, widoki Blade, migracje, modele Eloquent.
- Wzorcowe moduły redAi, na których możecie się wzorować: `strony` (rekordy, krótkie adresy, menu), `formularze` (publiczne wysyłki, mail, CSV), `poczta` (blok promptu czatu, komenda artisan), `contract_generator` (konfig, szablony w folderze).

Katalog modułów redAi to miejsce, do którego wysyłacie paczkę z modułem. Po naszym akcepcie moduł widzą instancje: wszystkie (moduł publiczny) albo tylko te, które wskażecie (moduł prywatny).

## Szybki start

Minimalny działający moduł „Notatki zespołu”: lista notatek i formularz dodawania. Pięć plików.

```
narzedzia/notatki/
  narzedzie.php
  trasy.php
  src/NotatkiController.php
  widoki/index.blade.php
  migracje/2026_09_25_120000_create_notatki_table.php
```

`narzedzia/notatki/narzedzie.php`:

```php
<?php

return [
    'nazwa' => 'Notatki zespołu',
    'wersja' => '1.0.0',
    'zmiany' => [
        '1.0.0' => ['Pierwsza wersja: lista notatek i dodawanie nowej.'],
    ],
    'wymaga_portalu' => '1.3.20',
    'opis' => 'Krótkie notatki widoczne dla całego zespołu.',
    'ikona' => '🗒️',
    'kategoria' => 'dokumenty',
    'domyslnie' => false,
    'trasy' => ['prefiks' => 'narzedzia/notatki', 'nazwy' => 'notatki.'],
];
```

`narzedzia/notatki/trasy.php`:

```php
<?php

use Illuminate\Support\Facades\Route;
use Narzedzia\Notatki\NotatkiController;

Route::get('/', [NotatkiController::class, 'index'])->name('index');   // notatki.index = ekran startowy
Route::post('/', [NotatkiController::class, 'store'])->name('store');
```

`narzedzia/notatki/src/NotatkiController.php`:

```php
<?php

namespace Narzedzia\Notatki;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class NotatkiController extends Controller
{
    public function index()
    {
        $notatki = DB::table('notatki')->latest()->limit(50)->get();

        return view('notatki::index', compact('notatki'));
    }

    public function store(Request $request)
    {
        $dane = $request->validate(['tresc' => 'required|string|max:2000']);
        DB::table('notatki')->insert([
            'user_id' => $request->user()->id,
            'tresc' => $dane['tresc'],
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return redirect()->route('notatki.index')->with('status', 'Notatka zapisana.');
    }
}
```

`narzedzia/notatki/widoki/index.blade.php`:

```blade
@extends('layouts.app', ['title' => 'Notatki zespołu'])
@section('header', 'Narzędzia')

@section('content')
<div class="ui-page ui-page--szeroka ui-stack">
    @include('tools._naglowek', ['nazwa' => 'Notatki zespołu', 'klucz' => 'notatki'])

    <form method="POST" action="{{ route('notatki.store') }}" class="ui-card ui-card--flat ui-stack">
        @csrf
        <div class="ui-field">
            <label class="ui-label" for="n-tresc">Nowa notatka</label>
            <textarea id="n-tresc" name="tresc" rows="3" class="ui-textarea" maxlength="2000" required></textarea>
        </div>
        <div><button class="ui-btn ui-btn--primary ui-btn--sm">Zapisz</button></div>
    </form>

    <div class="ui-card ui-card--flat">
        @forelse($notatki as $n)
            <p>{{ $n->tresc }}</p>
        @empty
            <div class="ui-empty"><b>Jeszcze nie ma notatek</b>Dodajcie pierwszą powyżej.</div>
        @endforelse
    </div>
</div>
@endsection
```

`narzedzia/notatki/migracje/2026_09_25_120000_create_notatki_table.php`:

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('notatki', function (Blueprint $t) {
            $t->id();
            $t->foreignId('user_id')->nullable()->index();
            $t->text('tresc');
            $t->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('notatki');
    }
};
```

Skopiujcie folder do `narzedzia/` na instancji testowej, wejdźcie w **Administracja › Moduły**, otwórzcie „Notatki zespołu” i włączcie. Włączenie samo uruchomi migrację. Ekran jest pod `/narzedzia/notatki`.

## Manifest `narzedzie.php`

Manifest to plik PHP, który zwraca tablicę (`return [...]`). To jedyny plik obowiązkowy.

**Manifest ma być samymi stałymi.** Wolno: napisy, liczby, `true`, `false`, `null`, tablice oraz `Nazwa\Klasy::class`. Nie wolno: wywołań funkcji (`__()`, `env()`, `config()`), zmiennych, stałych typu `__DIR__`, konkatenacji z wyrażeniami. redai.pl nie wykonuje kodu z Waszej paczki: manifest czytamy parserem tokenów PHP. Manifest z wyrażeniem zostanie odrzucony przy wysyłce.

| Pole | Typ | Obowiązkowe | Co robi |
|---|---|---|---|
| `nazwa` | napis | tak | Nazwa w menu, na liście narzędzi i w Modułach. |
| `wersja` | napis `x.y.z` | tak (w katalogu) | Numer wersji modułu (semver). Każda wysyłka musi mieć wyższy numer niż poprzednia. |
| `zmiany` | tablica | zalecane | Lista zmian: klucz = wersja, wartość = lista zdań dla użytkownika. Najnowsza pierwsza. |
| `wymaga_portalu` | napis `x.y.z` | nie | Minimalna wersja portalu instancji. Starszy portal nie dostanie modułu w katalogu. |
| `opis` | napis | tak | Jedno zdanie: co z tego macie. `{host}` zamienia się na domenę instancji. |
| `ikona` | emoji | nie | Ikona na liście narzędzi. Domyślnie 🔧. |
| `ikona_svg` | napis | nie | Wnętrze SVG 24x24 (obrys, bez znacznika `<svg>`) do menu po lewej. |
| `kategoria` | napis | nie | Grupa w Modułach: `dokumenty`, `komunikacja`, `rejestry`. Domyślnie `dokumenty`. |
| `kolejnosc` | liczba | nie | Pozycja na liście, mniejsza = wyżej. Domyślnie 100. |
| `domyslnie` | bool | nie | Czy moduł jest włączony od razu. Dla modułów z katalogu zostawcie `false`. |
| `trasy.prefiks` | napis | nie | Początek adresów. Domyślnie `narzedzia/<klucz z myślnikami>`. |
| `trasy.nazwy` | napis | nie | Przedrostek nazw tras. Domyślnie `<klucz>.` |
| `trasy.start` | napis | nie | Nazwa trasy ekranu startowego bez przedrostka. Domyślnie `index`. |
| `konfig` | napis | nie | Klucz `config()` dla `konfig.php`. Domyślnie `<klucz>`. |
| `komendy` | lista klas | nie | Komendy artisan z `src/`. |
| `klasa` | klasa | nie | Własna klasa haków. Domyślnie `src/Narzedzie.php`. |
| `aliasy` | tablica | nie | Stara klasa => nowa. Potrzebne tylko przy przenoszeniu kodu, zwykle puste. |
| `opis_pozycji` | tablica | zalecane | Karta modułu w Modułach i ekran przed włączeniem (szczegóły niżej). |

`opis_pozycji` ma klucze:

| Klucz | Co pokazuje |
|---|---|
| `daje` | Lista „Co Wam to daje”, 2-4 zdania. |
| `przyklady` | Lista przykładów użycia. |
| `przyklady_tytul` | Nagłówek przykładów, np. „Co zrobicie w narzędziu”. |
| `szczegoly` | Szczegóły działania. Tu wpiszcie też każdy zewnętrzny adres, z którym moduł się łączy. |
| `kroki` | Kroki startu, np. „Włączcie narzędzie przyciskiem poniżej.” |
| `wlacz` | Napis na przycisku włączenia. |

Pełny przykład z wersją i listą zmian:

```php
<?php

return [
    'nazwa' => 'Faktury kosztowe',
    'wersja' => '1.2.0',
    'zmiany' => [
        '1.2.0' => ['Import faktur z pliku CSV.', 'Szybsza lista przy tysiącach faktur.'],
        '1.1.0' => ['Filtr po kontrahencie.'],
        '1.0.0' => ['Pierwsza wersja.'],
    ],
    'wymaga_portalu' => '1.3.20',
    'opis' => 'Faktury kosztowe w jednym miejscu, z wyszukiwaniem po kontrahencie.',
    'ikona' => '🧾',
    'ikona_svg' => '<path d="M6 3h12v18l-3-2-3 2-3-2-3 2z"/><path d="M9 8h6M9 12h6"/>',
    'kategoria' => 'dokumenty',
    'kolejnosc' => 50,
    'domyslnie' => false,
    'trasy' => [
        'prefiks' => 'narzedzia/faktury-kosztowe',
        'nazwy' => 'faktury_kosztowe.',
        'start' => 'index',
    ],
    'konfig' => 'faktury_kosztowe',
    'komendy' => [Narzedzia\FakturyKosztowe\Importuj::class],
    'opis_pozycji' => [
        'daje' => ['Wszystkie faktury kosztowe w jednej liście.', 'Wyszukiwanie po kontrahencie i kwocie.'],
        'przyklady' => ['Wgrajcie CSV z banku i sprawdźcie, czego brakuje.'],
        'przyklady_tytul' => 'Co zrobicie w narzędziu',
        'szczegoly' => ['Moduł nie łączy się z żadnym zewnętrznym adresem.'],
        'kroki' => ['Włączcie narzędzie przyciskiem poniżej.', 'Kliknijcie „Nowa faktura”.'],
        'wlacz' => 'Włączcie narzędzie',
    ],
];
```

Teksty dla użytkownika piszcie jak w całym portalu: 2. osoba liczby mnogiej („Wy/Wasze”), bez em-dashów, bez żargonu i nazw plików.

## Struktura folderu

```
narzedzia/<klucz>/
  narzedzie.php          manifest (return [...]), JEDYNY plik obowiązkowy
  src/                   klasy PHP, przestrzeń Narzedzia\<Pascal>\ (ładuje je portal, bez composera)
  src/Narzedzie.php      opcjonalnie: haki, klasa Narzedzia\<Pascal>\Narzedzie extends App\Narzedzia\Narzedzie
  trasy.php              trasy za logowaniem
  trasy-publiczne.php    opcjonalnie: trasy bez logowania
  widoki/                widoki Blade: view('<klucz>::lista'), @include('<klucz>::_czesc')
  migracje/              migracje Laravela (tabele modułu)
  konfig.php             opcjonalnie: config('<klucz>.*')
  zasoby/                pliki statyczne (css, js, obrazy, fonty) pod /narzedzia-zasoby/<klucz>/...
  zasoby/zrzuty/         zrzuty ekranu do karty w Modułach (01.webp, 02.webp...)
  skille/<nazwa>/SKILL.md  opcjonalnie: skille czatu
```

- **Klucz** = nazwa folderu: małe litery, cyfry i podkreślnik, 2-41 znaków, zaczyna się od litery. Wzorzec: `^[a-z][a-z0-9_]{1,40}$`. Przykład: `faktury_kosztowe`.
- **Klucz jest globalnie unikalny w katalogu** i należy do autora, który pierwszy go wysłał. Wybierzcie nazwę, która nie zderzy się z innymi, np. z przedrostkiem firmy (`acme_faktury`).
- **Przestrzeń PHP** to klucz w PascalCase: `faktury_kosztowe` → `Narzedzia\FakturyKosztowe\`. Plik `src/Dane/Import.php` = klasa `Narzedzia\FakturyKosztowe\Dane\Import`. Autoloader portalu ładuje tylko tę przestrzeń.
- Foldery i pliki zaczynające się od kropki są zarezerwowane. Portal zapisuje w folderze modułu plik `.zrodlo.json` (skąd moduł przyszedł), nie wkładajcie go do paczki.
- Folder z błędem w manifeście jest pomijany, a błąd trafia do logu portalu. Portal działa dalej.
- **Folder modułu jest przy aktualizacji podmieniany w całości.** Nie trzymajcie w nim danych użytkowników. Dane idą do bazy albo do `storage/app/<klucz>/`.

## Trasy i bramki dostępu

`trasy.php` to zwykły plik tras Laravela. Portal sam dokłada do każdej trasy:

- logowanie,
- bramkę `module:tools` i `tool:<klucz>`: moduł musi być włączony na instancji, a osoba musi mieć do niego dostęp. Kto nie ma dostępu, trafia na listę narzędzi z komunikatem,
- prefiks adresu (`trasy.prefiks`) i przedrostek nazw (`trasy.nazwy`).

```php
<?php

use Illuminate\Support\Facades\Route;
use Narzedzia\FakturyKosztowe\FakturyController;

Route::get('/', [FakturyController::class, 'index'])->name('index');      // faktury_kosztowe.index = ekran startowy
Route::post('/', [FakturyController::class, 'store'])->name('store');
Route::get('/{faktura}', [FakturyController::class, 'show'])->name('show');
```

- Kontrolery trzymacie w `src/` (`extends App\Http\Controllers\Controller`).
- Bez trasy startowej (`<nazwy><start>`, domyślnie `<klucz>.index`) moduł nie pokaże się na liście narzędzi.
- W kontrolerze sprawdzacie tylko własne reguły rekordów, np. `abort_unless($rekord->mozeEdytowac($user), 403)`. Włączenia i dostępu osób nie sprawdzacie, robi to bramka.
- `\App\Support\Narzedzia::zarzadza($user)` mówi, czy osoba zarządza narzędziami (zwykle administrator, widzi wszystkie rekordy).

**Kto ma dostęp.** Administrator instancji w **Administracja › Moduły › <moduł>** włącza moduł i wybiera, kto może z niego korzystać: wszyscy albo wybrane osoby i grupy. Każda osoba z dostępem może jeszcze przypiąć moduł do swojego menu po lewej.

**Trasy bez logowania.** Plik `trasy-publiczne.php` (np. webhook, podgląd dla klienta). Tu portal dokłada tylko przedrostek nazw. O dostępie i limitach decydujecie sami:

```php
<?php

use Illuminate\Support\Facades\Route;
use Narzedzia\FakturyKosztowe\WebhookController;

Route::post('/webhook/faktury-kosztowe', [WebhookController::class, 'przyjmij'])
    ->middleware('throttle:30,1')
    ->name('webhook');
```

Krótkie adresy `/<adres>` (jak strony i formularze) robicie hakiem `pokazAdres`, nie własną trasą `/{cos}`.

## Widoki i wygląd

Widoki z `widoki/` wołacie jako `view('<klucz>::nazwa')`. Moduł ma wyglądać jak reszta portalu, więc używacie wspólnych elementów, a nie własnego CSS.

Zasady obowiązkowe:

1. Każdy ekran modułu dziedziczy `layouts.app`, ustawia `@section('header', 'Narzędzia')` i zaczyna się od `@include('tools._naglowek', [...])`. Nagłówek daje pasek „Narzędzia › Nazwa”, powrót, zakładki i okno ustawień modułu z sekcją Widoczność i Pomocą.
2. Tylko klasy `ui-*`: `ui-page`, `ui-stack`, `ui-card`, `ui-card--flat`, `ui-field`, `ui-label`, `ui-input`, `ui-textarea`, `ui-btn`, `ui-btn--primary`, `ui-rows`, `ui-row`, `ui-empty`, `ui-badge` i inne. Pełny wzornik z przykładami jest na każdej instancji pod `/administracja/dev`.
3. **Bez własnych hero**, wielkich banerów i kolorowych teł. Ekran modułu to narzędzie pracy, nie strona reklamowa.
4. **Tryb ciemny działa sam**, jeśli kolory bierzecie z tokenów `--ui-*` (np. `var(--ui-ink)`, `var(--ui-line)`, `var(--ui-card)`). Nie wpisujcie kolorów na sztywno (`#fff`, `black`). Portal przełącza motyw atrybutem `data-theme` na `<html>` i podmienia tokeny.
5. Przycisk „+ Nowy X” idzie do sekcji `narz-akcje` jako `ui-btn--primary`, nigdy jako zakładka. Ustawienia rekordu otwieracie w oknie z auto-zapisem, bez przycisków Zapisz i Anuluj.
6. Szukajka listy: `ui-search narz-szukaj`, stronicowanie: `narz-strony`.
7. Alpine.js jest w portalu, możecie używać `x-data` bez dokładania bibliotek.

```blade
@extends('layouts.app', ['title' => 'Faktury kosztowe'])
@section('header', 'Narzędzia')

@section('narz-akcje')   {{-- przyciski po prawej w wierszu linków --}}
    <a href="{{ route('faktury_kosztowe.create') }}" class="ui-btn ui-btn--primary ui-btn--sm">Nowa faktura</a>
@endsection

@section('content')
<div class="ui-page ui-page--szeroka ui-stack">
    @include('tools._naglowek', ['nazwa' => 'Faktury kosztowe', 'klucz' => 'faktury_kosztowe'])
    <div class="ui-card ui-card--flat">...</div>
</div>
@endsection
```

Parametry `tools._naglowek`:

| Parametr | Co robi |
|---|---|
| `nazwa` | Nazwa modułu w pasku. |
| `klucz` | Klucz modułu (włącza okno ustawień, Widoczność i przypięcie do menu). |
| `pod` | Podtytuł pod nazwą. |
| `badge` | Odznaka obok nazwy. |
| `pigulki` | Zakładki ekranu: lista linków. |
| `wroc`, `wrocLabel` | Adres i napis linku powrotu. |
| `rekord` | Nazwa otwartego rekordu (np. tytuł faktury) w pasku. |
| `ustawieniaStart`, `ustawieniaOtworz` | Która sekcja okna ustawień otwiera się pierwsza i czy okno ma się otworzyć od razu. |

Sekcje, które możecie wypełnić w widoku: `narz-akcje` (przyciski po prawej), `narz-ust-nav` i `narz-ust-sekcje` (własne sekcje okna ustawień modułu), `narz-pomoc` (treść Pomocy w oknie).

Wspólne elementy portalu (opis parametrów jest w komentarzu na górze każdego pliku):

| Element | Do czego |
|---|---|
| `tools._naglowek` | Pasek modułu, zakładki, okno ustawień z Widocznością i Pomocą. |
| klasy `ui-*` | Karty, przyciski, pola, tabele, zakładki, okna. Wzornik: `/administracja/dev`. |
| `partials._edytor-tresci` | Edytor WYSIWYG. Obrazy idą na `route('narzedzia.obraz')` i wracają jako `/strony-pliki/<losowa nazwa>`. |
| `App\Support\CzystyHtml` | Czyszczenie HTML z edytora białą listą, zanim trafi do bazy. |
| `partials._tresc-style` | Style treści z edytora przy wyświetlaniu. |
| `partials._adres-publiczny` | Wiersz „Adres” z krótkim adresem `/<adres>` i kopiowaniem linku. |
| `App\Support\AdresyPubliczne` | `losowy()`, `zajety($adres, $rekord)` dla krótkich adresów (razem z hakiem `modeleAdresow`). |
| `partials._widocznosc` + trait `App\Models\Concerns\WidocznoscJakCzat` | Kto widzi rekord: wszyscy, zalogowani albo wybrane osoby i grupy. |
| `partials._wyglad-i-menu` | Wygląd rekordu (cały ekran albo w portalu) i przypięcie do menu. |
| `partials._pliki` | Panel plików jak w czacie i projekcie. |
| `partials._ikona-ust` | Ikona zębatki ustawień. |
| trait `Spatie\Activitylog\Traits\LogsActivity` | Dziennik zmian rekordu. |

Nie kopiujcie tych plików do modułu. Gdy portal je poprawi, moduł dostanie poprawkę sam.

## Migracje i tabele

`migracje/` to zwykłe migracje Laravela (`return new class extends Migration`), z datą w nazwie pliku.

- Portal uruchamia brakujące migracje **przy włączeniu modułu**, przy każdym `php artisan migrate` i po aktualizacji modułu, gdy moduł jest włączony. Jeśli migracja się nie uda, moduł zostaje wyłączony, a administrator widzi błąd.
- **Tabele nazywacie od klucza modułu**: `faktury_kosztowe`, `faktury_kosztowe_pozycje`. Prefiks = klucz. Dzięki temu nigdy nie wejdziecie w tabele portalu ani innego modułu.
- Migracje tworzą i zmieniają **tylko własne tabele**. Nie dodajecie kolumn do tabel portalu (`users`, `projects` i innych) i nie zmieniacie ich danych.
- Wyłączenie modułu **nie kasuje** tabel ani danych. Po ponownym włączeniu wszystko wraca.
- Nowa wersja modułu dokłada nowe pliki migracji. Nie zmieniajcie migracji, które już wyszły w starszej wersji: instancja uznaje je za wykonane i nie uruchomi ich drugi raz.
- `down()` piszcie porządnie, ale nie liczcie na to, że ktoś go uruchomi.

## Konfiguracja i ustawienia

**Stałe modułu** trzymacie w `konfig.php` (zwykła tablica konfiguracji Laravela). Portal wczytuje go pod kluczem `konfig` z manifestu, domyślnie `<klucz>`:

```php
<?php
// narzedzia/faktury_kosztowe/konfig.php
return [
    'na_strone' => 50,
    'waluta' => 'PLN',
];
```

```php
$ile = config('faktury_kosztowe.na_strone');
```

**Ustawienia, które zmienia administrator** (np. adres usługi, klucz API zewnętrznego systemu, godzina importu), zapisujecie w tabeli ustawień portalu przez `App\Models\Setting`, **zawsze z kluczem zaczynającym się od klucza modułu**:

```php
use App\Models\Setting;

Setting::set('faktury_kosztowe_godzina_importu', '06:00');
$godzina = Setting::get('faktury_kosztowe_godzina_importu', '06:00');
```

- Ustawienia `narzedzie_wlaczone_<klucz>` i `narzedzie_dostep_<klucz>` należą do portalu (włączenie i dostęp). Nie zapisujcie ich sami.
- Formularz ustawień modułu dajecie w oknie ustawień nagłówka (`narz-ust-nav` + `narz-ust-sekcje`), z auto-zapisem.
- Sekrety (hasła, klucze API) wpisuje administrator instancji w ustawieniach modułu. **Nigdy nie wkładajcie ich do paczki** ani do `konfig.php`.

## Zasoby i zrzuty ekranu

Pliki statyczne modułu leżą w `zasoby/`. Portal serwuje je pod `/narzedzia-zasoby/<klucz>/<plik>`, dozwolone typy: css, js, png, jpg, jpeg, gif, webp, svg, woff2, json.

W widoku bierzcie adres przez `zasob()`, który dokleja wersję po dacie zmiany pliku (przeglądarka nie trzyma starej kopii):

```blade
@php($n = \App\Narzedzia\Rejestr::narzedzie('faktury_kosztowe'))
<link rel="stylesheet" href="{{ $n->zasob('faktury.css') }}">
<img src="{{ $n->zasob('logo.png') }}" alt="">
```

Z kodu modułu macie dostęp do samego siebie przez `\App\Narzedzia\Rejestr::narzedzie('<klucz>')`: `->nazwa()`, `->wersja()`, `->sciezka('szablony/wzor.xlsx')` (pełna ścieżka pliku w folderze), `->zasob('logo.png')`.

**Zrzuty ekranu** do karty modułu w Modułach i do ekranu przed włączeniem: `zasoby/zrzuty/*.png|jpg|jpeg|webp`, w kolejności nazw (`01.webp`, `02.webp`). Zalecane: szerokość 1280-1600 px, format webp, 2-4 zrzuty, bez prawdziwych danych klientów.

## Haki klasy `src/Narzedzie.php`

Klasa haków to `Narzedzia\<Pascal>\Narzedzie extends App\Narzedzia\Narzedzie` w pliku `src/Narzedzie.php` (albo klasa z pola `klasa` w manifeście, też podklasa `App\Narzedzia\Narzedzie`). Nadpisujecie tylko to, czego moduł używa. Każdy hak portal woła w `try`: błąd w module trafia do logu, portal działa dalej.

```php
<?php

namespace Narzedzia\FakturyKosztowe;

use App\Models\User;
use App\Narzedzia\Narzedzie as Baza;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Http\Request;

class Narzedzie extends Baza
{
    // ... haki opisane niżej
}
```

### rekordyWMenu

```php
public function rekordyWMenu(User $user): array
```

Rekordy, które osoba przypięła do menu po lewej. Zwracacie listę `[['url' => ..., 'nazwa' => ...]]`. Ikona = `ikona_svg` z manifestu.

```php
public function rekordyWMenu(User $user): array
{
    return Faktura::where('user_id', $user->id)->where('w_menu', true)->orderBy('nazwa')->get()
        ->map(fn ($f) => ['url' => route('faktury_kosztowe.show', $f), 'nazwa' => $f->nazwa])
        ->all();
}
```

### blokPromptu

```php
public function blokPromptu(int $userId): array
```

Linie tekstu dopisywane do promptu każdej tury czatu tej osoby. Tak moduł mówi czatowi, co może zrobić dla użytkownika. Pusta tablica = nic. Trzymajcie to krótko: te linie idą z każdą wiadomością.

```php
public function blokPromptu(int $userId): array
{
    $ile = Faktura::where('user_id', $userId)->where('status', 'nowa')->count();
    if ($ile === 0) {
        return [];
    }

    return ["Użytkownik ma {$ile} nowych faktur kosztowych w module Faktury kosztowe (adres: /narzedzia/faktury-kosztowe)."];
}
```

### uzycie

```php
public function uzycie(): ?array
```

Skąd karta modułu w Modułach liczy „kto i kiedy korzystał”. `null` = nie liczymy.

```php
public function uzycie(): ?array
{
    return [
        'tabela' => 'faktury_kosztowe',   // własna tabela modułu
        'czas' => 'created_at',           // kolumna z datą
        'co' => 'faktur',                 // „12 faktur”
        'kto' => 'user_id',               // kolumna z id autora, domyślnie user_id
        // 'warunek' => ['status' => 'zatwierdzona'],
    ];
}
```

### modeleAdresow

```php
public function modeleAdresow(): array
```

Modele z kolumną `adres`, które zajmują krótkie adresy `/<adres>` we wspólnej przestrzeni portalu. Dzięki temu `AdresyPubliczne::zajety()` nie wyda tego samego adresu dwóm modułom.

```php
public function modeleAdresow(): array
{
    return [Oferta::class];
}
```

### pokazAdres

```php
public function pokazAdres(Request $request, string $adres): mixed
```

Wejście na `https://<instancja>/<adres>` metodą GET, **bez logowania**. Portal pyta po kolei włączone moduły. Zwracacie odpowiedź albo `null`, gdy adres nie jest Wasz. O tym, kto może zobaczyć rekord, decydujecie sami.

```php
public function pokazAdres(Request $request, string $adres): mixed
{
    $oferta = Oferta::where('adres', $adres)->first();
    if (! $oferta) {
        return null;
    }
    abort_unless($oferta->opublikowana, 404);

    return view('faktury_kosztowe::oferta', compact('oferta'));
}
```

### wyslijNaAdres

```php
public function wyslijNaAdres(Request $request, string $adres): mixed
```

Wysłanie na `/<adres>` metodą POST, bez logowania, z limitem 10 wysłań na minutę. Zwracacie odpowiedź albo `null`. Walidujcie wszystko, co przychodzi.

```php
public function wyslijNaAdres(Request $request, string $adres): mixed
{
    $oferta = Oferta::where('adres', $adres)->where('opublikowana', true)->first();
    if (! $oferta) {
        return null;
    }
    $dane = $request->validate(['email' => 'required|email|max:190', 'pytanie' => 'required|string|max:5000']);
    $oferta->pytania()->create($dane);

    return back()->with('status', 'Dziękujemy, odpowiemy mailem.');
}
```

### harmonogram

```php
public function harmonogram(Schedule $schedule): void
```

Zadania cykliczne. Portal woła ten hak tylko dla **włączonych** modułów.

```php
public function harmonogram(Schedule $schedule): void
{
    $schedule->command('faktury-kosztowe:importuj')->dailyAt('06:00')->withoutOverlapping();
}
```

Komendę artisan piszecie w `src/` i wpisujecie jej klasę w `komendy` manifestu. Nazwę komendy zacznijcie od klucza modułu.

## Skille czatu

Moduł może dać czatom instancji nowe umiejętności: katalog `skille/<nazwa>/` z plikiem `SKILL.md` (i plikami obok, np. skryptami pomocniczymi albo wzorami).

```
narzedzia/faktury_kosztowe/skille/
  ksiegowanie/
    SKILL.md
    wzor-opisu.md
```

```markdown
---
name: faktury-kosztowe-ksiegowanie
description: Opisuje fakturę kosztową do księgowania według zasad firmy. Użyj, gdy użytkownik prosi o opis albo dekretację faktury kosztowej.
---

# Opis faktury do księgowania

1. Poproś o numer faktury albo plik.
2. Opisz fakturę według wzoru z pliku wzor-opisu.md.
3. ...
```

- Nazwa katalogu skilla: małe litery, cyfry, `-` i `_`, do 61 znaków.
- Gdy moduł jest włączony, portal kopiuje skille do katalogu skilli czatów instancji jako `<klucz z myślnikami>-<nazwa>` (np. `faktury-kosztowe-ksiegowanie`). Kopia ma znacznik, po którym portal ją rozpoznaje.
- Wyłączenie albo usunięcie modułu kasuje tylko te kopie. Skilli założonych w czacie ręcznie portal nie rusza, także gdy mają tę samą nazwę.
- Skille działają **we wszystkich czatach instancji**, nie tylko u osób z dostępem do modułu. Nie wkładajcie do nich niczego, czego nie może zobaczyć każdy użytkownik instancji.
- Skill opisuje, jak czat ma pracować. Nie może kazać czatowi omijać zabezpieczeń, wysyłać danych poza instancję ani ruszać plików portalu.

**Moduł z samymi skillami.** Moduł nie musi mieć ekranu. Wystarczy manifest (`nazwa`, `opis`, `wersja`, `zmiany`) i folder `skille/`, bez `trasy.php` i widoków. Taki moduł jest na liście Modułów jak każdy inny: administrator włącza go przełącznikiem w karcie, a portal kopiuje skille do czatów. Nie ma tylko pozycji w menu ani przycisku „Otwórz”.

## Projekty i czaty

Od 29.09.2026 portal daje modułom dwie klasy do pracy z projektami i czatami. Do tabel `projects` i `terminals` moduł nadal nie sięga wprost: czyta i zakłada przez te klasy, a one liczą dostęp tym samym kodem co portal. Na instancji bez tej wersji sprawdźcie najpierw `class_exists(\App\Narzedzia\Rozmowy::class)`.

**`App\Narzedzia\Projekty`: tylko odczyt.** Zwraca zwykłe tablice z polami wspólnymi dla wszystkich instancji. Instancje dokładają do projektów własne kolumny (faza, PM, backlog), ale na nich moduł nie może polegać.

| Metoda | Zwraca |
|---|---|
| `lista($dla)` | projekty widoczne dla osoby (`$dla` = User albo id): publiczne, własne, udostępnione wprost i przez grupę (administrator widzi wszystkie) |
| `projekt(int $id)` | `id`, `nazwa`, `slug`, `widocznosc`, `tworca_id`, `kontekst`, `adres` albo `null` |
| `czlonkowie(int $id)` | `publiczny`, `tworca_id`, `osoby` (`id`, `nazwa`, `email`), `grupy` (`id`, `nazwa`, `osoby` = id osób) |
| `maDostep(int $projektId, $osoba)` | `true`/`false` (`$osoba` = User albo id), jak przy wejściu na stronę projektu |
| `rozmowy(int $projektId)` | czaty projektu bez archiwum |
| `rozmowa(int $czatId)` | `id`, `nazwa`, `projekt_id`, `katalog`, `tworca_id`, `widok` (`prosty`/`terminal`), `zalozyl_modul`, `utworzono`, `adres` |
| `rozmowaPoKatalogu(string $katalog)` | to samo po katalogu roboczym czatu (pasuje też podkatalog) |

`rozmowaPoKatalogu(getcwd())` przydaje się w komendzie artisan modułu, którą woła czat: komenda wie wtedy, z którego czatu i projektu ją uruchomiono.

**`App\Narzedzia\Rozmowy::zaloz()`: nowy czat z pierwszą wiadomością.** Idzie tą samą drogą co przycisk „+ Nowy czat” i wysłanie wiadomości w Prostym czacie.

```php
use App\Narzedzia\Rozmowy;

$czat = Rozmowy::zaloz([
    'modul'  => 'pmo',                 // klucz Waszego modułu, obowiązkowy
    'osoba'  => $request->user(),      // właściciel czatu (User albo id)
    'projekt'=> $projekt['id'],        // albo null = czat poza projektem
    'nazwa'  => 'Kamień M3: ustalenia',
    'tresc'  => "Przygotuj plan kamienia M3 na podstawie...", // pierwsza wiadomość; pusta = sam czat
]);

return redirect($czat['adres']);       // ['id', 'adres', 'projekt_id', 'katalog', 'tura']
```

- Portal sprawdza, że moduł istnieje, osoba jest aktywna i ma dostęp do projektu. Brak dostępu = `RuntimeException`, zły moduł, osoba albo projekt = `InvalidArgumentException`. Nie zakładajcie czatu w projekcie, którego osoba nie widzi, „bo moduł może”.
- Czat powstaje zawsze w widoku Prosty czat. Pierwsza wiadomość idzie od razu w imieniu osoby, a odpowiedź pojawia się w czacie jak po zwykłym wysłaniu.
- Bez `nazwa` czat dostaje nazwę jak z przycisku, a tytuł dobiera się z pierwszej wiadomości.
- Czat ma znacznik `zalozyl_modul` (klucz modułu), widoczny w `Projekty::rozmowa()`. Po nim odróżnicie czaty założone przez moduł od założonych ręcznie.
- Każde wywołanie z treścią to prawdziwa tura Claude na koncie instancji. Nie zakładajcie czatów w pętli ani z harmonogramu bez wyraźnej potrzeby.

## Do czego moduł ma dostęp i zasady

Kod modułu działa w tym samym procesie co portal. Technicznie mógłby więcej, niż mu wolno, dlatego poniższe zasady sprawdzamy przy każdym akcepcie. Złamanie którejkolwiek = odrzucenie wydania, a w poważnym przypadku blokada modułu i konta.

**Moduł może:**

- czytać i zapisywać **własne tabele** (prefiks = klucz modułu),
- czytać i zapisywać **własne ustawienia** w `App\Models\Setting` (klucz zaczyna się od klucza modułu),
- czytać zalogowaną osobę (`$request->user()`) oraz listę osób i grup (`App\Models\User`, `App\Models\UserGroup`), żeby przypisywać rekordy i ustawiać widoczność,
- czytać projekty, ich członków i powiązanie czatu z projektem przez `App\Narzedzia\Projekty` oraz zakładać czaty z pierwszą wiadomością przez `App\Narzedzia\Rozmowy::zaloz()` (sekcja Projekty i czaty),
- zapisywać pliki w `storage/app/<klucz>/` i czytać pliki z własnego folderu,
- używać wspólnych elementów portalu z tabeli w sekcji Widoki, kolejki (`dispatch`), maila portalu (`Mail`) i klienta HTTP Laravela (`Http`) w granicach zasad niżej,
- dodawać haki, skille czatu, zadania cykliczne i krótkie adresy.

**Moduł nie może:**

1. **Zapisywać do tabel, których nie założył.** Tabele portalu (czaty, projekty, pliki, użytkownicy, ustawienia innych modułów) są tylko do odczytu, i to tylko w zakresie z listy wyżej. Jeśli potrzebujecie więcej, napiszcie do nas.
2. **Ruszać plików portalu** ani innych modułów: bez zapisu do `app/`, `routes/`, `config/`, `resources/`, `public/`, `.env`, `bootstrap/`, `vendor/`, `narzedzia/<inny klucz>/`.
3. **Łączyć się z siecią poza tym, co zadeklarowaliście.** Każdy zewnętrzny adres, z którym moduł rozmawia, wpisujecie w `opis_pozycji.szczegoly` (np. „Moduł pobiera kursy walut z api.nbp.pl”) i w polu `uwagi` przy wysyłce. Niezadeklarowane połączenie = odrzucenie.
4. **Mieć sekretów w paczce**: haseł, tokenów, kluczy API, plików `.env`, danych dostępowych do czegokolwiek. Sekrety wpisuje administrator instancji w ustawieniach modułu.
5. **Uruchamiać programów systemu**: bez `exec`, `shell_exec`, `system`, `passthru`, `proc_open`, `popen`, odwrotnych apostrofów, `eval` i ładowania kodu z sieci.
6. **Wysyłać danych klienta poza instancję bez jego zgody.** Dane firmy zostają na instancji. Wyjątek: wysyłka jest celem modułu, jest opisana w `opis_pozycji` i administrator świadomie włącza moduł (np. wysyłka faktur do systemu księgowego).
7. **Zmieniać zachowania portalu poza hakami**: bez podmiany widoków portalu, globalnych middleware, nadpisywania tras portalu i własnych tras `/{cos}` w przestrzeni krótkich adresów.
8. **Ukrywać działania**: kod ma być czytelny, bez zaciemniania, bez plików binarnych z kodem i bez minifikowanego PHP.

## Wersje i lista zmian

- `wersja` to semver `x.y.z`: trzy liczby, bez przyrostków typu `-beta`.
  - `z` (poprawka): błąd naprawiony, nic nowego dla użytkownika.
  - `y` (funkcja): nowa możliwość, stare dane i ekrany działają dalej.
  - `x` (zmiana łamiąca): np. zmiana danych, która wymaga uwagi administratora.
- **Każda wysyłka musi mieć wyższy numer** niż ostatnia wysłana wersja tego modułu (także odrzucona). Numery porównujemy jak `version_compare` w PHP.
- `zmiany` to lista zmian dla użytkownika, najnowsza wersja pierwsza. Administrator widzi ją w karcie modułu przed aktualizacją, więc piszcie po ludzku, co się zmieniło dla nich, nie nazwy klas.
- `wymaga_portalu`: jeśli moduł używa czegoś, co weszło w konkretnej wersji portalu, wpiszcie tę wersję. Instancja ze starszym portalem nie dostanie modułu ani aktualizacji, dopóki portal się nie podniesie. Katalog modułów działa na portalach od 1.3.20.

## Paczka i wysyłka

**Paczka** to plik `tar.gz` z jednym katalogiem najwyższego poziomu `<klucz>/`, w którym jest `<klucz>/narzedzie.php`.

- Tylko zwykłe pliki i katalogi: bez linków symbolicznych i twardych, bez ścieżek bezwzględnych i bez `..`.
- Limity: 20 MB spakowane, 60 MB po rozpakowaniu, 5000 plików.
- Pomijamy albo odrzucamy: `.git/`, `node_modules/`, `.env*`, `*.bak*`, `.claude-token`. Nie wkładajcie też `.zrodlo.json`.

Budowa z katalogu portalu (tam, gdzie leży `narzedzia/`):

```bash
cd /sciezka/do/portalu
COPYFILE_DISABLE=1 tar czf notatki-1.0.0.tar.gz \
  --exclude='.git' --exclude='node_modules' --exclude='.env*' --exclude='*.bak*' --exclude='.zrodlo.json' \
  -C narzedzia notatki
tar tzf notatki-1.0.0.tar.gz | head    # pierwsza linia: notatki/
```

`COPYFILE_DISABLE=1` wyłącza na macOS dokładanie plików `._*`, na Linuksie nic nie zmienia.

**Wysyłka** idzie formularzem na koncie developera (moduł › Wydania) albo przez `POST https://redai.pl/api/moduly/wyslij` z Waszym kluczem API w nagłówku `Authorization: Bearer ...` i plikiem w polu `paczka` (multipart). Pola opcjonalne:

- `uwagi`: tekst dla nas (co się zmieniło, z jakimi adresami moduł się łączy, jak przetestować),
- `klucz`: klucz modułu, do którego wysyłacie. Gdy katalog w paczce nazywa się inaczej, wysyłka jest odrzucona (chroni przed pomyłką w skrypcie).

API służy do wysyłania wydań, do odczytu stanu i do ustawiania, na jakich instancjach moduł ma być widoczny (`/api/moduly/adresy`, opis w sekcji Trzy zgody). Moduł na instancji zawsze instaluje i włącza jej administrator.

```bash
export REDAI_KLUCZ='rmk_...'   # Wasz klucz API, nie wklejajcie go do repozytorium

curl -sS -X POST https://redai.pl/api/moduly/wyslij \
  -H "Authorization: Bearer $REDAI_KLUCZ" \
  -F "paczka=@notatki-1.0.0.tar.gz" \
  -F "klucz=notatki" \
  -F "uwagi=Pierwsza wersja. Moduł nie łączy się z żadnym zewnętrznym adresem."
```

Odpowiedź, gdy paczka przeszła:

```json
{"ok": true, "klucz": "notatki", "wersja": "1.0.0", "status": "oczekuje", "id": 42}
```

- `status: "oczekuje"`: wydanie czeka na nasz akcept.
- `status: "zaakceptowane"`: wydanie jest od razu w katalogu (tylko zaufany autor i moduł prywatny, patrz Akcept).

Gdy klucz API ma przypisane instancje (wcześniejsza wersja konta, widać je w zakładce API), odpowiedź ma też pole `instancje`:

```json
{"ok": true, "klucz": "notatki", "wersja": "1.0.1", "status": "oczekuje", "id": 43,
 "instancje": {"dodane": [], "do_zatwierdzenia": ["https://firma.redai.pl"]}}
```

- `dodane`: instancje, które od razu są na liście modułu (moduł publiczny albo zaufany autor),
- `do_zatwierdzenia`: instancje, które czekają na decyzję redAi (dostaniecie maila).

Odpowiedź, gdy coś jest nie tak (kod HTTP 4xx):

```json
{"ok": false, "error": "Paczka nie przeszła sprawdzenia.", "bledy": ["narzedzie.php: manifest musi być samymi stałymi (wywołanie funkcji env w linii 5)", "src/Faktura.php: błąd składni PHP w linii 18"]}
```

Co sprawdzamy przy wysyłce, zanim wydanie w ogóle trafi do akceptu:

| Sprawdzenie | Typowy błąd |
|---|---|
| Klucz API | brak nagłówka, zły albo unieważniony klucz |
| Format paczki | więcej niż jeden katalog na górze, brak `<klucz>/narzedzie.php`, link symboliczny, `..`, przekroczone limity |
| Manifest | wyrażenie zamiast stałej, brak `nazwa` albo `wersja`, zły format wersji, klucz niezgodny ze wzorcem |
| Składnia PHP | `php -l` każdego pliku `.php` w paczce |
| Właściciel klucza | klucz modułu należy do innego autora |
| Wersja | numer nie jest wyższy od ostatniej wysłanej wersji |

W skrypcie sprawdzajcie pole `ok` i czytajcie `bledy`, nie opierajcie się tylko na kodzie HTTP.

**Stan Waszych modułów** (wydania, statusy, nasze uwagi, widoczność, wnioski czekające na redAi, która wersja stoi gdzie). Tylko odczyt:

```bash
curl -sS https://redai.pl/api/moduly/moje -H "Authorization: Bearer $REDAI_KLUCZ"
```

## Konto developera

- Konto zakładacie sami na **[redai.pl/dev/konto](https://redai.pl/dev/konto/)**: imię albo nazwa firmy, e-mail, hasło. Na maila przyjdzie link, który zakłada konto (ważny 48 godzin). Nie potrzebujecie nic od nas ani dostępu do naszego panelu.
- Konto ma dwie zakładki:
  - **Moduły**: lista Waszych modułów ze statusem u redAi (szkic, czeka na redAi, zaakceptowany, odrzucony z powodem), wersją i instancjami. Przycisk **Nowy moduł** zakłada szkic (nazwa, klucz, opis, ikona) albo przyjmuje od razu gotową paczkę.
  - **API**: klucze API (tworzenie, unieważnianie) i skrót endpointów z przykładem `curl`.
- Karta modułu ma trzy sekcje:
  - **Przegląd**: co teraz z modułem (status u redAi, nasze uwagi, co zrobić dalej), ostatnie wydanie i skrót instancji. Przy szkicu: cztery kroki pierwszego wydania i **gotowy szkielet** (`<klucz>-0.1.0.tar.gz`: manifest, trasa, kontroler, widok).
  - **Wydania**: wysyłka nowej wersji, polecenia `curl` i prompt dla asystenta AI pod ten moduł, historia z naszymi uwagami.
  - **Instancje**: publiczny albo wybrane instancje, jedna lista adresów ze stanem modułu na każdej (działa, zainstalowany i wyłączony, czeka na redAi, nieznana instancja).
- **Klucz API** tworzycie na koncie (format `rmk_` + 48 znaków). Pokazujemy go **tylko raz**, u nas zostaje tylko jego skrót. Zgubiony klucz unieważniacie i tworzycie nowy. Możecie mieć kilka kluczy, np. osobny na komputer, CI i asystenta AI.
- **Instancje ustawiacie przy module**, nie przy kluczu: sekcja Instancje na karcie albo `POST /api/moduly/adresy`. Klucze przypisane do instancji we wcześniejszej wersji konta działają dalej (każde wydanie wysłane takim kluczem proponuje moduł tym instancjom, nowe zatwierdza redAi). W zakładce API widać je przy kluczu i możecie je tam odłączyć.
- Klucz trzymajcie w zmiennej środowiskowej albo w menedżerze sekretów, nigdy w paczce, repozytorium ani w pliku, który czyta czat AI.
- Po każdej naszej decyzji o wydaniu (akcept, odrzucenie, wycofanie) i o widoczności dostajecie maila.

## Trzy zgody: jak moduł trafia na cudzą instancję

Typowa sytuacja: zrobiliście moduł na instancji jednej firmy i chcecie go dać na instancję, którą prowadzi inny developer albo inna firma. Moduł staje na instancji dopiero, gdy zgodzą się trzy strony:

1. **redAi** sprawdza każde wydanie (status `oczekuje` → `zaakceptowane`). Bez tego żadna instancja modułu nie widzi.
2. **Wy** wskazujecie instancję przy module (konto › moduł › Instancje albo `POST /api/moduly/adresy`). To Wasza zgoda, że moduł może tam trafić. Nową instancję na liście zatwierdza redAi (patrz niżej).
3. **Administrator instancji** widzi moduł w **Administracja › Moduły**, w kategorii **„Moduły innych developerów”**, z nazwą autora. Instaluje go sam i sam włącza. Dodanie adresu niczego nie instaluje.

Po instalacji kolejne zaakceptowane wersje modułu prywatnego wchodzą na tę instancję same. Usunięcie adresu z listy odcina instancji nowe wersje, ale nie odinstalowuje modułu, który już tam stoi.

Na koncie przy każdym adresie widzicie, na jakim etapie jest moduł:

| Stan | Co znaczy |
|---|---|
| czeka na akcept redAi | nie ma jeszcze zaakceptowanego wydania |
| jeszcze nie widzi | instancja nie pytała katalogu od dodania adresu (pyta raz dziennie albo po „Sprawdźcie teraz”) |
| czeka na administratora | instancja widzi moduł, administrator go jeszcze nie zainstalował |
| zainstalowany, wyłączony | stoi, administrator jeszcze go nie włączył |
| działa | zainstalowany i włączony, obok wersja |
| nieznana instancja | pod tym adresem nie znamy instancji redAi, sprawdźcie literówkę |

**Widoczność przez API.** Listę instancji (i przejście na moduł publiczny) ustawiacie też kluczem API, np. z terminala albo przez asystenta AI. Działa to dokładnie jak sekcja Instancje na karcie modułu: usunięcie adresu i powrót do prywatnego wchodzą od razu, nowe instancje i katalog publiczny idą do zatwierdzenia przez redAi (zaufany autor albo moduł publiczny: nowe instancje od razu). Dodanie adresu niczego nie instaluje.

```bash
# stan: widoczność, instancje na liście, wniosek czekający na redAi
curl -sS "https://redai.pl/api/moduly/adresy?klucz=notatki" -H "Authorization: Bearer $REDAI_KLUCZ"

# dopisanie i usunięcie instancji (reszta listy bez zmian)
curl -sS -X POST https://redai.pl/api/moduly/adresy -H "Authorization: Bearer $REDAI_KLUCZ" \
  -H "Content-Type: application/json" \
  -d '{"klucz": "notatki", "dodaj": ["https://firma.redai.pl"], "usun": ["https://stara-firma.redai.pl"]}'

# pełna lista (zastępuje obecną) albo zmiana widoczności
curl -sS -X POST https://redai.pl/api/moduly/adresy -H "Authorization: Bearer $REDAI_KLUCZ" \
  -H "Content-Type: application/json" -d '{"klucz": "notatki", "adresy": ["https://firma.redai.pl"], "publiczny": false}'
```

Pola: `klucz` (wymagany) i jedno z `adresy` (pełna lista) albo `dodaj` / `usun`, opcjonalnie `publiczny` (`true` / `false`, brak = bez zmian). `dodaj` i `usun` liczą się od obecnej listy razem z tym, co już czeka we wniosku. Adres może być bez `https://`. Odpowiedź:

```json
{"ok": true, "klucz": "notatki", "widocznosc": "prywatny", "adresy": ["https://firma-magazyn.redai.pl"],
 "wniosek": {"publiczny": false, "adresy": ["https://firma.redai.pl"]},
 "zmiana": {"dodane": [], "usuniete": ["https://stara-firma.redai.pl"], "prywatny": false,
            "do_zatwierdzenia": {"publiczny": false, "adresy": ["https://firma.redai.pl"]}},
 "pominiete": [], "komunikat": "Usunięte z listy: ... Do zatwierdzenia przez redAi: ..."}
```

- `adresy`: instancje, które widzą moduł już teraz, `wniosek`: to, co czeka na redAi (decyzja przyjdzie mailem),
- `pominiete`: wpisy, które nie są adresami. Jeden moduł ma najwyżej 200 instancji.

## Moduł publiczny i prywatny

Każdy moduł ma jedną z dwóch widoczności, ustawianą na koncie developera (moduł › Instancje). To samo pole może ustawić redAi w swoim panelu.

| | Publiczny | Prywatny (domyślnie) |
|---|---|---|
| Kto widzi moduł w katalogu | wszystkie instancje redAi | tylko instancje z Waszej listy adresów |
| Akcept redAi | każde wydanie | każde wydanie, chyba że jesteście zaufanym autorem |
| Instalacja | administrator instancji | administrator instancji (kategoria „Moduły innych developerów”) |
| Aktualizacja na instancji | administrator klika „Zaktualizujcie”, chyba że wydanie jest wymuszone | wchodzi sama na instancje z listy, na których moduł jest zainstalowany |

**Lista adresów instancji** dla modułu prywatnego: jeden adres na linię, np.

```
https://firma.redai.pl
https://firma-magazyn.redai.pl
```

Adres skracamy do samej domeny z `https://` (ścieżki i końcowy ukośnik są pomijane). Typowy przypadek: jedna firma ma dwie instancje, Wy robicie dla niej moduł, administrator instaluje go na obu, a każda kolejna wersja wchodzi już sama.

**Co zatwierdza redAi, a co wchodzi od razu:**

| Zmiana | Kiedy wchodzi |
|---|---|
| Odznaczenie instancji, powrót do prywatnego | od razu |
| Nowa instancja na liście | po akcepcie redAi (zaufany autor albo moduł już publiczny: od razu) |
| Przejście na publiczny | zawsze po akcepcie redAi |

Poszerzenie trafia do nas jako **wniosek** (jeden na moduł, kolejne zmiany go aktualizują). Do decyzji moduł widzą tylko instancje, które już są na liście. Wniosek możecie wycofać, a o decyzji (z powodem przy odrzuceniu) dostajecie maila. Status wniosku widać na karcie modułu i w `/api/moduly/moje` (pole `wniosek`).

## Akcept i zaufani autorzy

- Każde nowe wydanie ma status `oczekuje`, dopóki go nie sprawdzimy. Czytamy kod paczki pod kątem zasad z sekcji „Do czego moduł ma dostęp i zasady”.
- Po sprawdzeniu wydanie jest `zaakceptowane` albo `odrzucone` z naszą uwagą (widać ją na koncie developera, w mailu i w `/api/moduly/moje`). Odrzucone wydanie poprawiacie i wysyłacie z **wyższym numerem wersji**.
- Wydanie może też zostać `wycofane`, gdy znajdziemy problem po akcepcie. Instancje przestają je wtedy dostawać.
- **Zaufany autor** to programista, któremu nadaliśmy zaufanie po wcześniejszych, czystych wydaniach. Jego wydania **modułów prywatnych** są akceptowane od razu po wysyłce (status `zaakceptowane`) i trafiają na instancje z listy bez czekania na nas. Wydania modułów publicznych zawsze sprawdzamy.
- Zaufanie nie zdejmuje zasad. Wydania zaufanych autorów też przeglądamy, tylko po fakcie.

## Jak instancja pobiera i instaluje moduły

**Katalog.** Każda instancja raz dziennie pyta redai.pl o katalog: jakie moduły może zainstalować i jakie są najnowsze zaakceptowane wersje. Administrator może zapytać od razu przyciskiem **„Sprawdźcie teraz”** w **Administracja › Moduły** (pasek „Katalog modułów redAi” nad listą). Przy okazji instancja zgłasza, które moduły ma i w jakich wersjach, dlatego na koncie developera widzicie, gdzie stoi która wersja.

**Instalacja.** Instancja nigdy nie instaluje nowego modułu sama. Administrator wybiera moduł z katalogu (moduły redAi w swoich kategoriach, moduły innych autorów w kategorii „Moduły innych developerów”) i klika instaluj. Instancja:

1. pobiera paczkę i sprawdza jej sumę kontrolną,
2. rozpakowuje ją obok i sprawdza manifest (klucz, wersja),
3. odkłada obecny folder modułu jako kopię i wstawia nowy na jego miejsce,
4. uruchamia migracje, jeśli moduł jest włączony,
5. odświeża skille czatu i widoki.

Świeżo zainstalowany moduł jest wyłączony, dopóki administrator go nie włączy i nie wybierze, kto ma dostęp.

**Aktualizacja.** Karta modułu w Modułach ma kartę „Wersja”: numer, lista zmian, źródło i przycisk „Zaktualizujcie do X”. Aktualizacja idzie tą samą drogą co instalacja. Poprzednia wersja zostaje jako kopia (instancja trzyma dwie ostatnie). **Jeśli cokolwiek pójdzie źle** (migracja, błąd przy starcie), instancja sama wraca do poprzedniej wersji i pokazuje błąd administratorowi.

**Wymuszone aktualizacje.** Wydanie wchodzi na instancję samo, bez klikania administratora, gdy:

- moduł jest prywatny i już zainstalowany (każda zaakceptowana wersja wchodzi sama na instancje z listy), albo
- redAi oznaczy wydanie jako wymuszone (np. poprawka bezpieczeństwa w module publicznym).

Wymuszona aktualizacja wchodzi przy najbliższym sprawdzeniu katalogu. Gdy trzeba szybciej, możemy z naszej strony kazać instancjom sprawdzić katalog od razu: instancja przyjmuje sygnał od ręki i aktualizuje się w tle, nowa wersja jest widoczna na koncie po chwili. Jeśli potrzebujecie tego dla swojego modułu, napiszcie do nas.

**Przywrócenie.** W karcie „Wersja” administrator ma przycisk „Przywróćcie X”, który cofa moduł do poprzedniej wersji (dane zostają, migracji nie cofamy). Wersja cofnięta w ten sposób nie wgra się już sama, nawet jeśli jest wymuszona albo moduł jest prywatny. Administrator wgra ją przyciskiem „Zaktualizujcie do X”, a każde **nowsze** wydanie znowu wejdzie automatycznie. Jeśli ktoś cofnął Wasz moduł, poprawcie błąd i wydajcie wyższą wersję.

Moduły położone ręcznie w `narzedzia/` (bez katalogu) działają dalej, ale nie dostają aktualizacji z katalogu.

## Lista kontrolna przed wysłaniem

1. Klucz modułu zgodny ze wzorcem `^[a-z][a-z0-9_]{1,40}$` i taki sam jak nazwa folderu.
2. Manifest to same stałe, ma `nazwa`, `opis`, `wersja` (wyższa niż poprzednia) i wpis w `zmiany` dla tej wersji.
3. `php -l` przechodzi dla każdego pliku PHP: `find narzedzia/<klucz> -name '*.php' -print0 | xargs -0 -n1 php -l`.
4. Moduł widać w Modułach jako wyłączony, włączenie przechodzi, migracje bez błędu.
5. Ekran startowy i każda akcja działa w przeglądarce jako administrator **i jako zwykły użytkownik**, w trybie jasnym i ciemnym, na telefonie.
6. Po wyłączeniu modułu zwykły użytkownik wchodzący na jego adres ląduje na liście narzędzi z komunikatem, reszta portalu działa.
7. Tabele i ustawienia mają prefiks klucza, moduł nie zapisuje nic poza swoim.
8. Każdy zewnętrzny adres jest wpisany w `opis_pozycji.szczegoly`.
9. W paczce nie ma sekretów, `.env`, `.git`, `node_modules`, kopii `*.bak*`, `.zrodlo.json`.
10. Log portalu (`storage/logs/laravel.log`) bez nowych błędów po przeklikaniu modułu.
11. Paczka ma jeden katalog `<klucz>/` na górze (`tar tzf paczka.tar.gz | head`).

## FAQ

### Jak przetestować moduł przed wysłaniem?

Skopiujcie folder do `narzedzia/` na instancji, do której macie dostęp serwerowy, i włączcie go w Modułach. Jeśli nie macie takiej instancji, poproście nas o instancję testową.

### Potrzebuję haka, którego nie ma. Co robić?

Napiszcie do nas, czego potrzebujecie. Hak dopisujemy w portalu dla wszystkich modułów, a nie jako wyjątek pod jeden moduł. Nie obchodźcie tego zmianą plików portalu.

### Czy mogę użyć pakietu z composera albo npm?

Pakietów composera nie doinstalujecie na instancji. Macie wszystko, co ma portal (Laravel, klient `Http`, kolejki, mail, Alpine.js). Małą bibliotekę PHP bez zależności możecie włożyć do folderu i załadować `require_once` ze swojej klasy, jeśli licencja na to pozwala. Gotowy JS i CSS kładziecie w `zasoby/` jako zbudowane pliki, bez `node_modules`.

### Gdzie moduł trzyma pliki użytkowników?

W `storage/app/<klucz>/` albo w bazie. Nie w folderze modułu: przy aktualizacji jest podmieniany w całości.

### Co się dzieje z danymi po wyłączeniu modułu?

Nic. Tabele, ustawienia i pliki zostają, po ponownym włączeniu moduł widzi je z powrotem. Skille czatu z modułu znikają z czatów na czas wyłączenia.

### Moja wersja została odrzucona. Czy mogę wysłać ją jeszcze raz pod tym samym numerem?

Nie. Poprawcie kod, podnieście numer (np. z 1.2.0 na 1.2.1) i dopiszcie zmianę w `zmiany`.

### Czy mogę cofnąć instancję do starszej wersji modułu?

Instancja sama wraca do poprzedniej wersji, gdy aktualizacja się nie uda. Świadomy powrót robimy przez wycofanie wydania albo wysłanie poprawki z wyższym numerem. Napiszcie do nas, jeśli potrzebujecie cofnąć wersję na konkretnej instancji.

### Klucz, który chcę, jest zajęty.

Klucz należy do autora, który pierwszy go wysłał. Wybierzcie inny, najlepiej z przedrostkiem Waszej firmy.

### Czy moduł może zmienić menu albo wygląd całego portalu?

Nie. Moduł pokazuje się na liście narzędzi i w menu po przypięciu, może przypinać swoje rekordy (`rekordyWMenu`) i dokładać ekrany pod swoim prefiksem. Wygląd portalu zostaje wspólny.

### Czy czat AI może napisać moduł za mnie?

Tak. Dajcie czatowi ten plik: https://redai.pl/dev/moduly.md. Są tu wszystkie reguły, wzory i lista kontrolna. Przed wysyłką i tak przejdźcie listę kontrolną sami.

### Z kim rozmawiać?

Konto, zaufanie, pilne aktualizacje, brakujące haki: napiszcie na m@redai.pl.
