Tato stranka obsahuje vsetky klucove diagramy CVRN v poradi 01 -> 10.
Pri kazdom diagrame je:
- kratky popis,
- zdrojovy PlantUML kod,
- renderovany PNG obrazok (kliknutim otvoris vacsi nahlad).
01 System Context
Vysokourovnovy system context: host routing, hlavni akteri a napojenie na Supabase.
PlantUML kod
@startuml
title CVRN - System Context (Apex + App Hosts)
left to right direction
skinparam shadowing false
skinparam defaultFontName Monospaced
actor "Navstevnik" as Visitor
actor "Clen komunity" as Member
actor "Admin" as Admin
cloud "DNS + Vercel Edge" as Edge
rectangle "CVRN Next.js app\n(single repo, two hosts)" as NextApp {
rectangle "Middleware\n(host routing + auth gates)" as Middleware
rectangle "Landing host: cvrn.sk\n/landing, /log, /ciele,\n/amnestia, /contact" as Landing
rectangle "App host: app.cvrn.sk\n/login, /, /stats, /admin,\n/changelog, /auth/callback" as App
}
rectangle "Supabase Auth\nGoogle OAuth" as Auth
database "Supabase Postgres\nRLS + RPC" as DB
rectangle "Support\ninfo@cvrn.sk" as Support
Visitor --> Edge : Open cvrn.sk
Member --> Edge : Open app.cvrn.sk
Admin --> Edge : Open app.cvrn.sk/admin
Edge --> Middleware : Request + host header
Middleware --> Landing : Rewrite '/' -> '/landing'\nor serve landing routes
Middleware --> App : Redirect app-only routes\nfrom cvrn.sk to app.cvrn.sk
App --> Auth : signInWithOAuth + callback exchange
App --> DB : rent/return/admin/stats RPC
Middleware --> DB : is_allowed(), is_admin()
Landing --> App : CTA "Pozicaj si bicykel!" -> /login
Member --> Support : Nahlasenie problemu
Admin --> Support : Operativna komunikacia
note bottom of Middleware
Static assets are skipped by matcher
(.*\\..*) to keep /public files reachable.
end note
@endumlRenderovany diagram (PNG)

02 Auth Access Flow
Autentifikacia, autorizacia a vstupne guardy medzi user/admin castou aplikacie.
PlantUML kod
@startuml
title CVRN - Middleware Auth and Access Flow
skinparam shadowing false
skinparam defaultFontName Monospaced
start
:HTTP request na route;
:Normalize host + pathname;
if (Host je cvrn.sk/www.cvrn.sk?) then (ano)
if (Path == / ?) then (ano)
:Rewrite na /landing;
stop
endif
if (Path je app-only?\n/login, /stats, /admin,\n/auth/callback, /changelog, /api) then (ano)
:Redirect na https://app.cvrn.sk + path;
stop
endif
:Povolit landing route;
stop
endif
:Init Supabase middleware client;
if (Init zlyha?) then (ano)
if (Public path?) then (ano)
:Povolit request bez auth checku;
stop
else (nie)
:Redirect /login?error=...;
stop
endif
endif
:Nacitaj session usera;
if (User existuje?) then (nie)
if (Public path?) then (ano)
:Povolit request;
stop
else (nie)
:Redirect /login;
stop
endif
endif
if (Path == /login?) then (ano)
:Redirect /;
stop
endif
if (Public path?) then (ano)
:Povolit request;
stop
endif
:RPC is_allowed();
if (Allowed?) then (nie)
:Redirect /not-authorized;
stop
endif
if (Path zacina /admin?) then (ano)
:RPC is_admin();
if (Admin?) then (nie)
:Redirect /;
stop
endif
endif
:Povolit request;
stop
@endumlRenderovany diagram (PNG)

03 Rent Return Sequence
Sekvencny flow pozicania a vratenia bicykla vratane validacii a zapisov do DB.
PlantUML kod
@startuml
title CVRN - Rent and Return Sequence (Transition Engine)
skinparam shadowing false
skinparam defaultFontName Monospaced
actor "Clen komunity" as Member
participant "UI (/)" as UI
participant "Server Action" as Action
participant "RPC wrapper\nrent_bike / return_bike" as RpcWrapper
participant "Transition engine\nbike_transition_apply + guard" as Transition
database "Postgres\n(bikes, rentals)" as DB
== Rent bike ==
Member -> UI: Klik "Pozicat" na available bike
UI -> Action: rentBikeAction(bikeId)
Action -> RpcWrapper: rent_bike(bike_id)
RpcWrapper -> Transition: apply('user_rent', bike_id, auth.uid)
Transition -> DB: Guard checks\n(is_allowed, no active rental,\nbike status == available)
alt Rent uspesny
Transition -> DB: update bikes -> rented + current_user_id
Transition -> DB: insert rentals(user_id, bike_id)
RpcWrapper --> Action: id, name, secret_code
Action -> Action: revalidatePath('/'), revalidatePath('/stats')
Action --> UI: redirect '/?success=...'
else Rent zlyha
Transition --> RpcWrapper: error code\n(already_has_bike, bike_not_available, ...)
RpcWrapper --> Action: exception
Action --> UI: redirect '/?error=...'
end
== Return bike ==
Member -> UI: Submit "Vratit bicykel" (+ note optional)
UI -> Action: returnBikeAction(noteText)
Action -> RpcWrapper: return_bike(note_text)
RpcWrapper -> Transition: apply('user_return', null, auth.uid, meta)
Transition -> DB: Resolve current bike by current_user_id
Transition -> DB: Guard checks\n(is_allowed, bike rented by actor,\nactive rental exists)
alt Return uspesny
Transition -> DB: update bikes -> available + clear current_user_id
Transition -> DB: set issue_note if note is present
Transition -> DB: update active rental returned_at + return_note
RpcWrapper --> Action: void success
Action -> Action: revalidatePath('/'), revalidatePath('/stats')
Action --> UI: redirect '/?success=...'
else Return zlyha
Transition --> RpcWrapper: error code\n(no_active_bike, rental_not_found, ...)
RpcWrapper --> Action: exception
Action --> UI: redirect '/?error=...'
end
note over Transition,DB
bike_transition_guard/apply are internal-only functions.
execute grant is revoked from authenticated clients.
end note
@endumlRenderovany diagram (PNG)

04 Admin Flow
Admin procesy pre operativu: sprava bicyklov, stavov a servisnych zasahov.
PlantUML kod
@startuml
title CVRN - Admin Flow (Sidebar + RPC Mutations)
skinparam shadowing false
skinparam defaultFontName Monospaced
start
:Request na /admin;
:RPC is_admin();
if (Admin?) then (nie)
:Redirect /;
stop
endif
fork
:Load bikes\n(select id,name,status,secret_code,issue_note);
fork again
:Load allowlist\n(select allowed_emails);
fork again
:Load rentals history\n(rpc admin_list_rentals(limit=200));
fork again
:Load admin operations\n(rpc admin_list_operations(limit=200));
end fork
:Render full-width admin page;
:Render optional left sidebar\n(sekcie + active section highlight);
if (Akcia?) then (Allowlist add/remove)
:Insert/Delete allowed_emails;
if (DB error?) then (ano)
:Redirect /admin?error=...;
else (nie)
:revalidatePath('/admin');
:Redirect /admin?success=...;
endif
elseif (Bike code update)
:admin_update_bike_code(bike_id, new_secret_code);
:Write bike_code_history + admin_operations;
if (RPC error?) then (ano)
:Redirect /admin?error=...;
else (nie)
:revalidatePath('/admin');
:Redirect /admin?success=...;
endif
elseif (Bike status change)
:admin_set_bike_status(bike_id, available|service);
:Transition guard blocks rented bikes;
:Audit row in admin_operations;
if (RPC error?) then (ano)
:Redirect /admin?error=...;
else (nie)
:revalidatePath('/admin');
:Redirect /admin?success=...;
endif
elseif (Clear bike note)
:admin_clear_bike_note(bike_id);
:Audit row in admin_operations;
if (RPC error?) then (ano)
:Redirect /admin?error=...;
else (nie)
:revalidatePath('/admin');
:Redirect /admin?success=...;
endif
elseif (Reveal code history)
:POST /api/admin/reveal-bike-code-history\n(bikeId + PUK);
:RPC admin_reveal_bike_code_history(...);
:Success/fail is logged to admin_operations;
else (No action)
:Observe admin dashboard;
endif
stop
@endumlRenderovany diagram (PNG)

05 Stats Data Flow
Datovy tok pre komunitne statistiky, agregacie a zobrazenie na route /stats.
PlantUML kod
@startuml
title CVRN - Stats Data Flow (Month Switcher)
skinparam shadowing false
skinparam defaultFontName Monospaced
actor "Allowed user" as User
participant "Stats page\n/src/app/stats/page.tsx" as StatsPage
participant "MonthSelect\n(client)" as MonthSelect
participant "Supabase RPC layer" as RPC
database "Postgres\n(rentals, bikes, profiles)" as DB
User -> StatsPage: Open /stats?month=YYYY-MM
StatsPage -> RPC: auth.getUser() + is_allowed()
alt No session
StatsPage --> User: redirect /login
else Not allowed
StatsPage --> User: redirect /
else Allowed
StatsPage -> RPC: stats_get_available_months(tz)
RPC -> DB: First month with rentals .. current month
DB --> RPC: month_start[] (DESC)
RPC --> StatsPage: options for Select
StatsPage -> StatsPage: Validate month query\n(invalid/missing -> current month)
StatsPage -> StatsPage: month_start = YYYY-MM-01
par Daily chart
StatsPage -> RPC: stats_get_monthly_daily_rentals(month_start, tz)
RPC -> DB: Aggregate rentals by day in selected month
DB --> RPC: day + rentals_count
RPC --> StatsPage: daily rows
else Bike chart
StatsPage -> RPC: stats_get_monthly_rentals_by_bike(month_start, tz)
RPC -> DB: Aggregate rentals per bike in selected month
DB --> RPC: bike_id + bike_name + rentals_count
RPC --> StatsPage: bike rows
else Cyclist chart
StatsPage -> RPC: stats_get_monthly_rentals_by_cyclist(month_start, tz)
RPC -> DB: Aggregate rentals per cyclist in selected month
DB --> RPC: cyclist_id + cyclist_name + rentals_count
RPC --> StatsPage: cyclist rows
end
StatsPage -> StatsPage: monthTotal = sum(daily rows)
StatsPage --> User: Render month label + all charts for selected month
end
User -> MonthSelect: Change month option
MonthSelect -> MonthSelect: router.push(path?month=YYYY-MM)
MonthSelect -> StatsPage: Server rerender with new month
note over StatsPage
Current month is always included in options,
even when rentals_count is zero.
end note
@endumlRenderovany diagram (PNG)

06 DB ERD
Entitno-relacny model databazy CVRN a hlavne vazby medzi tabulkami.
PlantUML kod
@startuml
title CVRN - Database ERD (Current Schema)
skinparam shadowing false
skinparam defaultFontName Monospaced
hide methods
entity "profiles" as profiles {
* id : uuid <<PK>>
--
email : text
is_admin : boolean
created_at : timestamptz
}
entity "bikes" as bikes {
* id : bigint <<PK>>
--
name : text
status : text (available|rented|service)
secret_code : text
current_user_id : uuid <<FK nullable>>
issue_note : text <<nullable>>
issue_reported_at : timestamptz <<nullable>>
created_at : timestamptz
updated_at : timestamptz
}
entity "rentals" as rentals {
* id : bigint <<PK>>
--
user_id : uuid <<FK>>
bike_id : bigint <<FK>>
rented_at : timestamptz
returned_at : timestamptz <<nullable>>
return_note : text <<nullable>>
}
entity "allowed_emails" as allowed_emails {
* email : text <<PK>>
--
added_at : timestamptz
added_by : uuid <<FK nullable>>
}
entity "admin_operations" as admin_operations {
* id : bigint <<PK>>
--
action_type : text
bike_id : bigint <<FK nullable>>
actor_user_id : uuid <<FK nullable>>
actor_email : text
reason_text : text <<nullable>>
details : jsonb
created_at : timestamptz
}
entity "bike_code_history" as bike_code_history {
* id : bigint <<PK>>
--
bike_id : bigint <<FK>>
old_code : text
changed_by : uuid <<FK nullable>>
changed_by_email : text
changed_at : timestamptz
}
entity "admin_puk_config" as admin_puk_config {
* id : boolean <<PK, fixed true>>
--
puk_hash : text
updated_at : timestamptz
updated_by : uuid <<FK nullable>>
}
profiles ||--o{ rentals : user_id
bikes ||--o{ rentals : bike_id
profiles ||--o{ bikes : current_user_id
profiles ||--o{ allowed_emails : added_by
profiles ||--o{ admin_operations : actor_user_id
bikes ||--o{ admin_operations : bike_id
profiles ||--o{ bike_code_history : changed_by
bikes ||--o{ bike_code_history : bike_id
profiles ||--o{ admin_puk_config : updated_by
note right of rentals
Unique active constraints:
- one active rental per user
- one active rental per bike
(returned_at IS NULL)
end note
note right of bikes
Direct table reads/updates are admin-only (RLS).
Member flows use SECURITY DEFINER RPC functions.
end note
@endumlRenderovany diagram (PNG)

07 Bike State Machine
Stavovy model bicykla a povolene prechody medzi stavmi v prevadzke.
PlantUML kod
@startuml
title CVRN - Bike State Machine (Guard-driven)
skinparam shadowing false
skinparam defaultFontName Monospaced
state "Bike status" as BikeStatus {
[*] --> Available
Available --> Rented : user_rent\n(rent_bike)
Rented --> Available : user_return\n(return_bike)
Available --> Service : admin_set_service
Service --> Available : admin_set_available
Available --> Available : admin_update_code\n(no status change)
Service --> Service : admin_update_code\n(no status change)
}
state "Issue note flag" as IssueFlag {
[*] --> NoIssue
NoIssue --> HasIssue : user_return(note_text != empty)
HasIssue --> NoIssue : admin_clear_note
HasIssue --> HasIssue : user_return(note_text != empty)\n(overwrite with latest note)
}
note right of BikeStatus
Guard rules in bike_transition_guard:
- user_rent allowed only from Available
- user_return allowed only from Rented by same actor
- admin cannot switch rented bike to service/available
- direct Rented -> Service transition is blocked
end note
note right of IssueFlag
issue_note is orthogonal to bike status.
Bike can stay Available + HasIssue.
end note
@endumlRenderovany diagram (PNG)

08 RPC Security Boundary
Bezpecnostna hranica pre RPC vrstvu, role a policy enforcement.
PlantUML kod
@startuml
title CVRN - RPC Security Boundary
skinparam shadowing false
skinparam defaultFontName Monospaced
actor "Authenticated user" as User
participant "Browser UI" as Browser
participant "Next.js server layer\n(Server Components + Actions + Route Handlers)" as Server
participant "Supabase API" as Supabase
database "Postgres\n(RLS + RPC + internal guard funcs)" as DB
== Public bike list (safe) ==
User -> Browser: Open "/"
Browser -> Server: Render HomePage
Server -> Supabase: rpc("list_bikes_public")
Supabase -> DB: list_bikes_public() checks is_allowed()
DB --> Supabase: id, name, status, issue_note\n(no secret_code)
Supabase --> Server: bikes[]
Server --> Browser: Render list without lock codes
== User's own bike code ==
User -> Browser: Has active rental
Browser -> Server: Render HomePage
Server -> Supabase: rpc("get_my_bike")
Supabase -> DB: get_my_bike() scoped to auth.uid()
DB --> Supabase: one row incl. secret_code
Supabase --> Server: my_bike
Server --> Browser: Render own secret_code
== Admin mutation with audit ==
User -> Browser: Admin updates bike status/code
Browser -> Server: Server Action on /admin
Server -> Supabase: rpc("admin_set_bike_status" / "admin_update_bike_code")
Supabase -> DB: admin RPC -> bike_transition_apply(...)
DB --> DB: write bikes + admin_operations\n(+ bike_code_history for code update)
Supabase --> Server: success/error
Server --> Browser: redirect + refreshed /admin
== Direct table read attempt (blocked) ==
User -> Browser: Try direct client query
Browser -> Supabase: from("bikes").select("secret_code")
Supabase -> DB: SELECT bikes.secret_code
DB --> Supabase: denied by RLS (non-admin)
Supabase --> Browser: error / no rows
note over DB
Security boundary summary:
- RLS allows direct bikes/rentals writes only for admin.
- member flows must go through SECURITY DEFINER RPC.
- bike_transition_guard/apply execute is revoked
from authenticated clients.
end note
@endumlRenderovany diagram (PNG)

09 User Journey
End-to-end user journey od vstupu do appky po uspesne ukoncenie jazdy.
PlantUML kod
@startuml
title CVRN - User Journey (Landing + App)
skinparam shadowing false
skinparam defaultFontName Monospaced
[*] --> Visitor
state "Visitor on cvrn.sk" as Visitor
state "Landing pages\n/landing, /log, /ciele,\n/amnestia, /contact" as Landing
state "Login\napp.cvrn.sk/login" as Login
state "OAuth callback\n/auth/callback" as Callback
state "Dashboard\n/" as Dashboard
state "Not authorized\n/not-authorized" as NotAuthorized
state "No bike view" as NoBike
state "Active ride view" as HasBike
state "Stats\n/stats?month=YYYY-MM" as Stats
state "Admin\n/admin" as Admin
Visitor --> Landing : open cvrn.sk
Landing --> Login : CTA "Pozicaj si bicykel!"
Login --> Callback : signInWithOAuth(google)
Callback --> Dashboard : exchangeCodeForSession success
Callback --> Login : callback error
Dashboard --> NotAuthorized : is_allowed = false
NotAuthorized --> Login : retry / sign in as different user
Dashboard --> NoBike : get_my_bike = none
Dashboard --> HasBike : get_my_bike = active bike
NoBike --> HasBike : rentBikeAction -> rpc rent_bike
HasBike --> NoBike : returnBikeAction -> rpc return_bike
Dashboard --> Stats : open /stats
Stats --> Stats : switch month via Select\n(URL month=YYYY-MM)
Stats --> Dashboard : back to /
Dashboard --> Admin : open /admin [is_admin]
Admin --> Dashboard : back to /
Dashboard --> Login : signOutAction
NoBike --> Login : signOutAction
HasBike --> Login : signOutAction
Stats --> Login : signOutAction
Admin --> Login : signOutAction
Login --> [*] : close flow
note right of Landing
App-only routes on cvrn.sk are redirected
by middleware to app.cvrn.sk.
end note
@endumlRenderovany diagram (PNG)

10 Host Routing Flow
Routing medzi cvrn.sk a app.cvrn.sk, presmerovania a middleware rozhodovanie.
PlantUML kod
@startuml
title CVRN - Host Routing Flow (cvrn.sk vs app.cvrn.sk)
skinparam shadowing false
skinparam defaultFontName Monospaced
start
:HTTP request;
:Read host (x-forwarded-host / host);
if (Host in {cvrn.sk, www.cvrn.sk}?) then (ano)
if (Path == / ?) then (ano)
:Rewrite to /landing;
stop
endif
if (Path is app-only?) then (ano)
:Redirect to https://app.cvrn.sk + same path/query;
stop
endif
:Serve landing content directly;
stop
else (nie)
:Treat as app host flow;
if (Path is static asset?) then (ano)
:Skip middleware (matcher excludes .*\\..*);
stop
endif
:Initialize Supabase middleware client;
if (Init/session check fails on protected path?) then (ano)
:Redirect /login (with error when needed);
stop
endif
:Apply app auth gates\n(user, is_allowed, is_admin for /admin);
:Serve app route;
stop
endif
@endumlRenderovany diagram (PNG)

On This Page
01 System ContextPlantUML kodRenderovany diagram (PNG)02 Auth Access FlowPlantUML kodRenderovany diagram (PNG)03 Rent Return SequencePlantUML kodRenderovany diagram (PNG)04 Admin FlowPlantUML kodRenderovany diagram (PNG)05 Stats Data FlowPlantUML kodRenderovany diagram (PNG)06 DB ERDPlantUML kodRenderovany diagram (PNG)07 Bike State MachinePlantUML kodRenderovany diagram (PNG)08 RPC Security BoundaryPlantUML kodRenderovany diagram (PNG)09 User JourneyPlantUML kodRenderovany diagram (PNG)10 Host Routing FlowPlantUML kodRenderovany diagram (PNG)