maindrive
Platforma operacyjna dla MŚP — zarządzanie pracownikami, urlopami, sprzętem, marnotrawstwem i wynikami finansowymi w jednym miejscu.
Szybki start
Jak wejść do aplikacji i wykonać pierwszą konfigurację.
Logowanie do panelu admina
Panel admina obsługuje dwa tryby logowania — przełącznik widoczny jest w oknie logowania.
org_admin lub manager może zalogować się do panelu admina swoim osobistym tokenem. Widzi tylko sekcje do których ma uprawnienia.Sekcje dostępne wg roli
| Sekcja | superadmin | org_admin | manager |
|---|---|---|---|
| System motywacyjny | ✓ | — | — |
| Urlopy | ✓ | ✓ (cała org) | ✓ (team) |
| Wyposażenie | ✓ | ✓ | ✓ |
| Waste Radar | ✓ | ✓ | — |
| Obiady (zarządzanie menu) | ✓ | ✓ | — |
| Moje obiady (wybór) | ✓ | ✓ | ✓ |
| Organizacja | ✓ | ✓ | — |
| System | ✓ | — | — |
Architektura
Synchroniczny serwer HTTP. Wszystkie operacje na bazie są synchroniczne dzięki better-sqlite3 — brak callbacków, brak race conditions. Serwer główny: server-sqlite.js (~3800 linii). Moduły wyciągnięte: server/lunch.js.
Pojedynczy plik db/kokpit.db. WAL mode zapewnia odczyty równoległe bez blokowania zapisów. Idealna dla MŚP do ~500k rekordów.
Pojedynczy plik public/index.html. Brak frameworków — szybkie ładowanie, zero zależności frontendowych.
PM2 zarządza procesem Node.js. Auto-restart po awarii. Dwie instancje: kokpit (port 3000) i kokpit-dev (port 3001). Pipeline deploy: syntax check + 253 testów + smoke test.
Pokrywają wszystkie moduły: financial (math pul, tenure-weighted bonus), voting, vacations, surveys, equipment, departments, waste, lunch, support, ownership-proposals, api-keys, anti-escalation, admin-scope, filters, reports, tenants, backups. Każdy deploy blokowany jeśli failure.
Middleware requireNotEscalating() blokuje org_admina przed modyfikacją superadmina (5 endpointów). Zmiany udziałów wymagają zatwierdzenia przez wszystkich pozostałych udziałowców (multi-sig).
Schemat bazy danych
-- Organizacje (multi-tenant) tenants id · name · slug · active · modules · created_at -- Działy wewnątrz organizacji departments id · tenant_id · name · manager_id · created_at -- Pracownicy z rolą i przypisaniem do działu employees id · name · start_date · eligible_for_raise · ownership_pct token · tenant_id · department_id · role -- Moduł podwyżek i premii financial_years year · is_current · closed_at financial_records year · period · navi_* · buildi_* · is_active · saved_at votes employee_id · count used_tokens token · voted_at management employee_id · amount -- Moduły operacyjne vacations id · employee_id · type · start/end_date · status · tenant_id equipment id · employee_id · name · category · brand · model · serial_number · assigned_at · status · notes equipment_requests id · employee_id · type · category · equipment_id · title · description · priority · status · admin_note · created_at · resolved_at · resolved_by waste_items id · employee_id · type · title · description · scale · estimated_cost · status · assigned_to · created_at · resolved_at · tenant_id -- Obiady (template'owy: aktywne menu → resolveowane na konkretne daty) lunch_menus id · name · valid_from · valid_to · is_active · created_at · created_by · tenant_id lunch_menu_items id · menu_id · day_of_week (1..5) · name · description · price · position · is_vegetarian · is_vegan · is_gluten_free lunch_orders id · employee_id · date · menu_item_id · status · created_at · cancelled_at · cancellation_reason lunch_restaurant_orders id · date · generated_at · total_amount · items_json · recipients · email_subject · email_body · status -- Konfiguracja klucz-wartość (JSON) settings key · value
ALTER TABLE ADD COLUMN są opakowane w sprawdzenie PRAGMA table_info, więc restart serwera na istniejącej bazie jest bezpieczny.
Role
System uprawnień oparty o RBAC (Role-Based Access Control) z hierarchią czterech ról.
superadmin.employees.role. Zmiana następuje przez dropdown w sekcji Organizacja → Pracownicy (tryb edycji) lub przez PATCH /api/v1/employees/:id/role.
Macierz uprawnień
Każde uprawnienie ma przypisany zakres danych (scope), który określa jakie rekordy są widoczne w odpowiedzi API.
| Moduł / akcja | superadmin | org_admin | manager | employee |
|---|---|---|---|---|
| PRACOWNICY | ||||
| read | platform | org | team | own |
| create / delete | ✓ | ✓ | — | — |
| manage (role, dept) | ✓ | ✓ | team | — |
| WASTE RADAR | ||||
| read | platform | org | team | own |
| create / update own | ✓ | ✓ | ✓ | ✓ |
| approve / resolve | ✓ | ✓ | team | — |
| delete | ✓ | ✓ | — | — |
| URLOPY | ||||
| read | platform | org | team | own |
| create (wniosek) | ✓ | ✓ | ✓ | ✓ |
| approve / reject | ✓ | ✓ | team | — |
| FINANSE | ||||
| read | platform | org | team* | — |
| create / update | ✓ | ✓ | — | — |
| DZIAŁY | ||||
| read | platform | org | team | — |
| create / update / delete | ✓ | ✓ | — | — |
* Manager widzi sumaryczne wyniki teamu, bez indywidualnych kwot (konfigurowalnie)
Działy
Działy to podstawowa jednostka organizacyjna określająca zakres danych dla managerów.
Jak działają zakresy
Kiedy manager wywołuje GET /api/v1/vacations, serwer wykonuje:
-- 1. Pobierz ID wszystkich pracowników z tego działu SELECT id FROM employees WHERE department_id = '[deptId managera]' AND tenant_id = '[tenantId]'; -- 2. Filtruj urlopy tylko tych pracowników SELECT v.*, e.name FROM vacations v LEFT JOIN employees e ON v.employee_id = e.id WHERE v.tenant_id = '[tenantId]' AND v.employee_id IN (...);
Zarządzanie w panelu
Panel Organizacja → Działy i role umożliwia:
- Tworzenie działów z opcjonalnym managerem
- Przypisywanie pracowników przez modal „Członkowie"
- Zmianę roli inline (dropdown przy każdym pracowniku)
- Usuwanie działu — pracownicy zostają, tylko tracą przypisanie
employee do manager.
Multi-tenant
Aplikacja obsługuje wiele niezależnych organizacji na jednej instalacji. Każda organizacja ma własny tenant_id — dane są izolowane na poziomie każdego zapytania SQL.
-- Każde zapytanie filtruje po tenant_id WHERE tenant_id = '[tenantId z tokenu pracownika]'
Zarządzanie tenantami (API)
Dostępne moduły
Każda organizacja może włączyć lub wyłączyć poszczególne moduły:
POST /api/v1/tenants/me/modules { "modules": ["dashboard", "employees", "waste", "vacations", "equipment"] }
Wyłączony moduł zwraca 403 z komunikatem „Moduł X nie jest aktywny w Twojej organizacji".
Waste Radar
Pracownicy zgłaszają marnotrawstwo w firmie — drogie SaaS bez wartości, ad-hoc zakupy, nieefektywne procesy, marnowany czas. Za każde rozwiązane zgłoszenie — bonus dla całego zespołu (20% z odzyskanej kwoty).
4 kategorie
license · Subskrypcje i licencjepurchase · Zakupy i kosztyprocess · Procesytime · CzasKwota straty (sprzężone inputy)
Pracownik wpisuje konkretną kwotę — albo miesięczną, albo roczną. Druga wylicza się dynamicznie (jak w Revolut/ING przy wymianie walut):
- Wpiszesz
500w polu mies. → roczne pokaże6000, w stanie zapisuje sięestimatedCost = 6000 - Wpiszesz
25 000w polu rocznym → mies. pokaże2083, w stanieestimatedCost = 25000
Pole scale jest auto-derivowane z kwoty rocznej (etykieta agregatu w tabeli i raportach):
low— < 2 000 PLN/rok (Mała)mid— 2 000–10 000 PLN/rok (Średnia)high— 10 000–50 000 PLN/rok (Duża)huge— ≥ 50 000 PLN/rok (Krytyczna)
Legacy fallback: jeśli klient wyśle wyłącznie scale bez estimatedCost, serwer wylicza koszt z domyślnej mapy (1k/5k/25k/75k). API od maja 2026 preferuje estimatedCost.
Przepływ statusów
active → in_progress → resolved
↑ ↑
| └── admin / manager / org_admin oznacza jako rozwiązane
└── pracownik bierze się za naprawę (PATCH /waste/:id/assign)
Panel admina (zakładka Waste Radar)
- Statystyki na górze: liczba otwartych, szacunkowa strata roczna, premia × pracownik (przy 100% rozwiązaniu), liczba rozwiązanych
- Pasek strat per kategoria — kolorowy stack pokazujący w czym leży największy potencjał oszczędności
- Status tabs z licznikami: Wszystkie / Aktywne / W trakcie / Rozwiązane
- Filtr kategorii + checkbox "Tylko moje" (filtruje tabelę do zgłoszeń stworzonych przez zalogowanego użytkownika; agregaty na górze zostają firmowe)
- Tabela: badge kategorii, tytuł + opis, autor, skala (auto-derived z kwoty), roczna strata, data, status (+ kto wziął), akcje
- Akcje: → przypisz do mnie, ✓ rozwiąż (admin/manager dla swojego działu), × usuń (admin lub autor jeśli nie rozwiązane)
Mechanizm bonusów
Suma szacowanej rocznej straty z otwartych zgłoszeń × 20% = pula bonusów (gdyby udało się rozwiązać 100%). Pula podzielona równo na każdego pracownika to bonusPerPerson w GET /waste/stats.
3-krokowy formularz zgłoszenia
Schema bazy
CREATE TABLE waste_items (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL, -- autor zgłoszenia (FK do employees)
type TEXT NOT NULL, -- license | purchase | process | time
title TEXT NOT NULL,
description TEXT,
scale TEXT, -- low | mid | high | huge
estimated_cost REAL DEFAULT 0, -- roczna strata w PLN
status TEXT DEFAULT 'active', -- active | in_progress | resolved
assigned_to TEXT, -- kto się tym zajmuje (FK do employees)
created_at TEXT,
updated_at TEXT,
resolved_at TEXT,
tenant_id TEXT DEFAULT 'default'
);
Urlopy
Pracownicy składają wnioski urlopowe przez panel pracownika. Manager lub HR manager zatwierdza je w panelu admina → Urlopy.
Typy wniosków
- Urlop dzienny — zakres dat od/do, liczba dni obliczana automatycznie
- Urlop godzinowy — konkretna data + godzina od + liczba godzin
Statusy
Panel zarządzania urlopami (admin)
Sekcja Urlopy w panelu admina pokazuje:
- Statystyki: liczba oczekujących / zatwierdzonych / odrzuconych / łącznie
- Tabelę wszystkich wniosków z filtrem po statusie i pracowniku
- Przyciski Zatwierdź / Odrzuć inline przy wnioskach oczekujących
Dostępna dla: superadmin (wszystkie), org_admin (cała org), manager (tylko swój team).
Reguły zatwierdzania
- Manager widzi i zatwierdza wnioski pracowników ze swojego działu
- org_admin zatwierdza wszystkie wnioski w organizacji
- Wniosek ze statusem
pendingmoże być anulowany przez pracownika
Moje urlopy (admin/manager)
Każdy zalogowany użytkownik z panelem admina (superadmin/org_admin/manager) ma w grupie MÓJ PANEL → Moje urlopy osobną sekcję do składania własnych wniosków:
- Trzy karty statystyk: wykorzystano / pozostało / wymiar (z paskiem postępu)
- Formularz: typ (dzień wolny / godzinowy / chorobowy / inny), daty, notatka
- Historia własnych wniosków z badge'em statusu i przyciskiem anuluj dla pending
- Korzysta z tych samych endpointów
/api/v1/vacations/meco portal pracownika
Sprzęt
Ewidencja sprzętu firmowego z przypisaniem do konkretnego pracownika. Każdy element ma kategorię, status, opcjonalną markę/model/numer seryjny i datę przydzielenia.
Kategorie
Każda kategoria ma własną ikonę i kolor — używane spójnie w panelu admina i widoku pracownika.
Statusy sprzętu
active— aktywny, w użyciureplace— do wymiany (np. uszkodzony, stary)returned— zwrócony przez pracownika do magazynuinactive— nieaktywny, wycofany z użytku
Widok pracownika
Pracownik w zakładce Sprzęt widzi wyłącznie sprzęt do niego przypisany (employee_id = jego ID). Lista pochodzi z GET /api/v1/equipment/me.
Każdy element pokazany jest jako karta: ikona kategorii, nazwa, marka·model, numer seryjny, data przydziału, badge statusu.
Panel admina
- Status tabs z licznikami: Wszystkie / Aktywne / Do wymiany / Zwrócone / Nieprzypisane
- Filtry: pracownik, kategoria, wyszukiwarka tekstowa (nazwa, marka, model, S/N, imię)
- + Dodaj sprzęt — modal z formularzem (kategoria, status, nazwa, marka, model, S/N, przypisany do, notatki)
- Edycja inline — klik na ikonę ✏ otwiera ten sam modal w trybie edycji + przycisk Usuń
Schema bazy
CREATE TABLE equipment (
id TEXT PRIMARY KEY,
employee_id TEXT, -- FK do employees, NULL = magazyn
name TEXT NOT NULL,
category TEXT DEFAULT 'other',
brand TEXT,
model TEXT,
serial_number TEXT,
assigned_at TEXT, -- ISO timestamp, NULL gdy nieprzypisany
status TEXT DEFAULT 'active',
notes TEXT
);
Zgłoszenia sprzętowe
Każdy zalogowany pracownik (w tym admin/manager) może wysłać zgłoszenie sprzętowe trzech rodzajów:
new— zapotrzebowanie na nowy sprzęt (wymaga wskazania kategorii)fault— usterka istniejącego sprzętu (wymaga wskazania własnego sprzętu)replacement— potrzeba wymiany na nowszy / lepszy (wymaga wskazania własnego sprzętu)
Pracownik może zgłaszać usterki i wymianę tylko własnego sprzętu (walidacja po stronie serwera). Każde zgłoszenie ma priorytet (low / normal / high / urgent) i status, który zmienia administrator.
Statusy zgłoszeń
pending— oczekujące na decyzję administratora (nowe zgłoszenie)approved— zaakceptowane, oczekuje realizacji (np. zamówienie wysłane)in_progress— w trakcie realizacji (np. sprzęt w serwisie)resolved— zrealizowane (sprzęt dostarczony / naprawiony)rejected— odrzucone
Widok pracownika
Pod listą przypisanego sprzętu pracownik widzi 3 przyciski akcji: Zgłoś zapotrzebowanie, Zgłoś usterkę, Potrzebuję wymiany. Każda kafelka sprzętu ma też skrót do szybkiego zgłoszenia usterki tego konkretnego urządzenia. Poniżej widoczna jest lista moich zgłoszeń ze statusem, komentarzem administratora i opcją anulowania (gdy pending).
Panel admina (zakładka „Zgłoszenia")
- Status tabs z licznikami: Wszystkie / Oczekujące / W trakcie / Zaakceptowane / Zrealizowane / Odrzucone
- Badge na zakładce pokazuje liczbę
pendingwymagających reakcji - Kliknięcie Edytuj na zgłoszeniu otwiera modal: zmiana statusu + komentarz dla pracownika (widoczny u niego)
- Manager widzi i edytuje tylko zgłoszenia pracowników ze swojego działu (scope serwerowy)
- Usuwanie zgłoszeń: tylko superadmin / org_admin (manager nie usuwa)
Schema bazy — zgłoszenia
CREATE TABLE equipment_requests (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL, -- FK do employees (zgłaszający)
type TEXT NOT NULL, -- 'new' | 'fault' | 'replacement'
category TEXT, -- tylko dla type='new'
equipment_id TEXT, -- FK do equipment, dla fault/replacement
title TEXT NOT NULL,
description TEXT,
priority TEXT DEFAULT 'normal', -- low | normal | high | urgent
status TEXT DEFAULT 'pending',-- pending | approved | rejected | in_progress | resolved
admin_note TEXT, -- komentarz administratora (widoczny u pracownika)
created_at TEXT NOT NULL,
updated_at TEXT,
resolved_at TEXT, -- ustawiane automatycznie przy resolved/rejected
resolved_by TEXT, -- ID admina który zamknął zgłoszenie
tenant_id TEXT DEFAULT 'default'
);
Obiady
Codzienne menu obiadowe z wyborem przez pracowników. Obiady są darmowe dla pracowników — koszty pokrywa firma, monitoring kosztów per restauracja. Admin zarządza menu w sekcji „Operacje → Obiady", pracownik (każda rola) wybiera w „Moje obiady".
Model danych — template'owy
- Menu = template z polami
name,validFrom,validTo,isActive. Items są pogrupowane per dzień tygodnia (1=pon..5=pt). - Tylko jedno menu może być aktywne naraz (
isActive=1). Pozostałe template'y mogą być archiwum / draftami / przygotowaniem na przyszłość. - Pozycje — nazwa, opis, cena (widoczna tylko adminowi/managerowi), tagi: wege/wegan/bezglut,
day_of_weekiposition. - Wybór pracownika — jedna pozycja/dzień. UI to widok kalendarzowy miesiąca z kolumną numeru tygodnia ISO 8601. Klik dnia → modal z items aktywnego menu dla tego
day_of_week. - Resolve: endpoint
GET /lunch/effective?from=&to=dla każdej daty w zakresie zwraca items z aktywnego menu (jeśli data jest dniem roboczym i mieści się wvalidFrom–validTo).
Cykl życia zamówienia
1. Admin tworzy/aktywuje menu (np. „Menu majowe", validFrom 01.05, validTo 31.05) 2. Pracownicy nawigują kalendarzem i wybierają pozycje (status: pending) 3. Cutoff (default: tego samego dnia 09:00) — blokada edycji 4. Cutoff + delay (default: +5 min) — cron auto-generuje order do restauracji - Pracownicy z urlopem dziennym → status cancelled_vacation - Pozostali → status sent 5. Admin kopiuje treść maila do schowka i wysyła do restauracji 6. Po miesiącu admin sprawdza monthly-summary i porównuje z fakturą
Reguły blokady wyboru
- 🚫 Weekend — UI omija (kalendarz pokazuje tylko Pn-Pt)
- 🚫 Po cutoffie — wybór i anulowanie zablokowane (zamówienie poszło)
- 🚫 Urlop dzienny (approved + days/sick/other) — UI nie pozwala wybierać; backend waliduje
- 🚫 Przed datą zatrudnienia (
startDate) — UI blokuje - 🚫 Poza maxDaysAhead — backend zwraca 400
- 🚫 Brak aktywnego menu / poza validFrom-validTo — komórki kalendarza puste („brak menu")
Urlop zatwierdzony PO cutoffie
Restauracja nie akceptuje odwołań po ustalonym terminie — firma płaci. System pokazuje admin'owi te przypadki w „Zamówieniach → daily-summary", można je rozliczyć poza systemem.
Aktywacja menu
Endpoint PATCH /lunch/menus/:id/activate w transakcji ustawia is_active=0 dla wszystkich menu, potem is_active=1 dla wybranego. Nie da się usunąć aktywnego menu (409) — najpierw /deactivate.
Edycja menu po wyborach
Jeśli edycja PUT usuwa pozycje na które są aktywne picks (pending) — backend zwraca 409 z licznikiem. Z force: true picks są oznaczone jako cancelled_admin z reason menu_item_removed.
Konfiguracja (Operacje → Obiady → Ustawienia)
lunchModuleEnabled— feature flag (cron przestaje generować orderzy)lunchCutoffTime— HH:MM (default 09:00)lunchCutoffDayOffset—0tego samego dnia (default),-1dzień wcześniej,1następnego dnialunchOrderDelayMinutes— odstęp między cutoff a wysyłką orderu (default 5)lunchRestaurantEmail— adres do wysyłki (na razie informacyjny — system tylko przygotowuje treść do skopiowania)lunchMaxDaysAhead— ile dni do przodu pracownik może wybierać (default 30)
Permissions
- superadmin / org_admin: pełen zarząd menu, ustawień, raportów (sekcja Operacje → Obiady widoczna)
- manager: tylko read aktywnego menu (do wybierania własnych obiadów)
- employee: read aktywnego menu, create/update własnych picks (sekcja Moje obiady)
Schema bazy (po migracji 003)
CREATE TABLE lunch_menus (
id, name, valid_from, valid_to, is_active,
created_at, created_by, updated_at, tenant_id
);
CREATE INDEX idx_lunch_menus_active ON lunch_menus(is_active, valid_from, valid_to);
CREATE TABLE lunch_menu_items (
id, menu_id, day_of_week, -- 1=pon, 2=wt, ..., 5=pt
name, description, price, position,
is_vegetarian, is_vegan, is_gluten_free
);
CREATE INDEX idx_lunch_menu_items_menu_dow
ON lunch_menu_items(menu_id, day_of_week, position);
CREATE TABLE lunch_orders (
id, employee_id, date, menu_item_id,
status, -- pending | sent | cancelled_user | cancelled_vacation | cancelled_admin
created_at, updated_at, cancelled_at, cancellation_reason, tenant_id
);
CREATE UNIQUE INDEX idx_lunch_orders_emp_date
ON lunch_orders(employee_id, date); -- max 1 pick / dzień / pracownik
CREATE TABLE lunch_restaurant_orders (
id, date, generated_at, total_amount,
items_json, -- snapshot pozycji co poszło + ilości + emaily
recipients, email_subject, email_body,
status, -- generated | sent | failed
sent_at, tenant_id
);
Podwyżki i premie
Automatyczny system obliczania podwyżek i premii rocznych na podstawie wyników finansowych.
Pula podwyżek
-- Wzrost przychodu Navifleet + Buildibox netGrowth = (przychód_obecny - przychód_poprzedni) × 2 spółki -- Pula podwyżkowa = 20% wzrostu raisePool = max(0, netGrowth) × 0.20 -- Podział puli równa część = raisePool × 50% (wszyscy uprawnieni, po równo) teamowa część = raisePool × 25% (według głosowania) zarząd = raisePool × 25% (ręcznie przez admina)
Pula bonusów
-- Zysk netto obu spółek × 20% bonusPool = (naviProfit + buildiProfit) × 0.20 -- Premia proporcjonalna do stażu w roku koefficient = dni_przepracowanych / 365 premiaOsobista = koefficient × (bonusPool / sumKoefficients)
Głosowanie
Każdy uprawniony pracownik wskazuje do 5 osób, które wg niego zasługują na teamową część puli. Wyniki są anonimowe — system zapisuje tylko sumy głosów per osoba.
Rok finansowy
- Dane wpisywane kwartalnie (Q1–Q4) lub rocznie
- Aktywny rekord = podstawa obliczeń
- Zamknięcie roku tworzy nowy rok i opcjonalnie resetuje głosowanie
Zgłoszenia (helpdesk)
Pływający przycisk pomocy na każdej stronie. Krótki opis + screen (Cmd+V), wątek czat-style, statusy lifecycle.
Entry point
Po zalogowaniu (admin lub pracownik) w prawym dolnym rogu pojawia się pomarańczowy FAB z ikoną „?". Klik otwiera modal z polami: Tytuł, Opis, Screen (paste z clipboardu lub upload). Auto-dołączamy meta (route, viewport, browser) — admin dostaje kontekst bez pytania.
4 statusy
- new — wpłynął, admin nie tknął
- in_progress — admin podjął
- waiting_user — admin czeka na pracownika
- resolved — zamknięty
Auto-przejścia: gdy admin pierwszy raz odpisze → in_progress. Druga wiadomość admina → waiting_user. Pracownik odpowiada → wraca in_progress. Status resolved ustawia tylko admin ręcznie.
Wątek
Każdy ticket ma listę wiadomości. Pracownik i admin piszą w tym samym wątku. Każda wiadomość może mieć attachment (max 1 screen, JPEG ≤2 MB po kompresji client-side). Auth-gated download — tylko właściciel ticketu i admin mogą pobrać plik.
Widoczność
- Pracownik: widzi własne zgłoszenia w sekcji „Pomoc"
- Admin (superadmin / org_admin): widzi wszystkie w tenancie w sekcji „Zgłoszenia"
- Manager: widzi tylko swoje (jak zwykły pracownik)
Schema bazy
support_tickets (id, employee_id, title, status, route, user_agent, viewport, ...) support_messages (id, ticket_id, author_employee_id, author_role, body, created_at) support_attachments (id, message_id, filename, mime, size)
Storage
Screeny zapisywane na dysku w uploads/support/<ticketId>/<uuid>.jpg — POZA bazą. Backupy DB (codzienne snapshoty) nie obejmują uploads/. To celowe — DB lekka, backupy szybkie.
Wnioski udziałowe (multi-sig governance)
Każda zmiana ownership_pct wymaga zatwierdzenia przez wszystkich pozostałych obecnych udziałowców (unanimous-minus-proposer). Bezpośrednia edycja jest zablokowana.
Po co to?
Bez tego mechanizmu superadmin (nawet bez udziałów) mógłby przyznać sobie ownership_pct, co zniekształciłoby pulę bonusów udziałowców. Każda zmiana = wniosek + głosowanie.
Cykl życia wniosku
- pending — utworzony, czeka na głosy
- applied — wszyscy pozostali udziałowcy zaaprobowali → zmiana wchodzi w życie
- rejected — pojedynczy reject zamyka wniosek (z opcjonalnym komentarzem)
- cancelled — wnioskodawca odwołał własny wniosek
- expired — 30 dni bez decyzji → auto-zamknięty
Bootstrap
Gdy SUM(ownership_pct) = 0 (nikt jeszcze nie ma udziałów), pierwsze przyznanie udziałów stosuje się natychmiast — nie ma kogo pytać. Po pojawieniu się pierwszego udziałowca każda kolejna zmiana idzie przez wniosek.
Lone shareholder
Gdy proposer jest jedynym udziałowcem (sam sobie zmienia %), brak innych głosujących → wniosek aplikuje się natychmiast.
Walidacja
- Suma
ownership_pctpo zastosowaniu wszystkich zmian:≤100% - Każda wartość:
0–100 - No-op (wszystkie zmiany == aktualnym wartościom): odrzucone
UI
- Admin → sekcja „Wnioski udziałowe" → przycisk „+ Nowy wniosek" otwiera modal z tabelą wszystkich pracowników i edytowalnymi % (live walidator sumy)
- Udziałowiec (rola: dowolna, byleby
ownership_pct > 0) → sekcja „Wnioski udziałowe" w portalu pracownika z badge'em pending głosów - Każdy wniosek pokazuje: zmiany (old → new + delta), progress (głosy zatwierdzone / wymagane), listę głosów z komentarzami, opcjonalny powód odrzucenia
Zasada „pełny admin nie omija governance"
Recovery account (X-Admin-Token) także musi przejść przez wniosek + głosowanie. Inaczej kradzież tokena admina = kradzież wszystkich udziałów.
Schema bazy
ownership_proposals (id, proposer_id, status, changes_json, expires_at, ...) ownership_proposal_votes (proposal_id, shareholder_id, ownership_pct_at_vote, vote, comment)
Panel admina — Pracownicy
Dodawanie pracowników
W zakładce Pracownicy wpisz imię i nazwisko, datę zatrudnienia, czy uczestniczy w podwyżkach i ewentualny procent udziałów. System automatycznie generuje token.
Token pracownika
6-znakowy kod (np. A3F8C1). Używany do:
- Logowania w widoku pracownika (strona główna)
- Autoryzacji API (
Authorization: Bearer A3F8C1) - Identyfikacji głosu w głosowaniu
Udziałowcy
Pracownicy z ownership_pct > 0 nie uczestniczą w puli zarządowej, ale otrzymują dywidendę z puli udziałowców (netProfit × 20% × procent_udziałów).
Reset hasła pracownika
Obok każdego pracownika w tabeli jest przycisk 🔑. Klik otwiera modal generujący tymczasowe hasło:
- Generator tworzy 12-znakowe hasło (wielkie/małe litery, cyfry, znaki specjalne) używając
crypto.getRandomValues(). Pomija znaki łatwe do pomylenia (0/O,1/l/I). Można wygenerować nowe przyciskiem ↻. - Po zapisie API ustawia hasło i flagę
must_change_password = 1. - Modal pokazuje hasło z przyciskiem 📋 Kopiuj — przekaż pracownikowi bezpiecznym kanałem (Signal, 1Password, w cztery oczy).
- Hasło jest pokazane raz — po zamknięciu okna nie odzyskasz go (admin musiałby zresetować ponownie).
Powiązane endpointy: POST /api/v1/employees/:id/reset-password · POST /api/v1/profile/password
Panel admina — Dane finansowe
Wprowadź dane finansowe dla każdego kwartału lub okresu. Możesz wpisać dane dla Navifleet i Buildibox osobno.
Pola dla każdego okresu
- Przychód obecny rok — YTD lub kwartalny
- Przychód poprzedni rok — ten sam okres rok wcześniej (porównanie)
- Zysk netto — podstawa puli bonusów
Aktywny rekord
Kliknij „Ustaw jako aktywny" przy wybranym kwartale — na jego podstawie zostaną wyliczone wszystkie pule. Możesz mieć wiele rekordów i przełączać między nimi.
Panel admina — Głosowanie
Kopie zapasowe
Auto-backup
Przy każdym starcie serwera (restart PM2, deploy) automatycznie tworzona jest kopia z etykietą autostart.
Ręczna kopia
Przycisk „Utwórz kopię teraz" w panelu admina → Kopie zapasowe. Etykieta manual.
Przywracanie
Kliknij „Przywróć" przy wybranej kopii. Serwer:
- Tworzy kopię zapasową aktualnego stanu (
pre-restore) - Flushuje WAL (
wal_checkpoint(TRUNCATE)) - Zastępuje plik bazy danych
- Wykonuje
process.exit(0)— PM2 restartuje proces automatycznie
db/backups/.
Nazewnictwo plików
kokpit-{etykieta}-{YYYY-MM-DDTHH-MM-SS}.db
Przykłady:
kokpit-autostart-2026-05-01T19-15-41.db
kokpit-manual-2026-05-01T20-00-00.db
kokpit-pre-restore-2026-05-01T21-30-00.db
REST API v1
Pełna dokumentacja endpointów dostępna na osobnej stronie:
Grupy endpointów
Autoryzacja
Dwa systemy tokenów
X-Admin-Token: 65fef60e5ccb58cd...
64-znakowy hex. Generowany przy każdym logowaniu admina. Przechowywany w settings.adminSessionToken. Daje dostęp superadmin do wszystkich tras.
Authorization: Bearer A3F8C1
6-znakowy kod przypisany do pracownika. Zakres danych zależy od roli i przypisanego działu. Token jest case-insensitive (automatycznie konwertowany do uppercase).
Dual-auth w v1
Middleware empAuth akceptuje oba typy tokenów. Jeśli wykryje poprawny X-Admin-Token — tworzy wirtualny kontekst superadmin z tenantId = 'default'. Dzięki temu panel admina może używać v1 API bez osobnego konta pracowniczego.
// empAuth — uproszczona logika if (adminToken && adminToken === getSetting('adminSessionToken')) { req.employee = { role: 'superadmin', tenantId: 'default', ... }; return next(); } // lub Bearer token pracownika const row = db.prepare('SELECT * FROM employees WHERE token = ?').get(token); req.employee = dbEmpToJs(row); // zawiera: role, tenantId, departmentId
Odpowiedź przy braku uprawnień
// 403 — niewystarczająca rola { "error": "Brak uprawnień", "detail": "Rola \"employee\" nie może wykonać \"approve\" w module \"vacations\"" } // 403 — moduł wyłączony { "error": "Moduł \"waste\" nie jest aktywny w Twojej organizacji" }
Anti-escalation (security pattern)
org_admin nie może modyfikować superadmina w żadnym aspekcie — nawet jeśli adminAuth go przepuści. Zaimplementowane jako deklaratywne middleware requireNotEscalating():
// Wpinasz po adminAuth/empAuth — middleware ładuje target employee, // sprawdza eskalację, ustawia req.targetEmployee dla handlera v1.post('/employees/:id/reset-password', adminAuth, requireNotEscalating(), handler); v1.put('/employees/:id', adminAuth, requireNotEscalating(), handler); v1.delete('/employees/:id', adminAuth, requireNotEscalating(), handler); v1.patch('/employees/:id/role', empAuth, requireRole(...), requireNotEscalating(), handler); v1.patch('/employees/:id/department', empAuth, requireRole(...), requireNotEscalating(), handler);
Zwraca 403 z komunikatem "org_admin nie może modyfikować superadmina". Pattern jest opt-in — kolejny developer dorzucając endpoint /employees/:id/... po prostu wpina middleware. Brak ryzyka zapomnienia o ręcznym sprawdzeniu.
Ownership_pct — analogicznie zablokowane: zmiana udziałów wymaga multi-sig governance przez /ownership-proposals. Bezpośredni PUT/bulk zwracają 409 lub silent-skip.
API keys (integracje zewnętrzne)
Trzeci kanał auth (obok X-Admin-Token i Bearer pracownika). Dla mikrousług partnerskich które nie powinny mieć tożsamości żywego pracownika.
Po co osobno od user-tokenów?
- Pracownik odchodzi z firmy → user token przepada → ich serwis pada. API key żyje niezależnie.
- Audit log pokazuje „Klucz partner-HR zmienił X" zamiast „Anna zmieniła X" (bo to ich serwis udaje Annę).
- Wąskie scope-y zamiast pełnej roli (
vacations:readbez dostępu do equipment). - Per-klucz rate limit i revoke jednym klikiem.
Format klucza
kk_live_aB3xY2<28 znaków>
Klucz zwracany TYLKO przy generowaniu (response z POST /api/v1/api-keys). W DB trzymamy sha256(key). Frontend pokazuje raz z przyciskiem „Kopiuj" — potem jest niedostępny. Przy podejrzeniu wycieku → revoke + nowy.
Header w requestach
X-API-Key: kk_live_aB3xY2…
Scope-y
Płaska lista moduł:akcja. Akcja: read lub write.
employees:read | employees:write vacations:read | vacations:write equipment:read | equipment:write lunch:read | lunch:write support:read | support:write waste:read | waste:write surveys:read | surveys:write departments:read | departments:write financials:read
Forbidden ops (zawsze 403, niezależnie od scope-a): financials:write, backups:manage, voting:approve, employees:delete, settings:manage. To operacje zarezerwowane dla zalogowanego użytkownika.
Rate limit
Token bucket per klucz. Default 60 req/min, configurable 1–6000 przy generowaniu. Przekroczenie → 429 z headerem Retry-After: 60.
Audit log
Każde wywołanie z api-key → wpis w api_audit_log (key_id, method, path, status, duration_ms, ip, ts). Retencja 30 dni (auto-cleanup codziennie). Admin widzi feed w panelu „Integracje" → zakładka „Audyt".
Gdzie zarządzać
Panel admina (superadmin / org_admin) → sekcja Integracje. Lista kluczy z statusem (Aktywny / Cofnięty / Wygasły), generowanie nowych z wyborem scope-ów + rate limit + opcjonalnej daty wygaśnięcia.
Polling vs webhooks
Aktualnie obsługujemy tylko polling przez GET /:moduł. Webhook delivery (real-time push do partnera) — planowane w v2 jeśli pojawi się use case. Poll jest prostszy, partnerzy automatycznie nadrabiają zaległości po awariach.
Schema bazy
api_keys (id, name, key_hash, scopes JSON, rate_limit, created_by, last_used_at, revoked_at, ...) api_audit_log (key_id, method, path, status, duration_ms, ip, ts)
Anti-eskalacja w Auth (related)
Niezależnie od kanału (user / admin / api-key), org_admin nie może modyfikować superadmin: reset hasła, edit, delete, role change, dept change → 403. Sprawdzane w 6 endpointach. UI ukrywa przyciski (🔑, „Usuń") na wierszu superadmina dla org_admina, mimo że backend i tak by zablokował.