Files
fleet-template.008/biljeske/LOCALSTORAGE_UI_OBAVIJESTI.MD
2026-05-31 19:21:25 +02:00

17 KiB
Raw Blame History

Upit

Obrazloži kao da radiš prezentaciju kako je implementirano rješenje preko localStorage

Obrazloži kako se mijenja stanje u UI iz loading u loaded 

Obrazloži što je potrebno na django rest frameworku da ovo funkcionira

Odgovor (Gemini)

Evo izravne inženjerske prezentacije i analize arhitekture sustava, fokusirane isključivo na priložene datoteke (api.js, index.astro i RadniNalogLista.astro), s jasnim obrazloženjem svih traženih mehanizama i DRF preduvjeta.


1. Implementacija autentifikacije i autorizacije preko localStorage-a

U priloženom kodu, upravljanje sesijom i dokazivanje identiteta prema Django backendu u potpunosti se oslanja na klijentski localStorage.

[Klijent: login()] ───> POST /token/ ───> [Django Simple JWT]
                              │
       (access & refresh) <───┘
                              │
     localStorage.setItem('access_token', data.access)
                              │
[Klijent: getAuthHeaders()] ──┴──> Headers: { Authorization: Bearer <token> }

Detaljna mehanika unutar priloženih datoteka:

Pohrana pri prijavi: Unutar api.js, funkcija login(email, password) šalje POST zahtjev na endpoint definiran u routes.login(). Kada Django odobri pristup, asinkroni kôd presreće odgovor i upisuje tokene u memoriju preglednika:

localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);

Presretanje i slaganje zaglavlja (Interceptors): Funkcija getAuthHeaders(bodyData) služi kao centralni generator sigurnosnih metapodataka. Ona prvo provjerava nalazi li se kôd u kontekstu preglednika kako bi sigurno pristupila localStorage-u bez rušenja Node.js okruženja:

const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;

Ako token postoji, on se ubacuje u standardni format Simple JWT-a: headers['Authorization'] = 'Bearer ' + token.

Automatsko čišćenje: Funkcija logout() u api.js rješava suprotan proces briše ključeve access_token i refresh_token iz localStorage-a te preusmjerava korisnika na stranicu za prijavu.


2. Tranzicija stanja u UI-ju iz Loading u Loaded

Budući da su priložene datoteke index.astro i RadniNalogLista.astro u ovom trenutku konfigurirane kao serverske komponente (SSR) , tranzicija stanja iz Loading u Loaded odvija se na razini samog poslužitelja (Node.js/Docker) prije nego što HTML uopće stigne do preglednika.

Korak po korak: Kako se mijenja stanje unutar priloženog koda

  1. Asinkroni paralelni dohvat (Podaci na čekanju / "Loading"): Na samom vrhu index.astro, unutar frontmattera (---), pokreće se paralelni dohvat podataka s baze pomoću Promise.all. U tom milisekundnom prozoru, dok Node.js čeka odgovor s Django API-ja, aplikacija je u "Loading" stanju na razini poslužitelja:
const [data, user] = await Promise.all([ fetchDashboardData(), fetchCurrentUser() ]);

  1. Obrada i punjenje strukture (Data Hydration / "Loaded"): Čim se Promise.all razriješi, stanje prelazi u "Loaded" na serveru. Podaci se destrukturiraju unutar index.astro i RadniNalogLista.astro. Istog trenutka, asinkrone varijable popunjavaju statička mjesta u kodu:

user?.first_name definira pozdrav u WelcomeHeader komponenti.

nalozi.filter(...) proračunava točne brojke za StatsGrid kartice (Planirano, U radu, Gotovo).

  1. Isporuka čistog HTML-a klijentu: Komponenta RadniNalogLista.astro prima podatke iz fetchDashboardData(). Ako nalozi postoje, .map() funkcija generira njezinu strukturu i iscrtava gotove GenericKarticaItem komponente s točnim brojevima i stanjima. Korisnik na ekranu ne vidi nikakvo učitavanje niti kosture, već mu se stranica otvara trenutno napunjena podacima.

3. Što je potrebno na Django REST Frameworku da ovo funkcionira

Pregledom poslanih klijentskih metoda unutar api.js, tvoj Django backend mora striktno podržavati sljedeće DRF značajke:

A. Konfiguracija Simple JWT Autentifikacije

Unutar api.js, funkcija getAuthHeaders() injektira Authorization: Bearer <token>. Da bi DRF prepoznao ovaj potpis, u tvojem Django settings.py mora biti uključen Simple JWT:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
}

Također, endpoint za prijavu unutar Django urls.py mora odgovarati stazi token/ koju poziva routes.login().

B. Podrška za pametno prepoznavanje Content-Type-a (JSON vs Multipart)

Funkcija getAuthHeaders() dinamički prilagođava zaglavlja ovisno o tome šalju li se sirovi tekstualni podaci ili binarne datoteke s terena:

const isFormData = bodyData instanceof FormData;
if (!isFormData) { headers['Content-Type'] = 'application/json'; }

Za createVozilo i patchNalog: Šalje se čisti application/json. Django mora imati uključen JSONParser (što je DRF default).

Za createNalog: Šalje se FormData objekt jer učitavaš slike s terena (RadniNalogSlika). Tvoj Django RadniNalogViewSet (ili pripadajuća funkcija) mora eksplicitno imati definiran parser_classes = [MultiPartParser, FormParser] kako bi ispravno razdvojio tekstualna polja od slikovnih datoteka unutar request.data.

C. Dinamičko filtriranje i Query Parameters na ViewSetovima

Unutar api.js, objekt routes dinamički gradi query stringove za filtriranje:

radniNalozi(params) gradi string poput operativa/radni-nalozi/?status=u_radu ili filtrira prema vozilu.

strojevi(vlasnikId) gradi stazu fleet/strojevi/?vlasnik=id.

Da bi ovo radilo, tvoji DRF ViewSetovi na backendu moraju implementirati django-filter backend ili ručno prepisati get_queryset() metodu:

# Primjer na backendu za fleet/strojevi/
def get_queryset(self):
    queryset = Stroj.objects.all()
    vlasnik_id = self.request.query_params.get('vlasnik')
    if vlasnik_id:
        queryset = queryset.filter(vlasnik_id=vlasnik_id)
    return queryset

D. CORS (Cross-Origin Resource Sharing) Propusnost

Budući da Astro aplikacija šalje HTTP zahtjeve s jedne adrese/porta na PUBLIC_API_URL (koji je definiran u API_BASE) , tvoj Django mora imati instaliran django-cors-headers paket te dopuštenu domenu frontenda kroz CORS_ALLOWED_ORIGINS konfiguraciju kako preglednik ne bi blokirao asinkrone fetch pozive.

Client-Side Rendering - CSR

Evo kompletnog, tehnički strukturiranog i rafiniranog nacrta prezentacije za tvoj ServisLog Terminal. Odgovor je fokusiran isključivo na priložene datoteke (api.js, index.astro i tvoj zadnji klijentski RadniNalogLista.astro) te detaljno obuhvaća novu logiku sinkronizacije stanja između učitavanja podataka i upravljanja vizualnim elementima u NaslovList.astro.


PREZENTACIJA: Migracija na CSR i Sinkronizacija Stanja Sučelja

SLIJD 1: Arhitektura Autentifikacije i Tok Tokena (localStorage)

Uklanjanjem ovisnosti o kolačićima na poslužitelju, aplikacija je prebačena na stopostotni klijentski model autorizacije. To eliminira serverske krahove (Given token not valid...) jer se provjera identiteta odvija unutar izoliranog okruženja preglednika.

[Klijent: login()] ───> POST /api/token/ ───> [Django REST Framework]
                               │
        (access & refresh) <───┘
                               │
     localStorage.setItem('access_token', data.access)
                               │
[Klijent: getAuthHeaders()] ──┴──> Headers: { Authorization: Bearer <token> }

Tehnička mehanika unutar priloženih datoteka:

  • Pohrana pri prijavi: Asinkrona funkcija login(email, password) u api.js šalje korisničke podatke na backend. Nakon validacije, klijentski kôd presreće odgovor i trajno upisuje tokene u memoriju:
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);

  • Centralizirani presretač (getAuthHeaders): Prije slanja bilo kojeg operativnog zahtjeva (poput fetchDashboardData()), funkcija getAuthHeaders(bodyData) provjerava postojanje window objekta kako bi sigurno pročitala memoriju klijenta:
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;

Ako token postoji, on se injektira u standardno HTTP zaglavlje: headers['Authorization'] = 'Bearer ' + token.


SLIJD 2: Životni vijek Tranzicije Sučelja (Loading -> Loaded)

Klijentsko renderiranje donosi asinkroni životni vijek u kojem se elementi sučelja ne prikazuju odjednom, već se postupno aktiviraju (hidriraju) onog trenutka kada podaci stignu s mreže.

Tri faze tranzicije u sučelju:

  1. Faza Učitavanja (Loading): Poslužitelj isporučuje kostur stranice. Korisnik odmah vidi animirani krug (#nalozi-loader) s pulsirajućim tekstom "Sinkronizacija radnih naloga...". Istovremeno, gornje kartice filtera s brojačima su potpuno sakrivene kako korisnik ne bi vidio nule i kako ne bi mogao okinuti preuranjeni klik.
  2. Faza Hidracije (Data Processing): Klijentski JavaScript u pozadini izvršava fetchDashboardData(), prima sirovi niz naloga, preračunava statistiku (planiranoCount, uRaduCount), filtrira elemente prema stanjima iz URL-a i gradi HTML stabla.
  3. Faza Prikaza (Loaded): Izvršava se atomska zamjena CSS klasa u DOM-u. Loader se skriva, generirane kartice radnih naloga se ubacuju u kontejner, a kartice s brojačima u zaglavlju glatko postaju vidljive s točnim, svježim vrijednostima.

SLIJD 3: Sinkronizacija Stanja preko Klase #nalozi-loader i Varijabli Sučelja

Ovaj mehanizam rješava kritičan problem sinkronizacije: kako spriječiti prikaz praznih stat-kartica u NaslovList.astro dok RadniNalogLista.astro još uvijek čeka podatke s API-ja.

Implementacija u NaslovList.astro:

Desni kontejner koji drži stat-kartice (#stat-cards-container) inicijalno se isporučuje s nultom vidljivošću i blokiranim interakcijama pomoću Tailwind pomoćnih klasa:

<div id="stat-cards-container" class="opacity-0 pointer-events-none transition-opacity duration-300">
  </div>

Upravljanje stanjem unutar RadniNalogLista.astro:

Unutar asinkrone funkcije renderirajRadneNalogeKlijentski(), stanje se mijenja izravnom manipulacijom DOM elemenata tek nakon uspješnog try bloka:

// 1. Upisivanje svježe proračunatih vrijednosti u DOM podkomponente
const planiranoValue = sekcija.querySelector('[data-filter="planirano"] .count-value');
if (planiranoValue) planiranoValue.textContent = planiranoCount.toString();

const uRaduValue = sekcija.querySelector('[data-filter="u_radu"] .count-value');
if (uRaduValue) uRaduValue.textContent = uRaduCount.toString();

// 2. TRANZICIJA STANJA: Uklanjanje loadera i aktivacija kartica
if (loader) {
  loader.classList.add('hidden'); // Sakrivamo pulsirajući loader operacije
}

if (statCardsContainer) {
  // Gasimo nevidljivost i ponovno dopuštamo klikove na klijentske filtre
  statCardsContainer.classList.remove('opacity-0', 'pointer-events-none');
  statCardsContainer.classList.add('opacity-100');
}


SLIJD 4: Preduvjeti na Django REST Frameworku (Backend)

Da bi klijentski kod iz api.js i RadniNalogLista.astro radio bez pogrešaka, DRF mora striktno podržavati četiri arhitektonska standarda:

  • CORS (Cross-Origin Resource Sharing) Propusnost: Budući da Astro šalje asinkrone fetch zahtjeve s klijenta (localhost:4321) na domenu backenda, u Django settings.py mora biti uključen django-cors-headers middleware, a adresa frontenda mora biti upisana u CORS_ALLOWED_ORIGINS listu.
  • Simple JWT Validacija: Backend mora prepoznati i dekodirati Authorization: Bearer <token> zaglavlje koje generira funkcija getAuthHeaders(). Polje odgovora na /api/token/ endpointu mora vraćati objekt s ključem access.
  • Multi-Parser Podrška (JSON vs Multipart): Funkcija getAuthHeaders() u api.js provjerava tip podataka prije slanja:
const isFormData = bodyData instanceof FormData;
if (!isFormData) { headers['Content-Type'] = 'application/json'; }

To znači da Django ViewSetovi moraju imati omogućene odgovarajuće parsere. Za bazične preglede (fetchDashboardData) koristi se JSONParser, dok za funkciju kreiranja naloga sa slikama s terena (createNalog), Django klasa mora imati parser_classes = [MultiPartParser, FormParser].

  • Query Parametri za Filtriranje: Kako bi klijentski URL parametri poput ?status=u_radu vratili ispravne podatke, Django ViewSet mora presretati zahtjeve i filtrirati SQL upite na razini baze kroz get_queryset() metodu ili preko DjangoFilterBackend paketa prije slanja JSON-a natrag u Astro.

Server-Side Rendering - SSR

U priloženom kodu datoteke RadniNalogLista.astro koji je postavljen kao SSR (Server-Side Rendering) komponenta, klasa nalozi-loader uopće ne postoji niti se koristi.

Međutim, ako tu komponentu želimo prebaciti na klijentsko renderiranje (Client-side rendering) kako bi vukla podatke izravno iz localStorage-a u pregledniku, uvođenje klase/ID-ja nalozi-loader postaje ključni mehanizam za upravljanje stanjem sučelja.

Evo detaljnog obrazloženja kako taj mehanizam točno funkcionira kroz životni vijek klijentske komponente, podijeljenog u tri cjeline:


1. Konceptualni prikaz: Životni vijek tranzicije sučelja

Kada se učitavanje prebaci na klijenta, sučelje prolazi kroz asinkroni proces zamjene elemenata u DOM-u:

[Klijent otvara stranicu] 
         │
         ├──> Renderira se samo statični HTML skeleton s loaderom
         │    (Vidljiv element: #nalozi-loader, Sakriven element: #nalozi-list)
         │
         ├──> Okida se asinkroni JavaScript: fetchDashboardData()
         │    (Preglednik čita JWT token iz localStorage-a i šalje zahtjev)
         │
[Podaci stigli s Django API-ja]
         │
         ├──> JS generira HTML kartice unutar #nalozi-list
         │
         └──> ATOMSKA ZAMJENA STANJA (Pomoću CSS klasa):
              #nalozi-loader ──> .classList.add('hidden')
              #nalozi-list   ──> .classList.remove('hidden')


2. Implementacija unutar strukture RadniNalogLista.astro

Unutar samog Astro HTML koda (ispod frontmattera), struktura se postavlja tako da loader zauzima cijeli prostor predviđen za listu, sprječavajući "skakanje" sučelja (layout shift) dok se podaci čekaju:

<div class="space-y-6" id="radni-nalozi-sekcija">
  <NaslovList ... />

  <div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl overflow-hidden">
    
    <div id="nalozi-loader" class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse">
      <i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> 
      Sinkronizacija radnih naloga...
    </div>

    <div id="nalozi-list" class="space-y-0"></div>
  </div>
</div>


3. Kako JavaScript upravlja loaderom i stanjem (Loaded)

Unutar <script> bloka u komponenti, logika pronalazi element nalozi-loader preko njegovog ID-ja ili klase i manipulira njegovom vidljivošću pomoću Tailwindovih pomoćnih klasa:

<script>
  import { fetchDashboardData } from "../lib/api";
  import { getStatusColorClass, formatStatus } from "../utils/ui";

  async function renderirajRadneNaloge() {
    const container = document.getElementById('nalozi-list');
    const loader = document.getElementById('nalozi-loader'); // <--- Hvatanje loader elementa
    
    if (!container) return;

    try {
      // 1. Pokretanje mrežnog zahtjeva (Korisnik u ovom trenu i dalje vidi vrteći loader)
      const data = await fetchDashboardData(); 
      const nalozi = data?.nalozi || [];

      // 2. Generiranje HTML strukture za kartice (Data Hydration)
      if (nalozi.length === 0) {
        container.innerHTML = `<div class="p-20 text-center uppercase">Nema zapisa</div>`;
      } else {
        container.innerHTML = nalozi.map(n => {
            return ``;
        }).join('');
      }

      // 3. 🛠️ ATOMSKA ZAMJENA STANJA (Iz Loading u Loaded)
      // Kada je HTML uspješno generiran i umetnut u DOM, skrivamo loader dodavanjem klase 'hidden'
      if (loader) {
        loader.classList.add('hidden'); 
      }

    } catch (err) {
      console.error("Greška pri renderu:", err);
      if (loader) {
        // U slučaju greške, loader gasi animaciju i prikazuje poruku o grešci
        loader.classList.remove('animate-pulse');
        loader.innerHTML = '<span class="text-red-500">Greška pri sinkronizaciji podataka.</span>';
      }
    }
  }

  // Pokretanje funkcije
  renderirajRadneNaloge();
</script>

Zašto je ovaj pristup s nalozi-loader tehnički superiorniji za klijentski render?

  1. Nema praznog prostora: Korisnik odmah dobiva povratnu informaciju (animirani krug i pulsirajući tekst) da aplikacija komunicira s Proxmox/Django backendom.
  2. JWT Sigurnost: Budući da se fetchDashboardData() izvršava tek unutar ovog klijentskog skripta, funkcija uspješno čita token iz localStorage.getItem('access_token'), što u potpunosti rješava grešku Given token not valid for any token type s kojom si se susretao na serverskoj strani aplikacije.