This commit is contained in:
Tomas Dvorak
2025-11-14 15:53:12 +01:00
parent f3db65d350
commit c941313fd5
149 changed files with 4366 additions and 12935 deletions
-29
View File
@@ -1,29 +0,0 @@
# Project Diagrams
This folder contains Mermaid diagrams for the project:
- ER Diagram of the database schema
- System Architecture (frontend ↔ backend ↔ integrations)
- Admin Module Map (grouped by navigation categories)
- Frontpage Data Map (sections → data sources)
## Recommended extensions (VS Code)
- Markdown Preview Mermaid Support (ID: bpruitt-goddard.vscode-mermaid-preview)
- Alternative: Markdown Preview Enhanced (ID: shd101wyy.markdown-preview-enhanced)
## How to preview
1) Install one of the extensions above.
2) Open any .md file here (e.g., er-diagram.md).
3) Press Ctrl+Shift+V (or Right click → Open Preview / Open Preview to the Side).
4) If prompted to allow scripts for Mermaid, accept.
## Files
- er-diagram.md — ER diagram of DB entities and relationships
- system-architecture.md — high-level system flow
- admin-map.md — map of admin sections
- frontpage-data-map.md — frontpage sections → data sources
## Optional: Export as images
- You can install Mermaid CLI to export to PNG/SVG: `npm i -g @mermaid-js/mermaid-cli`
- For Markdown files in this folder, run: `mmdc -i er-diagram.md -o er-diagram.svg --inputType markdown`
(If you extract the mermaid code to a standalone .mmd file, you can omit `--inputType`.)
-61
View File
@@ -1,61 +0,0 @@
# Admin Module Map
```mermaid
graph LR
subgraph Zakladni
ADASH[Nastenka]
AANALYT[Analytika]
end
subgraph Sport
TEAMS[Tymy]
MATCHES[Zapasy]
PLAYERS[Hraci]
ALIASES[Alias_soutezi]
SCORE[Tabule_Scoreboard]
SCORE_R[Scoreboard_Remote]
end
subgraph Obsah
ARTICLES[Clanky]
ACTIVITIES[Aktivity]
CATEGORIES[Kategorie]
COMMENTS[Komentare]
end
subgraph Media
VIDEOS[Videa]
GALLERY[Galerie]
FILES[Soubory]
BANNERS[Bannery]
end
subgraph Komunikace
MSGS[Zpravy]
NEWSLTR[Zpravodaj]
CONTACTS[Kontakty]
end
subgraph Marketing
SPONSORS[Sponzori]
MERCH[Obleceni]
POLLS[Ankety]
SWEEP[Souteze]
ENGAGE[Odmeny_a_Uspechy]
SHORT[Zkracene_odkazy]
end
subgraph Nastroje
PREFETCH[Prefetch_a_Cache]
ERRORS[Chyby]
DOCS[Dokumentace]
end
subgraph Nastaveni
SETTINGS[Nastaveni]
USERS[Uzivatele]
NAV[Navigace]
ABOUT[O_klubu]
end
ADASH --> Sport
ADASH --> Obsah
ADASH --> Media
ADASH --> Komunikace
ADASH --> Marketing
ADASH --> Nastroje
ADASH --> Nastaveni
```
-57
View File
@@ -1,57 +0,0 @@
graph LR
subgraph Zakladni
ADASH[Nastenka]
AANALYT[Analytika]
end
subgraph Sport
TEAMS[Tymy]
MATCHES[Zapasy]
PLAYERS[Hraci]
ALIASES[Alias_soutezi]
SCORE[Tabule_Scoreboard]
SCORE_R[Scoreboard_Remote]
end
subgraph Obsah
ARTICLES[Clanky]
ACTIVITIES[Aktivity]
CATEGORIES[Kategorie]
COMMENTS[Komentare]
end
subgraph Media
VIDEOS[Videa]
GALLERY[Galerie]
FILES[Soubory]
BANNERS[Bannery]
end
subgraph Komunikace
MSGS[Zpravy]
NEWSLTR[Zpravodaj]
CONTACTS[Kontakty]
end
subgraph Marketing
SPONSORS[Sponzori]
MERCH[Obleceni]
POLLS[Ankety]
SWEEP[Souteze]
ENGAGE[Odmeny_a_Uspechy]
SHORT[Zkracene_odkazy]
end
subgraph Nastroje
PREFETCH[Prefetch_a_Cache]
ERRORS[Chyby]
DOCS[Dokumentace]
end
subgraph Nastaveni
SETTINGS[Nastaveni]
USERS[Uzivatele]
NAV[Navigace]
ABOUT[O_klubu]
end
ADASH --> Sport
ADASH --> Obsah
ADASH --> Media
ADASH --> Komunikace
ADASH --> Marketing
ADASH --> Nastroje
ADASH --> Nastaveni
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

+420
View File
@@ -0,0 +1,420 @@
%%{init: {
'theme': 'base',
'securityLevel': 'loose',
'flowchart': { 'curve': 'basis' },
'themeVariables': {
'primaryColor': '#0b5cff',
'primaryTextColor': '#ffffff',
'lineColor': '#64748b',
'tertiaryColor': '#f8fafc',
'fontSize': '12px'
},
'themeCSS': '.edgePath path { stroke-dasharray: 5 5; animation: dash 24s linear infinite; } @keyframes dash { to { stroke-dashoffset: 1000; } } .cluster rect { rx:8; ry:8; }'
}}%%
flowchart TB
classDef route stroke:#2563eb,fill:#eff6ff,color:#1e3a8a,stroke-width:1px;
classDef page stroke:#0ea5e9,fill:#e0f7ff,color:#0b5cff,stroke-width:1px;
classDef layout stroke:#6b7280,fill:#f8fafc,color:#111827,stroke-width:1px;
classDef component stroke:#94a3b8,fill:#f1f5f9,color:#111827,stroke-width:1px;
classDef svc stroke:#22c55e,fill:#ecfdf5,color:#065f46,stroke-width:1px;
classDef adminsvc stroke:#16a34a,fill:#dcfce7,color:#064e3b,stroke-width:1px;
classDef hook stroke:#f97316,fill:#fff7ed,color:#9a3412,stroke-width:1px;
classDef ctx stroke:#8b5cf6,fill:#f5f3ff,color:#4c1d95,stroke-width:1px;
classDef guard stroke:#ef4444,fill:#fef2f2,color:#991b1b,stroke-width:1px;
classDef ext stroke:#a855f7,fill:#faf5ff,color:#6b21a8,stroke-width:1px;
classDef infra stroke:#0f766e,fill:#ecfeff,color:#155e75,stroke-width:1px;
classDef legacy stroke-dasharray:3 3,stroke:#9ca3af,fill:#fafafa,color:#6b7280;
%% Routing & Guards
subgraph ROUTING [Routing]
direction TB
pr_admin[ProtectedRoute requiredRole=admin]:::guard
pr_editor[ProtectedRoute requiredRole=editor]:::guard
subgraph ROUTES [Admin Routes]
direction TB
r_admin["/admin"]:::route
r_docs["/admin/docs"]:::route
r_about["/admin/o-klubu"]:::route
r_videos["/admin/videa"]:::route
r_gallery["/admin/galerie"]:::route
r_merch["/admin/obleceni"]:::route
r_sponsors["/admin/sponzori"]:::route
r_matches["/admin/zapasy"]:::route
r_players["/admin/hraci"]:::route
r_teams["/admin/tymy"]:::route
r_users["/admin/uzivatele"]:::route
r_banners["/admin/bannery"]:::route
r_messages["/admin/zpravy"]:::route
r_settings["/admin/nastaveni"]:::route
r_newsletter["/admin/newsletter"]:::route
r_polls["/admin/ankety"]:::route
r_aliases["/admin/aliasy-soutezi"]:::route
r_prefetch["/admin/prefetch"]:::route
r_reset["/admin/users/send-reset"]:::route
r_score["/admin/scoreboard"]:::route
r_score_remote["/admin/scoreboard/remote"]:::route
r_analytics["/admin/analytika"]:::route
r_shortlinks["/admin/shortlinks"]:::route
r_files["/admin/soubory"]:::route
r_contacts["/admin/kontakty"]:::route
r_nav["/admin/navigace"]:::route
r_comments["/admin/komentare"]:::route
r_engagement["/admin/engagement"]:::route
r_sweep["/admin/sweepstakes"]:::route
r_sweep_visual["/admin/sweepstakes/:id/visual"]:::route
r_articles["/admin/clanky"]:::route
r_activities["/admin/aktivity"]:::route
end
pr_admin --> r_admin
pr_admin --> r_docs
pr_admin --> r_about
pr_admin --> r_videos
pr_admin --> r_gallery
pr_admin --> r_merch
pr_admin --> r_sponsors
pr_admin --> r_matches
pr_admin --> r_players
pr_admin --> r_teams
pr_admin --> r_users
pr_admin --> r_banners
pr_admin --> r_messages
pr_admin --> r_settings
pr_admin --> r_newsletter
pr_admin --> r_polls
pr_admin --> r_aliases
pr_admin --> r_prefetch
pr_admin --> r_reset
pr_admin --> r_score
pr_admin --> r_score_remote
pr_admin --> r_analytics
pr_admin --> r_shortlinks
pr_admin --> r_files
pr_admin --> r_contacts
pr_admin --> r_nav
pr_admin --> r_comments
pr_admin --> r_engagement
pr_admin --> r_sweep
pr_admin --> r_sweep_visual
pr_editor --> r_articles
pr_editor --> r_activities
end
%% Admin UI Shell
subgraph UI [Admin UI Shell]
direction TB
layout[AdminLayout]:::layout
sidebar[AdminSidebar]:::component
header[AdminHeader]:::component
search[AdminSearchModal]:::component
support[AdminSupportButton]:::component
auth[AuthContext]:::ctx
ups[usePublicSettings]:::hook
layout --> sidebar
layout --> header
layout --> search
layout --> support
layout --> auth
layout --> ups
end
%% Common Admin Hooks
subgraph HOOKS [Common Admin Hooks]
direction TB
h_adminTable[useAdminTable]:::hook
h_autoSave[useAutoSave]:::hook
end
%% Admin Pages (each includes AdminLayout)
subgraph PAGES [Admin Pages]
direction TB
p_dashboard[AdminDashboardPage]:::page
p_docs[AdminDocsPage]:::page
p_about[AboutAdminPage]:::page
p_videos[AdminVideosPage]:::page
p_gallery[GalleryAdminPage]:::page
p_merch[AdminMerchPage]:::page
p_sponsors[SponsorsAdminPage]:::page
p_matches[MatchesAdminPage]:::page
p_players[PlayersAdminPage]:::page
p_teams[TeamsAdminPage]:::page
p_users[UsersAdminPage]:::page
p_banners[BannersAdminPage]:::page
p_messages[MessagesAdminPage]:::page
p_settings[SettingsAdminPage]:::page
p_newsletter[NewsletterAdminPage]:::page
p_polls[PollsAdminPage]:::page
p_aliases[CompetitionAliasesAdminPage]:::page
p_prefetch[PrefetchAdminPage]:::page
p_reset[AdminResetPasswordPage]:::page
p_score[ScoreboardAdminPage]:::page
p_score_remote[MobileScoreboardControlPage]:::page
p_analytics[AnalyticsAdminPage]:::page
p_shortlinks[ShortlinksAdminPage]:::page
p_files[FilesAdminPage]:::page
p_contacts[ContactsAdminPage]:::page
p_nav[NavigationAdminPage]:::page
p_comments[CommentsAdminPage]:::page
p_engagement[EngagementAdminPage]:::page
p_sweep[SweepstakesAdminPage]:::page
p_sweep_visual[SweepstakeVisualPage]:::page
p_articles[ArticlesAdminPage]:::page
p_activities[AdminActivitiesPage]:::page
%% Unrouted/Legacy admin pages present in codebase
p_devdocs[DevDocsPage]:::legacy
p_media[MediaAdminPage]:::legacy
p_standings[StandingsAdminPage]:::legacy
p_errors[ErrorsAdminPage]:::legacy
p_docs_old[AdminDocsPage_Old]:::legacy
p_dash_legacy[DashboardPage]:::legacy
end
%% Route -> Page wiring
r_admin --> p_dashboard
r_docs --> p_docs
r_about --> p_about
r_videos --> p_videos
r_gallery --> p_gallery
r_merch --> p_merch
r_sponsors --> p_sponsors
r_matches --> p_matches
r_players --> p_players
r_teams --> p_teams
r_users --> p_users
r_banners --> p_banners
r_messages --> p_messages
r_settings --> p_settings
r_newsletter --> p_newsletter
r_polls --> p_polls
r_aliases --> p_aliases
r_prefetch --> p_prefetch
r_reset --> p_reset
r_score --> p_score
r_score_remote --> p_score_remote
r_analytics --> p_analytics
r_shortlinks --> p_shortlinks
r_files --> p_files
r_contacts --> p_contacts
r_nav --> p_nav
r_comments --> p_comments
r_engagement --> p_engagement
r_sweep --> p_sweep
r_sweep_visual --> p_sweep_visual
r_articles --> p_articles
r_activities --> p_activities
%% Pages include AdminLayout
p_dashboard --> layout
p_docs --> layout
p_about --> layout
p_videos --> layout
p_gallery --> layout
p_merch --> layout
p_sponsors --> layout
p_matches --> layout
p_players --> layout
p_teams --> layout
p_users --> layout
p_banners --> layout
p_messages --> layout
p_settings --> layout
p_newsletter --> layout
p_polls --> layout
p_aliases --> layout
p_prefetch --> layout
p_reset --> layout
p_score --> layout
p_score_remote --> layout
p_analytics --> layout
p_shortlinks --> layout
p_files --> layout
p_contacts --> layout
p_nav --> layout
p_comments --> layout
p_engagement --> layout
p_sweep --> layout
p_sweep_visual --> layout
p_articles --> layout
p_activities --> layout
%% Services
subgraph SERVICES [Service Layer]
direction TB
s_api["api.ts (Axios + interceptors)"]:::infra
s_settings[settings.ts]:::svc
s_setup[setup.ts]:::svc
s_seo[seo.ts]:::svc
s_articles[articles.ts]:::svc
s_files[files.ts]:::svc
s_navigation[navigation.ts]:::svc
s_players[players.ts]:::svc
s_polls[polls.ts]:::svc
s_shortlinks[shortlinks.ts]:::svc
s_banners[banners.ts]:::svc
s_clothing[clothing.ts]:::svc
s_contacts[contactInfo.ts]:::svc
s_comments_pub[comments.ts]:::svc
s_events[eventService.ts]:::svc
s_image[imageProcessing.ts]:::svc
s_scoreboard[scoreboard.ts]:::svc
s_sweep[sweepstakes.ts]:::svc
s_youtube[youtube.ts]:::svc
s_zonerama[zonerama.ts]:::svc
s_analytics[analyticsService.ts]:::svc
s_errors[errors.ts]:::svc
s_facr_cache[facr/cache.ts]:::svc
s_facr_api[facr/facrApi.ts]:::svc
s_ai[ai.ts]:::svc
subgraph ADMIN_APIs [Admin API Modules]
direction TB
s_admin_comments[admin/comments.ts]:::adminsvc
s_admin_msgs[admin/contactMessages.ts]:::adminsvc
s_admin_eng[admin/engagement.ts]:::adminsvc
s_admin_news[admin/newsletter.ts]:::adminsvc
s_admin_prefetch[admin/prefetch.ts]:::adminsvc
s_admin_matches[adminMatches.ts]:::adminsvc
end
end
%% External/Integrations
subgraph EXTERNAL [External Systems]
direction TB
x_facr[FAČR API]:::ext
x_youtube[YouTube]:::ext
x_zonerama[Zonerama]:::ext
x_logoapi[logoapi.sportcreative.eu]:::ext
x_sportlogos[sportlogos.tdvorak.dev]:::ext
x_smtp[SMTP / Email]:::ext
x_errrev[Error Review Service]:::ext
end
%% Infra
s_api -->|interceptors, auth, csrf| s_api
%% Service <-> External mappings
s_youtube --> x_youtube
s_zonerama --> x_zonerama
s_facr_api --> x_facr
s_scoreboard --> x_logoapi
s_scoreboard --> x_sportlogos
s_admin_news --> x_smtp
s_settings --> x_errrev
%% Page -> Service dependencies
p_dashboard --> s_analytics
p_dashboard --> s_facr_cache
p_dashboard --> s_api
p_docs --> s_api
p_about --> s_settings
p_about --> s_articles
p_about --> s_api
p_about --> s_ai
p_videos --> s_settings
p_videos --> s_admin_prefetch
p_videos --> s_youtube
p_gallery --> s_api
p_gallery --> s_zonerama
p_merch --> s_clothing
p_sponsors --> s_sponsors
p_sponsors --> s_articles
p_matches --> s_admin_matches
p_matches --> s_settings
p_matches --> s_facr_cache
p_matches --> s_facr_api
p_matches --> s_comp_alias
p_players --> s_players
p_players --> s_image
p_players --> s_articles
p_teams --> s_admin_matches
p_teams --> s_image
p_teams --> s_facr_api
p_teams --> s_scoreboard
p_users --> s_admin_eng
p_users --> s_api
p_banners --> s_banners
p_banners --> s_articles
p_messages --> s_admin_msgs
p_settings --> s_settings
p_settings --> s_seo
p_settings --> s_admin_prefetch
p_settings --> s_articles
p_newsletter --> s_admin_news
p_newsletter --> s_settings
p_polls --> s_polls
p_polls --> s_categories
p_polls --> s_events
p_polls --> s_articles
p_polls --> s_players
p_aliases --> s_comp_alias
p_aliases --> s_api
p_prefetch --> s_admin_prefetch
p_prefetch --> s_api
p_reset --> s_api
p_score --> s_scoreboard
p_score --> s_sponsors
p_score_remote --> s_scoreboard
p_analytics --> s_analytics
p_shortlinks --> s_shortlinks
p_files --> s_files
p_files --> s_api
p_contacts --> s_contacts
p_contacts --> s_settings
p_contacts --> s_image
p_contacts --> s_facr_cache
p_nav --> s_navigation
p_comments --> s_admin_comments
p_comments --> s_comments_pub
p_comments --> s_articles
p_comments --> s_events
p_comments --> s_youtube
p_comments --> s_admin_eng
p_engagement --> s_admin_eng
p_sweep --> s_sweep
p_sweep --> s_articles
p_sweep_visual --> s_sweep
p_sweep_visual --> s_settings
p_articles --> s_articles
p_articles --> s_youtube
p_articles --> s_shortlinks
p_articles --> s_zonerama
p_articles --> s_settings
p_articles --> s_facr_api
p_articles --> s_events
p_articles --> h_autoSave
p_articles --> s_ai
p_activities --> s_events
p_activities --> s_articles
p_activities --> s_youtube
p_activities --> s_settings
p_activities --> s_shortlinks
p_activities --> s_facr_api
p_activities --> h_autoSave
%% Missing simple aliases for a few symbols referenced above
s_sponsors[sponsors.ts]:::svc
s_categories[categories.ts]:::svc
s_comp_alias[competitionAliases.ts]:::svc
%% Legacy pages light mappings
p_errors --> s_errors
p_errors --> s_settings
p_media --> s_files
p_media --> s_image
p_standings --> s_facr_cache
%% UI / Guards dependencies
sidebar --> s_navigation
sidebar --> s_events
sidebar --> s_settings
pr_admin --> s_setup
pr_editor --> s_setup
%% Notes:
%% - All admin pages render AdminLayout which composes Sidebar, Header, Search modal and Support button.
%% - Guards: routes under /admin require admin unless explicitly editor-accessible (/admin/clanky, /admin/aktivity).
%% - Services use Axios instance with interceptors (api.ts), many pages use React Query for data fetching.
+44
View File
@@ -0,0 +1,44 @@
%%{init: {
'theme': 'neutral'
}}%%
sequenceDiagram
autonumber
participant U as User
participant FE as Frontend (React)
participant BE as Backend API (Gin)
participant DB as Postgres
Note over FE,BE: Auth uses either HttpOnly cookie (auth_token) or Bearer token
U->>FE: Submit credentials (email/password)
FE->>BE: POST /api/v1/auth/login {email, password}
BE->>DB: Verify user by email
DB-->>BE: User + password hash
BE->>BE: Check password, issue JWT
BE-->>FE: 200 OK + Set-Cookie auth_token=JWT (HttpOnly)
rect rgba(200, 255, 200, 0.15)
Note over U,BE: Accessing protected endpoints
FE->>BE: GET /api/v1/admin/... (with cookie or Authorization: Bearer)
BE->>BE: JWTAuth parses token, loads user
BE->>DB: SELECT users WHERE id=claims.userID
DB-->>BE: User
BE->>BE: RoleAuth("admin" or "editor")
BE-->>FE: 200 OK (or 403/401)
end
rect rgba(200, 200, 255, 0.15)
Note over U,BE: Get current user
FE->>BE: GET /api/v1/auth/me
BE->>BE: JWTOptional (if present)
BE-->>FE: 200 OK {user}
end
rect rgba(255, 220, 200, 0.15)
Note over U,BE: Logout
FE->>BE: POST /api/v1/auth/logout
BE-->>FE: 200/204 + Clear-Cookie auth_token
end
Note over BE: Dev shortcuts (non-production)
Note over BE: X-Admin-Token or X-Dev-Admin grant admin for local/dev only
+42
View File
@@ -0,0 +1,42 @@
%%{init: {
'theme': 'forest',
'flowchart': { 'curve': 'linear' },
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 20s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }'
}}%%
flowchart LR
classDef job fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
classDef svc fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
classDef ext fill:#faf5ff,stroke:#a855f7,color:#6b21a8;
classDef api fill:#eef2ff,stroke:#6366f1,color:#312e81;
subgraph jobs["Background Jobs & Services"]
direction TB
j_prefetch["Prefetcher (StartPrefetcher)\nFetches public endpoints periodically"]:::job
j_news_sched["NewsletterScheduler"]:::job
j_news_auto["NewsletterAutomation\nWeekly, match alerts, blog notifications, results"]:::job
j_sweep["SweepstakesScheduler\nFinalize & pick winners"]:::job
j_err_auto["ErrorReview Auto-Register\nRegisters backend in monitors"]:::job
j_filetrk["FileTracker\nScan/uploads tracking"]:::svc
j_logo["LogoCache"]:::svc
j_cache["CacheService"]:::svc
j_imgopt["ImageOptimizer"]:::svc
j_umami["UmamiService"]:::svc
end
api_public["/api/v1 (public)"]:::api
smtp["SMTP Provider"]:::ext
err_recv["Error Receiver: errors.tdvorak.dev or local :8083"]:::ext
facr["FACR Scraper/API"]:::ext
umami["Umami Server"]:::ext
j_prefetch -.-> api_public
j_news_sched --> j_news_auto
j_news_auto --> smtp
j_sweep --> smtp
j_err_auto --> err_recv
j_umami <---> umami
j_filetrk --> j_cache
j_imgopt --> j_cache
j_logo --> j_cache
-37
View File
@@ -1,37 +0,0 @@
graph LR
subgraph Backend
Router[API Router /api/v1]
Middleware[Middleware JWT RateLimit CORS Gzip Recovery]
Controllers[Controllers]
Services[Services]
Models[Models GORM]
DB[PostgreSQL]
Migrations[Migrations]
Jobs[Background jobs Prefetcher Newsletter]
Uploads[uploads static dist]
end
subgraph Integrations
FACR[FACR API]
YT[YouTube API]
ZON[Zonerama]
SMTP[SMTP Email]
MAPS[Google Maps]
UMAMI[Umami Analytics]
end
Router --> Middleware
Router --> Controllers
Controllers --> Services
Services --> Models
Models --> DB
Migrations --> DB
Jobs --> Services
Jobs --> DB
Controllers --> Uploads
Controllers --> FACR
Controllers --> YT
Controllers --> ZON
Controllers --> SMTP
Controllers --> MAPS
Controllers -. telemetry .-> UMAMI
Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

+64
View File
@@ -0,0 +1,64 @@
%%{init: {
'theme': 'base',
'flowchart': { 'curve': 'linear' },
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 18s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } } .cluster rect { rx:8; ry:8; }'
}}%%
flowchart TB
classDef stage fill:#f1f5f9,stroke:#475569,color:#0f172a;
classDef mid fill:#ecfeff,stroke:#0891b2,color:#0e7490;
classDef api fill:#eef2ff,stroke:#6366f1,color:#312e81;
classDef route fill:#ede9fe,stroke:#7c3aed,color:#4c1d95;
classDef stat fill:#e2e8f0,stroke:#334155,color:#0f172a;
subgraph req["HTTP Request Pipeline"]
direction TB
client["Client"]:::stage
router["Gin Router"]:::stage
m_reqid["RequestID"]:::mid
m_logger["RequestLogger"]:::mid
m_recovery["CustomRecoveryWithReporter"]:::mid
m_errstatus["ErrorStatusReporter"]:::mid
m_sanitize["SanitizeHeaders"]:::mid
m_dbctx["DBContext (with timeout)"]:::mid
m_size["RequestSizeLimit (2MB)"]:::mid
m_ct["ValidateContentType (JSON for mutating)"]:::mid
m_sec["SecurityHeaders"]:::mid
m_assets["AssetCacheControl"]:::mid
m_cors["CORS Handler (reflect allowed origins)"]:::mid
client --> router
router --> m_reqid --> m_logger --> m_recovery --> m_errstatus --> m_sanitize --> m_dbctx --> m_size --> m_ct --> m_sec --> m_assets --> m_cors
end
subgraph endpoints["Endpoints"]
direction TB
api_v1["/api/v1"]:::api
root["/robots.txt, /sitemap.xml, /s/:code, /r"]:::api
subgraph groups["API Groups"]
direction TB
g_public["Public"]:::route
g_protected["Protected (JWTAuth + CSRF)"]:::route
g_admin["Admin (Role: admin)"]:::route
end
m_cors --> api_v1
m_cors --> root
api_v1 --> g_public --> g_protected --> g_admin
end
subgraph static["Static & Assets"]
direction TB
s_cache["/cache -> ./cache"]:::stat
s_uploads["/uploads -> UPLOAD_DIR"]:::stat
s_dist["/dist -> ./static"]:::stat
s_prem["/premium-assets -> ./pro"]:::stat
s_metrics["/metrics (prometheus)"]:::stat
end
m_cors --> s_cache
m_cors --> s_uploads
m_cors --> s_dist
m_cors --> s_prem
m_cors --> s_metrics
+78
View File
@@ -0,0 +1,78 @@
%%{init: {
'theme': 'forest',
'flowchart': { 'curve': 'linear' },
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }'
}}%%
flowchart LR
classDef bin fill:#ffe4e6,stroke:#be123c,color:#7f1d1d;
classDef internal fill:#e0f2fe,stroke:#0369a1,color:#0c4a6e;
classDef pkg fill:#dcfce7,stroke:#16a34a,color:#065f46;
classDef third fill:#ede9fe,stroke:#7c3aed,color:#4c1d95;
main["main.go"]:::bin
subgraph internal_pkgs["internal/* packages"]
direction TB
p_config["internal/config"]:::internal
p_routes["internal/routes"]:::internal
p_controllers["internal/controllers"]:::internal
p_services["internal/services"]:::internal
p_models["internal/models"]:::internal
p_middleware["internal/middleware"]:::internal
end
subgraph shared_pkgs["pkg/*"]
direction TB
p_db["pkg/database"]:::pkg
p_email["pkg/email"]:::pkg
p_logger["pkg/logger"]:::pkg
end
subgraph third_party["third-party"]
direction TB
t_gin["github.com/gin-gonic/gin"]:::third
t_gzip["github.com/gin-contrib/gzip"]:::third
t_prom["github.com/prometheus/client_golang/promhttp"]:::third
t_gorm["gorm.io/gorm"]:::third
end
%% main dependencies
main --> p_config
main --> p_logger
main --> p_db
main --> p_models
main --> p_middleware
main --> p_services
main --> p_routes
main --> p_email
main --> t_gin
main --> t_gzip
main --> t_prom
%% routes wiring
p_routes --> p_controllers
p_routes --> p_middleware
p_routes --> p_services
p_routes --> p_email
p_routes --> t_gin
p_routes --> t_gorm
%% controllers wiring
p_controllers --> p_models
p_controllers --> p_services
%% middleware wiring
p_middleware --> p_config
p_middleware --> t_gorm
%% services wiring
p_services --> p_models
p_services --> p_email
%% database wiring
p_db --> p_config
p_db --> t_gorm
%% logger
p_logger --> main
+113
View File
@@ -0,0 +1,113 @@
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
flowchart TB
classDef group fill:#eef7ff,stroke:#2b6cb0,color:#0b3a60;
classDef sec fill:#fef9c3,stroke:#ca8a04,color:#7c2d12;
classDef admin fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
classDef pub fill:#f1f5f9,stroke:#334155,color:#0f172a;
classDef root fill:#f3e8ff,stroke:#6d28d9,color:#3b0764;
classDef route fill:#e2e8f0,stroke:#475569,color:#111827;
classDef ext fill:#faf5ff,stroke:#a855f7,color:#6b21a8;
client((Browser)):::ext
api["/api/v1"]:::group
rootgrp["Root"]:::group
client ==>|HTTP| api
client ==>|HTTP| rootgrp
subgraph PUBLIC["Public endpoints"]
direction TB
p_health["GET /health"]:::pub
p_csrf["GET /csrf-token"]:::pub
p_image_proxy["GET /proxy/image"]:::pub
p_seo["GET /seo"]:::pub
p_nav["GET /navigation, /social-links"]:::pub
p_page_elems["GET /page-elements"]:::pub
p_short_public["POST /shortlinks/public"]:::pub
p_email["GET /email/open.gif | /email/click | /email/unsubscribe"]:::pub
p_setup["GET /setup/status | POST /setup/initialize | POST /setup/validate-smtp"]:::pub
p_errors_ingest["POST /errors (rate-limited)"]:::pub
p_comments_list["GET /comments (JWT optional)"]:::pub
p_eng_rewards["GET /engagement/rewards"]:::pub
p_scoreboard_pub["GET /scoreboard | /scoreboard/sponsors | /scoreboard/qr"]:::pub
p_settings["GET /settings"]:::pub
p_comp_aliases["GET /competition-aliases"]:::pub
p_team_logo_over["GET /public/team-logo-overrides"]:::pub
p_articles["/articles: featured, list, slug/:slug, :id, read, track-view"]:::pub
p_categories["GET /categories"]:::pub
p_youtube["GET /youtube/videos"]:::pub
p_about["GET /about"]:::pub
p_teams["GET /teams, /teams/:id"]:::pub
p_players["GET /players, /players/:id"]:::pub
p_sponsors["GET /sponsors"]:::pub
p_banners["GET /banners"]:::pub
p_matches["GET /matches | /matches/history | /standings"]:::pub
p_gallery["GET /gallery/albums | /gallery/albums/:id | /gallery/proxy-image"]:::pub
p_zonerama["GET /zonerama/album | /zonerama-album | /zonerama/picks"]:::pub
p_clothing["GET /clothing"]:::pub
p_sweep_pub["GET /sweepstakes/current | /sweepstakes/:id/visual"]:::pub
p_polls["GET /polls | /polls/:id | POST /polls/:id/vote | GET /polls/:id/results"]:::pub
p_contact["POST /contact"]:::pub
p_newsletter_pub["POST /newsletter/subscribe | /newsletter/unsubscribe/:email | /newsletter/setup | /newsletter/preferences"]:::pub
p_newsletter_token["POST /newsletter/unsubscribe-token | GET /newsletter/preferences (by token)"]:::pub
p_facr["/facr: club/search | club/:type/:id | table"]:::pub
end
subgraph PROTECTED["Protected (JWTAuth + CSRF for state)"]
direction TB
prot_sweep["POST /sweepstakes/:id/enter | POST /sweepstakes/:id/played | GET /sweepstakes/my-winnings"]:::route
prot_eng["Engagement: GET /leaderboard, /profile, /achievements, /transactions | POST /checkin, /article-read, /redeem | PATCH /profile, /avatar"]:::route
prot_comments["Comments: POST /comments | PUT/DELETE /comments/:id | react/unreact | unban-request | report"]:::route
prot_editor_preview["/editor: GET/POST /preview/:session_id | apply | delete | validate | GET /variants/:element_name"]:::sec
prot_newsletter_me["GET /newsletter/token/me"]:::route
prot_user["PUT /me | GET /me"]:::route
prot_events["/events (editor): POST, PUT, DELETE"]:::sec
prot_shortlinks["/shortlinks (editor): POST, GET"]:::sec
prot_articles["/articles (editor): POST, PUT/:id, DELETE/:id, match-link PUT/DELETE"]:::sec
prot_admin_groups["/admin/* groups (require admin role)"]:::admin
end
subgraph ADMIN["Admin groups (JWT + Role: admin)"]
direction TB
ad_errors["/admin/errors: list, get, external proxies"]:::admin
ad_comments["/admin/comments: list, status, bans, unban requests"]:::admin
ad_comp_aliases["/admin/competition-aliases: CRUD + reorder"]:::admin
ad_settings["/admin/settings: GET/PUT"]:::admin
ad_about["/admin/about: GET/PUT/DELETE"]:::admin
ad_scoreboard["/admin/scoreboard: GET/PUT + timer + sponsors + QR + presets"]:::admin
ad_users["/admin/users: CRUD + send reset + reset by ID"]:::admin
ad_matches["/admin/matches: merged list"]:::admin
ad_overrides["/admin/*-overrides: GET/PUT/PATCH for match/team logos"]:::admin
ad_contacts["/admin/contact-messages: list/get/read/forward/delete"]:::admin
ad_newsletter["/admin/newsletter: send/test/preview/status + automation"]:::admin
ad_notifications["/admin/notifications: competition, match"]:::admin
ad_prefetch["/admin/prefetch: status/trigger"]:::admin
ad_cache["/admin/cache: list/file"]:::admin
ad_gallery["/admin/gallery: profile, fetch, refresh, delete"]:::admin
ad_zonerama["/admin/zonerama: save-album, pick"]:::admin
ad_seo["/admin/seo: GET/PUT"]:::admin
ad_files["/admin/files: list/unused/duplicates/usage/usages/delete/scan/refresh-tracking"]:::admin
ad_navigation["/admin/navigation + /admin/social-links: CRUD + reorder + seed"]:::admin
ad_clothing["/admin/clothing: CRUD + reorder"]:::admin
ad_polls["/admin/polls: CRUD + stats + votes"]:::admin
ad_engagement["/admin/engagement: rewards CRUD, redemptions, leaderboard, transactions, adjust, profile"]:::admin
ad_page_elements["/admin/page-elements: CRUD + batch"]:::admin
ad_myuibrix["/admin/myuibrix: validate, batch-validate, preview, optimize-layout"]:::admin
ad_shortlinks["/admin/shortlinks: create/list + stats"]:::admin
ad_sweep["/admin/sweepstakes: CRUD + entries/winners/prizes + finalize"]:::admin
end
subgraph ROOT["Root endpoints"]
direction TB
r_robots["GET /robots.txt"]:::root
r_sitemap["GET /sitemap.xml"]:::root
r_short["GET /s/:code"]:::root
r_redirect["GET /r (tracked redirect)"]:::root
end
api --> PUBLIC
api --> PROTECTED
api --> ADMIN
rootgrp --> ROOT
note["Note: This overview groups related endpoints; see routes.go for exact definitions and middlewares."]:::route
+72
View File
@@ -0,0 +1,72 @@
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
autonumber
participant V as Visitor/User
participant FE as Frontend
participant BE as Backend API
participant DB as Postgres
Note over FE,BE: Public list (JWT optional personalizes reactions)
FE->>BE: GET /api/v1/comments
BE->>BE: JWTOptional
BE->>DB: Query comments + aggregates
DB-->>BE: Rows
BE-->>FE: 200 OK [comments]
rect rgba(220,255,220,0.2)
Note over V,BE: Create/Edit/Delete comment (protected)
FE->>BE: POST /api/v1/comments (RateLimit)
BE->>BE: JWTAuth + CSRF
BE->>DB: Insert Comment
BE-->>FE: 201 Created
FE->>BE: PUT /api/v1/comments/:id
BE->>BE: JWTAuth + CSRF
BE->>DB: Update Comment
BE-->>FE: 200 OK
FE->>BE: DELETE /api/v1/comments/:id
BE->>BE: JWTAuth + CSRF
BE->>DB: Delete Comment
BE-->>FE: 204 No Content
end
rect rgba(220,220,255,0.2)
Note over V,BE: Reactions & unban/report actions
FE->>BE: POST /api/v1/comments/:id/react | DELETE /comments/:id/react
BE->>BE: JWTAuth + RateLimit
BE->>DB: Insert/Delete reaction
BE-->>FE: 200 OK
FE->>BE: POST /api/v1/comments/unban-request
BE->>BE: JWTAuth + RateLimit
BE->>DB: Insert UnbanRequest
BE-->>FE: 200 OK
FE->>BE: POST /api/v1/comments/:id/report
BE->>BE: JWTAuth + RateLimit
BE->>DB: Insert CommentReport
BE-->>FE: 200 OK
end
rect rgba(255,240,220,0.2)
Note over FE,BE: Admin moderation
FE->>BE: GET /api/v1/admin/comments
BE->>BE: JWTAuth + RoleAuth(admin)
BE->>DB: List with filters
BE-->>FE: 200 OK
FE->>BE: PATCH /api/v1/admin/comments/:id/status
BE->>DB: Update status
BE-->>FE: 200 OK
FE->>BE: POST /api/v1/admin/comments/ban
BE->>DB: Insert CommentBan
BE-->>FE: 200 OK
FE->>BE: GET /api/v1/admin/comments/bans
FE->>BE: POST /api/v1/admin/comments/bans/:id/lift
FE->>BE: GET /api/v1/admin/comments/unban-requests
FE->>BE: POST /api/v1/admin/comments/unban-requests/:id/resolve
end
+39
View File
@@ -0,0 +1,39 @@
%%{init: { 'theme': 'forest' }}%%
erDiagram
USER ||--o{ ARTICLE : author
ARTICLE }o--|| CATEGORY : belongs_to
USER ||--o{ COMMENT : writes
COMMENT ||--o{ COMMENT_REACTION : has
USER ||--o{ COMMENT_REACTION : reacts
USER ||--o{ COMMENT_BAN : ban
USER ||--o{ UNBAN_REQUEST : request
COMMENT ||--o{ COMMENT_REPORT : reported
USER ||--o{ USER_PROFILE : has
POLL ||--o{ POLL_OPTION : has
POLL ||--o{ POLL_VOTE : has
USER ||--o{ POLL_VOTE : votes
SHORT_LINK ||--o{ LINK_CLICK : tracked
SWEEPSTAKE ||--o{ SWEEPSTAKE_PRIZE : has
SWEEPSTAKE ||--o{ SWEEPSTAKE_ENTRY : has
SWEEPSTAKE ||--o{ SWEEPSTAKE_WINNER : has
UPLOADED_FILE ||--o{ FILE_USAGE : used_in
NAVIGATION_ITEM ||--o{ ARTICLE : links
SOCIAL_LINK ||--o{ NAVIGATION_ITEM : part_of
TEAM ||--o{ PLAYER : has
COMPETITION_ALIAS ||--o{ TEAM : member_of
SCOREBOARD_STATE ||..|| ARTICLE : references
CONTACT ||--o{ CONTACT_MESSAGE : has
CONTACT_CATEGORY ||--o{ CONTACT : categorizes
NEWSLETTER_SUBSCRIPTION ||..|| CONTACT : same_person
ERROR_EVENT ||..|| USER : context
%% Note: Names reflect models; exact FKs are simplified for overview
+122
View File
@@ -0,0 +1,122 @@
%%{init: {
'theme': 'neutral',
'flowchart': { 'curve': 'linear' },
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 18s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }'
}}%%
flowchart TB
classDef grp fill:#0f172a,stroke:#334155,color:#e5e7eb;
classDef model fill:#111827,stroke:#475569,color:#e5e7eb;
subgraph CORE["Core"]
direction LR
m_settings["Settings"]:::model
m_user["User"]:::model
m_user_profile["UserProfile"]:::model
end
subgraph CONTENT["Content"]
direction LR
m_article["Article"]:::model
m_category["Category"]:::model
m_page_el["PageElementConfig"]:::model
m_uploaded["UploadedFile"]:::model
m_file_usage["FileUsage"]:::model
end
subgraph NAV["Navigation"]
direction LR
m_nav_item["NavigationItem"]:::model
m_social["SocialLink"]:::model
m_short["ShortLink"]:::model
m_click["LinkClick"]:::model
end
subgraph MATCHES["Matches & Teams"]
direction LR
m_team["Team"]:::model
m_player["Player"]:::model
m_match_over["MatchOverride"]:::model
m_team_logo_over["TeamLogoOverride"]:::model
m_comp_alias["CompetitionAlias"]:::model
m_score_state["ScoreboardState"]:::model
end
subgraph POLLS["Polls"]
direction LR
m_poll["Poll"]:::model
m_poll_opt["PollOption"]:::model
m_poll_vote["PollVote"]:::model
end
subgraph SWEEPSTAKES["Sweepstakes"]
direction LR
m_sw["Sweepstake"]:::model
m_sw_prize["SweepstakePrize"]:::model
m_sw_entry["SweepstakeEntry"]:::model
m_sw_winner["SweepstakeWinner"]:::model
end
subgraph COMMENTS["Comments & Moderation"]
direction LR
m_comment["Comment"]:::model
m_comment_react["CommentReaction"]:::model
m_comment_ban["CommentBan"]:::model
m_unban_req["UnbanRequest"]:::model
m_comment_rep["CommentReport"]:::model
end
subgraph CONTACTS["Contacts & Newsletter"]
direction LR
m_contact_cat["ContactCategory"]:::model
m_contact["Contact"]:::model
m_contact_msg["ContactMessage"]:::model
m_news_sub["NewsletterSubscription"]:::model
m_email_log["EmailLog (models.email)"]:::model
end
subgraph ENGAGE["Engagement & Rewards"]
direction LR
m_points_tx["PointsTransaction"]:::model
m_ach["Achievement"]:::model
m_user_ach["UserAchievement"]:::model
m_reward_item["RewardItem"]:::model
m_reward_red["RewardRedemption"]:::model
end
subgraph SHOP["Shop"]
direction LR
m_cloth["Clothing"]:::model
end
subgraph GALLERY["Gallery"]
direction LR
m_zonerama_album["ZoneramaAlbum (derived)"]:::model
end
subgraph ERRORS["Error Tracking"]
direction LR
m_error["ErrorEvent"]:::model
end
%% (Optional) Indicative relationships (no strict cardinalities)
%% Using simple arrows to avoid ER notation parse issues
m_article --> m_category
m_article --> m_user
m_file_usage --> m_uploaded
m_click --> m_short
m_comment_react --> m_comment
m_comment_ban --> m_user
m_unban_req --> m_user
m_comment_rep --> m_comment
m_user_profile --> m_user
m_points_tx --> m_user
m_user_ach --> m_user
m_user_ach --> m_ach
m_reward_red --> m_reward_item
m_sw_entry --> m_sw
m_sw_winner --> m_sw
m_poll_opt --> m_poll
m_poll_vote --> m_poll
m_contact_msg --> m_contact
m_nav_item --> m_article
-79
View File
@@ -1,79 +0,0 @@
# ER Diagram
```mermaid
erDiagram
USERS ||--o{ ARTICLES : author_id
CATEGORIES ||--o{ ARTICLES : category_id
ARTICLES ||--o{ ARTICLE_TEAM_LINKS : article_id
ARTICLES ||--o{ ARTICLE_MATCH_LINKS : article_id
TEAMS ||--o{ PLAYERS : team_id
USERS ||--o{ EVENTS : created_by_id
EVENTS ||--o{ EVENT_ATTACHMENTS : event_id
POLLS ||--o{ POLL_OPTIONS : poll_id
POLLS ||--o{ POLL_VOTES : poll_id
POLL_OPTIONS ||--o{ POLL_VOTES : option_id
USERS o{--o| POLL_VOTES : user_id
CATEGORIES o{--o| POLLS : category_id
ARTICLES o{--o| POLLS : related_article_id
EVENTS o{--o| POLLS : related_event_id
PLAYERS o{--o| POLL_OPTIONS : player_id
USERS ||--|| USER_PROFILES : user_id
USERS ||--o{ PASSWORD_RESETS : user_id
USERS ||--o{ COMMENTS : user_id
COMMENTS o{--o| COMMENTS : parent_id
USERS ||--o{ COMMENT_BANS : user_id
USERS ||--o{ UNBAN_REQUESTS : user_id
COMMENTS ||--o{ COMMENT_REACTIONS : comment_id
COMMENTS ||--o{ COMMENT_REPORTS : comment_id
USERS o{--o| UPLOADED_FILES : uploaded_by_id
UPLOADED_FILES ||--o{ FILE_USAGES : file_id
CONTACT_CATEGORIES o{--o| CONTACTS : category_id
SHORT_LINKS o{--o| LINK_CLICKS : short_link_id
USERS o{--o| SHORT_LINKS : created_by_id
EMAIL_LOGS ||--o{ EMAIL_EVENTS : email_log_id
ARTICLES ||--o{ BLOG_NOTIFICATIONS : article_id
SWEEPSTAKES ||--o{ SWEEPSTAKE_PRIZES : sweepstake_id
SWEEPSTAKES ||--o{ SWEEPSTAKE_ENTRIES : sweepstake_id
SWEEPSTAKES ||--o{ SWEEPSTAKE_WINNERS : sweepstake_id
USERS ||--o{ SWEEPSTAKE_ENTRIES : user_id
USERS ||--o{ SWEEPSTAKE_WINNERS : user_id
SWEEPSTAKE_ENTRIES ||--o{ SWEEPSTAKE_WINNERS : entry_id
SWEEPSTAKE_PRIZES |o--o{ SWEEPSTAKE_WINNERS : prize_id
USERS ||--o{ POINTS_TRANSACTIONS : user_id
USERS ||--o{ USER_ACHIEVEMENTS : user_id
ACHIEVEMENTS ||--o{ USER_ACHIEVEMENTS : achievement_id
REWARD_ITEMS ||--o{ REWARD_REDEMPTIONS : reward_id
USERS ||--o{ REWARD_REDEMPTIONS : user_id
USERS o{--o| AUDIT_LOGS : user_id
USERS o{--o| ERROR_EVENTS : user_id
USERS o{--o| VISITOR_EVENTS : user_id
SETUP_INFO ||--o{ CLUB_INFO : setup_info_id
NAVIGATION_ITEMS o{--o| NAVIGATION_ITEMS : parent_id
%% Standalone/core tables (configured/consumed by services)
SETTINGS
ABOUT_PAGES
SPONSORS
BANNERS
CLOTHING
COMPETITION_ALIASES
MATCH_OVERRIDES
TEAM_LOGO_OVERRIDES
NEWSLETTER_SUBSCRIPTIONS
NEWSLETTER_SENT_LOG
MATCH_NOTIFICATIONS
SCOREBOARD_STATES
```
-75
View File
@@ -1,75 +0,0 @@
erDiagram
USERS ||--o{ ARTICLES : author_id
CATEGORIES ||--o{ ARTICLES : category_id
ARTICLES ||--o{ ARTICLE_TEAM_LINKS : article_id
ARTICLES ||--o{ ARTICLE_MATCH_LINKS : article_id
TEAMS ||--o{ PLAYERS : team_id
USERS ||--o{ EVENTS : created_by_id
EVENTS ||--o{ EVENT_ATTACHMENTS : event_id
POLLS ||--o{ POLL_OPTIONS : poll_id
POLLS ||--o{ POLL_VOTES : poll_id
POLL_OPTIONS ||--o{ POLL_VOTES : option_id
USERS |o--o{ POLL_VOTES : user_id
CATEGORIES |o--o{ POLLS : category_id
ARTICLES |o--o{ POLLS : related_article_id
EVENTS |o--o{ POLLS : related_event_id
PLAYERS |o--o{ POLL_OPTIONS : player_id
USERS ||--|| USER_PROFILES : user_id
USERS ||--o{ PASSWORD_RESETS : user_id
USERS ||--o{ COMMENTS : user_id
COMMENTS |o--o{ COMMENTS : parent_id
USERS ||--o{ COMMENT_BANS : user_id
USERS ||--o{ UNBAN_REQUESTS : user_id
COMMENTS ||--o{ COMMENT_REACTIONS : comment_id
COMMENTS ||--o{ COMMENT_REPORTS : comment_id
USERS |o--o{ UPLOADED_FILES : uploaded_by_id
UPLOADED_FILES ||--o{ FILE_USAGES : file_id
CONTACT_CATEGORIES |o--o{ CONTACTS : category_id
SHORT_LINKS |o--o{ LINK_CLICKS : short_link_id
USERS |o--o{ SHORT_LINKS : created_by_id
EMAIL_LOGS ||--o{ EMAIL_EVENTS : email_log_id
ARTICLES ||--o{ BLOG_NOTIFICATIONS : article_id
SWEEPSTAKES ||--o{ SWEEPSTAKE_PRIZES : sweepstake_id
SWEEPSTAKES ||--o{ SWEEPSTAKE_ENTRIES : sweepstake_id
SWEEPSTAKES ||--o{ SWEEPSTAKE_WINNERS : sweepstake_id
USERS ||--o{ SWEEPSTAKE_ENTRIES : user_id
USERS ||--o{ SWEEPSTAKE_WINNERS : user_id
SWEEPSTAKE_ENTRIES ||--o{ SWEEPSTAKE_WINNERS : entry_id
SWEEPSTAKE_PRIZES |o--o{ SWEEPSTAKE_WINNERS : prize_id
USERS ||--o{ POINTS_TRANSACTIONS : user_id
USERS ||--o{ USER_ACHIEVEMENTS : user_id
ACHIEVEMENTS ||--o{ USER_ACHIEVEMENTS : achievement_id
REWARD_ITEMS ||--o{ REWARD_REDEMPTIONS : reward_id
USERS ||--o{ REWARD_REDEMPTIONS : user_id
USERS |o--o{ AUDIT_LOGS : user_id
USERS |o--o{ ERROR_EVENTS : user_id
USERS |o--o{ VISITOR_EVENTS : user_id
SETUP_INFO ||--o{ CLUB_INFO : setup_info_id
NAVIGATION_ITEMS |o--o{ NAVIGATION_ITEMS : parent_id
%% Standalone/core tables (configured/consumed by services)
SETTINGS
ABOUT_PAGES
SPONSORS
BANNERS
CLOTHING
COMPETITION_ALIASES
MATCH_OVERRIDES
TEAM_LOGO_OVERRIDES
NEWSLETTER_SUBSCRIPTIONS
NEWSLETTER_SENT_LOG
MATCH_NOTIFICATIONS
SCOREBOARD_STATES
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 91 KiB

+29
View File
@@ -0,0 +1,29 @@
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
autonumber
participant FE as Frontend (React)
participant BE as Backend API
participant ER as Error Receiver (errors.tdvorak.dev or :8083)
participant EV as Error Review Admin UI
Note over FE: JS errors captured (window.onerror,<br/>unhandledrejection, manual report)
FE->>BE: POST /api/v1/errors {event}
BE->>BE: RateLimit(120/min)
BE->>BE: Validate & normalize
alt External ingest configured
BE->>ER: POST /api/v1/errors (Bearer/X-Ingest-Token)
ER-->>BE: 202 Accepted {request_id}
else Local DB fallback
BE->>BE: Store as ErrorEvent (DB)
end
BE-->>FE: 200 OK
Note over ER,EV: Admin inspects
EV->>ER: GET /admin/api/errors
ER-->>EV: List, details
rect rgba(240,240,255,0.2)
Note over BE,EV: Auto-register monitor (background)
BE->>ER: Register/heartbeat monitor (retry)
EV->>ER: Autologin redirect injects token (local dev)
end
+108
View File
@@ -0,0 +1,108 @@
%%{init: {
'theme': 'forest',
'flowchart': { 'curve': 'linear' },
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }'
}}%%
flowchart LR
classDef svc fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
classDef admin fill:#dcfce7,stroke:#16a34a,color:#065f46;
classDef pub fill:#f1f5f9,stroke:#334155,color:#0f172a;
classDef core fill:#ede9fe,stroke:#7c3aed,color:#4c1d95;
api_core["services/api.ts (Axios core)"]:::core
subgraph Services
direction TB
s_settings[settings.ts]:::svc
s_page_elements[pageElements.ts]:::svc
s_articles[articles.ts]:::svc
s_players[players.ts]:::svc
s_sponsors[sponsors.ts]:::svc
s_banners[banners.ts]:::svc
s_comp_alias[competitionAliases.ts]:::svc
s_events[eventService.ts]:::svc
s_setup[setup.ts]:::svc
s_engagement[engagement.ts]:::svc
s_action[actionLog.ts]:::svc
s_facr[facr/facrApi.ts]:::svc
s_files[files.ts]:::svc
s_image[imageProcessing.ts]:::svc
s_shortlinks[shortlinks.ts]:::svc
s_scoreboard[scoreboard.ts]:::svc
s_youtube[youtube.ts]:::svc
s_zonerama[zonerama.ts]:::svc
s_errors[errors.ts]:::svc
s_contactInfo[contactInfo.ts]:::svc
s_public[public.ts]:::svc
s_editor[editorController.ts]:::svc
end
subgraph AdminAPIs
direction TB
s_admin_comments[admin/comments.ts]:::admin
s_admin_msgs[admin/contactMessages.ts]:::admin
s_admin_eng[admin/engagement.ts]:::admin
s_admin_news[admin/newsletter.ts]:::admin
s_admin_prefetch[admin/prefetch.ts]:::admin
s_admin_matches[adminMatches.ts]:::admin
end
api_core --> Services
api_core --> AdminAPIs
subgraph PublicEndpoints["Representative public endpoints"]
direction TB
e_articles["/articles, /articles/slug/:slug, /articles/:id"]:::pub
e_featured["/articles/featured"]:::pub
e_players["/players, /players/:id"]:::pub
e_teams["/teams, /teams/:id"]:::pub
e_scores["/matches, /standings, /matches/history"]:::pub
e_gallery["/gallery/albums, /gallery/albums/:id"]:::pub
e_youtube["/youtube/videos"]:::pub
e_settings["/settings"]:::pub
e_scoreboard_pub["/scoreboard, /scoreboard/sponsors, /scoreboard/qr"]:::pub
e_contact["/contact"]:::pub
e_short_pub["/shortlinks/public"]:::pub
e_polls["/polls, /polls/:id, /polls/:id/vote, /polls/:id/results"]:::pub
end
%% Map key services to endpoints
s_articles --> e_articles
s_articles --> e_featured
s_players --> e_players
s_settings --> e_settings
s_page_elements --> e_settings
s_comp_alias --> e_scores
s_events --> e_scores
s_zonerama --> e_gallery
s_youtube --> e_youtube
s_scoreboard --> e_scoreboard_pub
s_contactInfo --> e_contact
s_shortlinks --> e_short_pub
s_public --> e_settings
subgraph AdminEndpoints["Representative admin endpoints"]
direction TB
a_settings["/admin/settings"]:::admin
a_files["/admin/files"]:::admin
a_nav["/admin/navigation, /admin/social-links"]:::admin
a_comments["/admin/comments"]:::admin
a_msgs["/admin/contact-messages"]:::admin
a_news["/admin/newsletter"]:::admin
a_matches["/admin/matches"]:::admin
a_sweep["/admin/sweepstakes"]:::admin
a_scoreboard["/admin/scoreboard"]:::admin
a_shortlinks["/admin/shortlinks"]:::admin
end
s_admin_comments --> a_comments
s_admin_msgs --> a_msgs
s_admin_eng --> a_settings
s_admin_news --> a_news
s_admin_prefetch --> a_settings
s_admin_matches --> a_matches
s_files --> a_files
s_navigation[navigation.ts]:::svc --> a_nav
s_shortlinks --> a_shortlinks
s_scoreboard --> a_scoreboard
+39
View File
@@ -0,0 +1,39 @@
flowchart TD
%% Provider & runtime architecture
classDef infra fill:#1f2835,stroke:#5b6e8a,color:#e8eaf0;
classDef ctx fill:#2b233f,stroke:#7a63a0,color:#e8eaf0;
classDef comp fill:#1d2a2a,stroke:#3d7a6a,color:#e8eaf0;
subgraph Entry[index.tsx]
RootDOM[(#root)]:::infra --> ErrorBoundary:::comp --> ColorModeScript:::infra --> AppLazy[App.lazy]:::infra
ServiceWorker[serviceWorkerRegistration]:::infra
ErrorReporter[services/errorReporter.installGlobalErrorHandlers]:::infra
end
subgraph Providers
Chakra[ChakraProvider]:::infra --> RQ[QueryClientProvider]:::infra --> Router[BrowserRouter]:::infra --> AuthProv[AuthProvider]:::ctx --> ClubThemeProv[ClubThemeProvider]:::ctx --> Helmet[HelmetProvider]:::infra --> Suspense:::infra --> Routes:::infra
DefaultSEO:::comp
CookieBanner:::comp
end
AppLazy --> Chakra
AppLazy --> DefaultSEO
AppLazy --> CookieBanner
subgraph Routing
Routes:::infra --> PublicRoutes
Routes:::infra --> AdminRoutes
ProtectedRoute:::comp
end
AuthProv --> ProtectedRoute
subgraph PublicRoutes
HomeRoute
BlogRoute
OtherPublic[(other public pages)]
end
subgraph AdminRoutes
AdminPages[(admin pages...)]
end
+225
View File
@@ -0,0 +1,225 @@
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
flowchart LR
%% Everything Graph: combined overview of frontend
classDef cluster fill:#eef7ff,stroke:#2b6cb0,color:#0b3a60;
classDef page fill:#fff7ed,stroke:#f59e0b,color:#7c2d12;
classDef comp fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
classDef ctx fill:#f3e8ff,stroke:#6d28d9,color:#3b0764;
classDef hook fill:#fef9c3,stroke:#ca8a04,color:#7c2d12;
classDef svc fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
classDef util fill:#e2e8f0,stroke:#475569,color:#111827;
classDef infra fill:#e3f2fd,stroke:#1e88e5,color:#0c4a6e;
%% Entry & Providers
subgraph Entry[Entry / Boot]
index[index.tsx]:::infra --> AppLazy[App.lazy.tsx]:::infra
index --> ErrorBoundary:::comp
index --> ServiceWorker[serviceWorkerRegistration]:::infra
end
subgraph Providers[Providers]
Chakra[ChakraProvider]:::infra --> RQ[QueryClientProvider]:::infra --> Router[BrowserRouter]:::infra --> AuthProv[AuthProvider]:::ctx --> ClubThemeProv[ClubThemeProvider]:::ctx --> Helmet[HelmetProvider]:::infra --> Suspense:::infra --> Routes:::infra
DefaultSEO:::comp
CookieBanner:::comp
end
AppLazy --> Chakra
AppLazy --> DefaultSEO
AppLazy --> CookieBanner
Router --> Routes
%% Routing
subgraph Routing[Routes]
HomeRoute:::comp --> HomePage
BlogRoute:::comp --> BlogPage
NotFoundRoute:::comp --> NotFoundPage
ProtectedRoute:::comp
end
AuthProv --> ProtectedRoute
%% Pages
subgraph Pages
HomePage:::page
BlogPage:::page
ArticleDetailPage:::page
ActivityDetailPage:::page
MatchDetailPage:::page
ClubPage:::page
AboutPage:::page
CalendarPage:::page
ActivitiesCalendarPage:::page
TablesPage:::page
MatchesPage:::page
PlayersPage:::page
PlayerDetailPage:::page
SponsorsPage:::page
ContactPage:::page
GalleryPage:::page
AlbumDetailPage:::page
VideosPage:::page
SearchPage:::page
ClothingPage:::page
PollsPage:::page
OverlayScoreboardPage:::page
OverlaySponsorsPage:::page
ForbiddenPage:::page
SetupPage:::page
StylePreviewPage:::page
AuthPage:::page
RegisterPage:::page
ForgotPasswordPage:::page
ResetPasswordPage:::page
NewsletterUnsubscribePage:::page
NewsletterPreferencesPage:::page
SemiAdminPage:::page
end
%% Admin Pages
subgraph Admin[Admin]
AdminDashboardPage:::page
AdminDocsPage:::page
AboutAdminPage:::page
AdminVideosPage:::page
GalleryAdminPage:::page
AdminMerchPage:::page
SponsorsAdminPage:::page
MatchesAdminPage:::page
PlayersAdminPage:::page
TeamsAdminPage:::page
UsersAdminPage:::page
BannersAdminPage:::page
MessagesAdminPage:::page
SettingsAdminPage:::page
NewsletterAdminPage:::page
PollsAdminPage:::page
CompetitionAliasesAdminPage:::page
PrefetchAdminPage:::page
AdminResetPasswordPage:::page
ScoreboardAdminPage:::page
MobileScoreboardControlPage:::page
AnalyticsAdminPage:::page
ErrorsAdminPage:::page
FilesAdminPage:::page
ContactsAdminPage:::page
NavigationAdminPage:::page
CommentsAdminPage:::page
ShortlinksAdminPage:::page
EngagementAdminPage:::page
SweepstakesAdminPage:::page
SweepstakeVisualPage:::page
end
%% Components (subset of key ones)
subgraph Components
MainLayout[components/layout/MainLayout]:::comp
ClubHeroTopbar[components/home/ClubHeroTopbar]:::comp
BannerDisplay[components/banners/BannerDisplay]:::comp
BlogCardsScroller[components/home/BlogCardsScroller]:::comp
BlogSwiper[components/home/BlogSwiper]:::comp
VideosSection[components/home/VideosSection]:::comp
MerchSection[components/home/MerchSection]:::comp
PollsWidget[components/home/PollsWidget]:::comp
GallerySection[components/home/GallerySection]:::comp
NewsletterSubscribe[components/newsletter/NewsletterSubscribe]:::comp
NewsList[components/pack/NewsList]:::comp
StandingsCard[components/pack/StandingsCard]:::comp
NextMatch[components/pack/NextMatch]:::comp
MatchesSlider[components/pack/MatchesSlider]:::comp
ActivitiesList[components/pack/ActivitiesList]:::comp
TeamLogo[components/common/TeamLogo]:::comp
SweepstakeWidget[components/sweepstakes/SweepstakeWidget]:::comp
ClubModal[components/home/ClubModal]:::comp
MatchModal[components/home/MatchModal]:::comp
end
HomePage --> MainLayout
HomePage --> ClubHeroTopbar
HomePage --> BannerDisplay
HomePage --> BlogCardsScroller
HomePage --> BlogSwiper
HomePage --> VideosSection
HomePage --> MerchSection
HomePage --> PollsWidget
HomePage --> GallerySection
HomePage --> NewsletterSubscribe
HomePage --> NewsList
HomePage --> StandingsCard
HomePage --> NextMatch
HomePage --> MatchesSlider
HomePage --> ActivitiesList
HomePage -. uses .- TeamLogo
HomePage --> SweepstakeWidget
HomePage --> ClubModal
HomePage --> MatchModal
%% Contexts & Hooks
subgraph Contexts
AuthContext[contexts/AuthContext]:::ctx
ClubThemeContext[contexts/ClubThemeContext]:::ctx
end
subgraph Hooks
usePublicSettings[hooks/usePublicSettings]:::hook
useFontLoader[hooks/useFontLoader]:::hook
useUmami[hooks/useUmami]:::hook
usePageElementConfig[hooks/usePageElementConfig]:::hook
useAllPageElementConfigs[hooks/usePageElementConfig.useAllPageElementConfigs]:::hook
end
Providers --> Contexts
Pages --> Contexts
Pages --> Hooks
%% Services & Utils
subgraph Services
apiCore["services/api (API_URL)"]:::svc
errorReporter[services/errorReporter]:::svc
settingsSvc[services/settings]:::svc
pageElementsSvc[services/pageElements]:::svc
articlesSvc[services/articles]:::svc
playersSvc[services/players]:::svc
sponsorsSvc[services/sponsors]:::svc
bannersSvc[services/banners]:::svc
compAliasesSvc[services/competitionAliases]:::svc
eventsSvc[services/eventService]:::svc
setupSvc[services/setup]:::svc
engagementSvc[services/engagement]:::svc
actionLogSvc[services/actionLog]:::svc
facrApi[services/facr/facrApi]:::svc
end
subgraph Utils
urlUtil[utils/url]:::util
nationalityUtil[utils/nationality]:::util
colorsUtil[utils/colors]:::util
logosUtil[utils/sportLogosAPI]:::util
end
Pages --> apiCore
Pages --> errorReporter
Pages --> settingsSvc
Pages --> pageElementsSvc
Pages --> articlesSvc
Pages --> playersSvc
Pages --> sponsorsSvc
Pages --> bannersSvc
Pages --> compAliasesSvc
Pages --> eventsSvc
ClubThemeContext --> facrApi
ClubThemeContext --> colorsUtil
ClubThemeContext --> logosUtil
Pages --> urlUtil
Pages --> nationalityUtil
Hooks --> settingsSvc
%% Backends
subgraph Backends
Backend[(fotbal-club backend API)]:::infra
ErrorIngest[(errors.tdvorak.dev)]:::infra
FACR[(FACR APIs)]:::infra
end
apiCore --> Backend
errorReporter -. sends .- ErrorIngest
facrApi --> FACR
+69
View File
@@ -0,0 +1,69 @@
flowchart LR
%% Homepage composition (components and data)
classDef page fill:#1c243a,stroke:#4b5b8a,color:#e8eaf0;
classDef comp fill:#1d2a2a,stroke:#3d7a6a,color:#e8eaf0;
classDef svc fill:#0b273f,stroke:#3a72a0,color:#e8eaf0;
classDef ctx fill:#2b233f,stroke:#7a63a0,color:#e8eaf0;
classDef hook fill:#2a2a1f,stroke:#9a8a3d,color:#e8eaf0;
HomePage:::page
HomePage --> MainLayout[components/layout/MainLayout]:::comp
HomePage --> ClubHeroTopbar[components/home/ClubHeroTopbar]:::comp
HomePage --> BannerDisplay[components/banners/BannerDisplay]:::comp
HomePage --> BlogCardsScroller[components/home/BlogCardsScroller]:::comp
HomePage --> BlogSwiper[components/home/BlogSwiper]:::comp
HomePage --> VideosSection[components/home/VideosSection]:::comp
HomePage --> MerchSection[components/home/MerchSection]:::comp
HomePage --> PollsWidget[components/home/PollsWidget]:::comp
HomePage --> GallerySection[components/home/GallerySection]:::comp
HomePage --> NewsletterSubscribe[components/newsletter/NewsletterSubscribe]:::comp
HomePage --> NewsList[components/pack/NewsList]:::comp
HomePage --> StandingsCard[components/pack/StandingsCard]:::comp
HomePage --> NextMatch[components/pack/NextMatch]:::comp
HomePage --> MatchesSlider[components/pack/MatchesSlider]:::comp
HomePage --> ActivitiesList[components/pack/ActivitiesList]:::comp
HomePage --> SweepstakeWidget[components/sweepstakes/SweepstakeWidget]:::comp
HomePage --> ClubModal[components/home/ClubModal]:::comp
HomePage --> MatchModal[components/home/MatchModal]:::comp
HomePage -. uses .- TeamLogo[components/common/TeamLogo]:::comp
%% Data sources
subgraph Services
settingsSvc[services/settings.getPublicSettings]:::svc
pageElementsSvc[services/pageElements.getPageElementConfigs]:::svc
articlesSvc[services/articles]:::svc
playersSvc[services/players]:::svc
sponsorsSvc[services/sponsors]:::svc
bannersSvc[services/banners]:::svc
compAliasesSvc[services/competitionAliases]:::svc
eventsSvc[services/eventService.getUpcomingEvents]:::svc
facrApi[services/facr/facrApi]:::svc
apiCore[services/api - API_URL]:::svc
end
HomePage --> settingsSvc
HomePage --> pageElementsSvc
HomePage --> articlesSvc
HomePage --> playersSvc
HomePage --> sponsorsSvc
HomePage --> bannersSvc
HomePage --> compAliasesSvc
HomePage --> eventsSvc
HomePage --> facrApi
HomePage --> apiCore
%% Contexts & Hooks
subgraph Contexts
AuthContext[contexts/AuthContext]:::ctx
ClubThemeContext[contexts/ClubThemeContext]:::ctx
end
subgraph Hooks
useAllPageElementConfigs[hooks/usePageElementConfig.useAllPageElementConfigs]:::hook
usePublicSettings[hooks/usePublicSettings]:::hook
end
HomePage --> AuthContext
HomePage --> ClubThemeContext
HomePage --> useAllPageElementConfigs
HomePage --> usePublicSettings
+81
View File
@@ -0,0 +1,81 @@
flowchart LR
%% Modules and dependencies (key subset)
classDef svc fill:#0b273f,stroke:#3a72a0,color:#e8eaf0;
classDef util fill:#2b2f3f,stroke:#6a7aa0,color:#e8eaf0;
classDef ctx fill:#2b233f,stroke:#7a63a0,color:#e8eaf0;
classDef hook fill:#2a2a1f,stroke:#9a8a3d,color:#e8eaf0;
classDef page fill:#1c243a,stroke:#4b5b8a,color:#e8eaf0;
subgraph Contexts
AuthContext[contexts/AuthContext]:::ctx
ClubThemeContext[contexts/ClubThemeContext]:::ctx
end
subgraph Hooks
usePublicSettings[hooks/usePublicSettings]:::hook
usePageElementConfig[hooks/usePageElementConfig]:::hook
useAllPageElementConfigs[hooks/usePageElementConfig.useAll]:::hook
useUmami[hooks/useUmami]:::hook
useFontLoader[hooks/useFontLoader]:::hook
end
subgraph Services
apiCore[services/api]:::svc
errorReporter[services/errorReporter]:::svc
settingsSvc[services/settings]:::svc
pageElementsSvc[services/pageElements]:::svc
articlesSvc[services/articles]:::svc
playersSvc[services/players]:::svc
sponsorsSvc[services/sponsors]:::svc
bannersSvc[services/banners]:::svc
compAliasesSvc[services/competitionAliases]:::svc
eventsSvc[services/eventService]:::svc
setupSvc[services/setup]:::svc
engagementSvc[services/engagement]:::svc
actionLogSvc[services/actionLog]:::svc
facrApi[services/facr/facrApi]:::svc
end
subgraph Utils
urlUtil[utils/url]:::util
nationalityUtil[utils/nationality]:::util
colorsUtil[utils/colors]:::util
logosUtil[utils/sportLogosAPI]:::util
end
subgraph Pages
HomePage:::page
BlogPage:::page
ArticleDetailPage:::page
MatchDetailPage:::page
ActivityDetailPage:::page
AdminPages[(Admin Pages...)]:::page
end
HomePage --> settingsSvc
HomePage --> pageElementsSvc
HomePage --> articlesSvc
HomePage --> playersSvc
HomePage --> sponsorsSvc
HomePage --> bannersSvc
HomePage --> compAliasesSvc
HomePage --> eventsSvc
HomePage --> facrApi
Pages --> apiCore
Pages --> errorReporter
Pages --> usePublicSettings
Pages --> usePageElementConfig
Pages --> useUmami
Pages --> useFontLoader
Pages --> urlUtil
Pages --> nationalityUtil
ClubThemeContext --> usePublicSettings
ClubThemeContext --> facrApi
ClubThemeContext --> colorsUtil
ClubThemeContext --> logosUtil
errorReporter -. sends .- ErrorIngest[(errors.tdvorak.dev)]
apiCore -. REST .- Backend[(fotbal-club backend)]
facrApi -. data .- FACR[(FACR APIs)]
+236
View File
@@ -0,0 +1,236 @@
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
flowchart LR
%% Overall Frontend Architecture
classDef cluster fill:#eef7ff,stroke:#2b6cb0,color:#0b3a60;
classDef svc fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
classDef page fill:#fff7ed,stroke:#f59e0b,color:#7c2d12;
classDef comp fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
classDef ctx fill:#f3e8ff,stroke:#6d28d9,color:#3b0764;
classDef hook fill:#fef9c3,stroke:#ca8a04,color:#7c2d12;
classDef infra fill:#e3f2fd,stroke:#1e88e5,color:#0c4a6e;
subgraph Client
Browser((Browser)):::infra
end
Browser --> index_tsx[index.tsx]:::infra
index_tsx --> ErrorBoundary
index_tsx --> AppLazy[App.lazy.tsx]
index_tsx --> ServiceWorker[serviceWorkerRegistration.ts]
index_tsx --> ErrorReporterSvc[services/errorReporter.ts]
subgraph Providers
Chakra[ChakraProvider]:::infra --> RQ[QueryClientProvider]:::infra --> Router[BrowserRouter]:::infra --> AuthProv[AuthProvider]:::ctx --> ClubThemeProv[ClubThemeProvider]:::ctx --> Helmet[HelmetProvider]:::infra --> Suspense:::infra
SEO[DefaultSEO]:::comp
CookieBanner:::comp
end
AppLazy --> Chakra
AppLazy --> SEO
AppLazy --> CookieBanner
Suspense --> Routes
subgraph Routing
Routes:::infra
ProtectedRoute:::comp
end
Routes --> PublicRoutes
Routes --> AdminRoutes
subgraph PublicRoutes[Public Pages]
HomeRoute --> HomePage
HomeRoute --> PremiumHomePage
BlogRoute --> BlogPage
BlogRoute --> PremiumBlogPage
PublicOther[Other Public Routes]
end
subgraph AdminRoutes[Admin Pages]
AdminDashboardPage
AdminContent[Other Admin Pages]
end
Router --> Routes
AuthProv --> ProtectedRoute
%% Pages and components
subgraph Pages[Key Pages]
HomePage:::page
BlogPage:::page
ArticleDetailPage:::page
ActivityDetailPage:::page
MatchDetailPage:::page
ClubPage:::page
AboutPage:::page
CalendarPage:::page
ActivitiesCalendarPage:::page
TablesPage:::page
MatchesPage:::page
PlayersPage:::page
PlayerDetailPage:::page
SponsorsPage:::page
ContactPage:::page
GalleryPage:::page
AlbumDetailPage:::page
VideosPage:::page
SearchPage:::page
ClothingPage:::page
PollsPage:::page
OverlayScoreboardPage:::page
OverlaySponsorsPage:::page
NotFoundPage:::page
ForbiddenPage:::page
SetupPage:::page
StylePreviewPage:::page
AuthPage:::page
RegisterPage:::page
ForgotPasswordPage:::page
ResetPasswordPage:::page
NewsletterUnsubscribePage:::page
NewsletterPreferencesPage:::page
SemiAdminPage:::page
ShortRedirectPage:::page
PremiumHomePage:::page
PremiumBlogPage:::page
end
PublicRoutes --> Pages
AdminRoutes --> AdminContent
subgraph AdminPages[Admin Management]
AdminDashboardPage:::page
AdminDocsPage:::page
AboutAdminPage:::page
AdminVideosPage:::page
GalleryAdminPage:::page
AdminMerchPage:::page
SponsorsAdminPage:::page
MatchesAdminPage:::page
PlayersAdminPage:::page
TeamsAdminPage:::page
UsersAdminPage:::page
BannersAdminPage:::page
MessagesAdminPage:::page
SettingsAdminPage:::page
NewsletterAdminPage:::page
PollsAdminPage:::page
CompetitionAliasesAdminPage:::page
PrefetchAdminPage:::page
AdminResetPasswordPage:::page
ScoreboardAdminPage:::page
MobileScoreboardControlPage:::page
AnalyticsAdminPage:::page
ErrorsAdminPage:::page
FilesAdminPage:::page
ContactsAdminPage:::page
NavigationAdminPage:::page
CommentsAdminPage:::page
ShortlinksAdminPage:::page
EngagementAdminPage:::page
SweepstakesAdminPage:::page
SweepstakeVisualPage:::page
end
AdminContent --> AdminPages
%% Components (subset)
subgraph Components
MainLayout[components/layout/MainLayout]:::comp
ProtectedRoute:::comp
CookieBanner:::comp
DefaultSEO[components/seo/DefaultSEO]:::comp
TeamLogo[components/common/TeamLogo]:::comp
ClubHeroTopbar[components/home/ClubHeroTopbar]:::comp
BannerDisplay[components/banners/BannerDisplay]:::comp
BlogCardsScroller[components/home/BlogCardsScroller]:::comp
BlogSwiper[components/home/BlogSwiper]:::comp
VideosSection[components/home/VideosSection]:::comp
MerchSection[components/home/MerchSection]:::comp
PollsWidget[components/home/PollsWidget]:::comp
GallerySection[components/home/GallerySection]:::comp
NewsletterSubscribe[components/newsletter/NewsletterSubscribe]:::comp
MyUIbrixEditor[components/editor/MyUIbrixEditor]:::comp
MyUIbrixErrorBoundary[components/editor/MyUIbrixErrorBoundary]:::comp
ClubModal[components/home/ClubModal]:::comp
MatchModal[components/home/MatchModal]:::comp
NewsList[components/pack/NewsList]:::comp
StandingsCard[components/pack/StandingsCard]:::comp
NextMatch[components/pack/NextMatch]:::comp
MatchesSlider[components/pack/MatchesSlider]:::comp
ActivitiesList[components/pack/ActivitiesList]:::comp
SweepstakeWidget[components/sweepstakes/SweepstakeWidget]:::comp
end
HomePage --> MainLayout
HomePage --> ClubHeroTopbar
HomePage --> BannerDisplay
HomePage --> BlogCardsScroller
HomePage --> BlogSwiper
HomePage --> VideosSection
HomePage --> MerchSection
HomePage --> PollsWidget
HomePage --> GallerySection
HomePage --> NewsletterSubscribe
HomePage --> NewsList
HomePage --> StandingsCard
HomePage --> NextMatch
HomePage --> MatchesSlider
HomePage --> ActivitiesList
HomePage --> SweepstakeWidget
HomePage --> ClubModal
HomePage --> MatchModal
TeamLogo -. logos .- HomePage
%% Contexts & Hooks
subgraph Contexts
AuthContext[contexts/AuthContext]:::ctx
ClubThemeContext[contexts/ClubThemeContext]:::ctx
end
subgraph Hooks
usePublicSettings[hooks/usePublicSettings]:::hook
usePageElementConfig[hooks/usePageElementConfig]:::hook
useUmami[hooks/useUmami]:::hook
useFontLoader[hooks/useFontLoader]:::hook
end
AuthProv --> AuthContext
ClubThemeProv --> ClubThemeContext
Pages --> AuthContext
Pages --> ClubThemeContext
Pages --> Hooks
%% Services & Data
subgraph Services
settingsSvc[services/settings.ts]:::svc
pageElementsSvc[services/pageElements.ts]:::svc
articlesSvc[services/articles.ts]:::svc
playersSvc[services/players.ts]:::svc
sponsorsSvc[services/sponsors.ts]:::svc
bannersSvc[services/banners.ts]:::svc
compAliasesSvc[services/competitionAliases.ts]:::svc
eventsSvc[services/eventService.ts]:::svc
facrApi[services/facr/facrApi.ts]:::svc
setupSvc[services/setup.ts]:::svc
engagementSvc[services/engagement.ts]:::svc
actionLogSvc[services/actionLog.ts]:::svc
errorReporter[services/errorReporter.ts]:::svc
apiCore[services/api.ts - API_URL]:::svc
end
Components --> Services
Pages --> Services
Hooks --> Services
subgraph Backends
Backend[(fotbal-club backend API)]:::infra
ErrorIngest[(errors.tdvorak.dev)]:::infra
FACR[(FACR APIs)]:::infra
end
Services --> Backend
errorReporter --> ErrorIngest
facrApi --> FACR
apiCore --> Backend
ServiceWorker -.->|PWA| Browser
+94
View File
@@ -0,0 +1,94 @@
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
flowchart TD
%% Routes to Pages Mapping (from App.lazy.tsx)
classDef page fill:#fff7ed,stroke:#f59e0b,color:#7c2d12;
classDef route fill:#e2e8f0,stroke:#475569,color:#111827;
Router[BrowserRouter]:::route --> Routes:::route
subgraph PublicRoutes[Public Routes]
R0["/"]:::route --> HomeRoute:::route --> HomePage:::page
R1["/blog"]:::route --> BlogRoute:::route --> BlogPage:::page
R2["/hledat"]:::route --> SearchPage:::page
R3["/search"]:::route --> SearchPage:::page
R4["/overlay/scoreboard"]:::route --> OverlayScoreboardPage:::page
R5["/overlay/sponsors"]:::route --> OverlaySponsorsPage:::page
R6["/klub"]:::route --> ClubPage:::page
R7["/o-klubu"]:::route --> AboutPage:::page
R8["/kalendar"]:::route --> CalendarPage:::page
R9["/aktivity"]:::route --> ActivitiesCalendarPage:::page
R10["/tabulky"]:::route --> TablesPage:::page
R11["/zapasy"]:::route --> MatchesPage:::page
R12["/players"]:::route --> PlayersPage:::page
R13["/hraci"]:::route --> PlayersPage:::page
R14["/players/:id"]:::route --> PlayerDetailPage:::page
R15["/hraci/:id"]:::route --> PlayerDetailPage:::page
R16["/sponzori"]:::route --> SponsorsPage:::page
R17["/kontakt"]:::route --> ContactPage:::page
R18["/ankety"]:::route --> PollsPage:::page
R19["/galerie"]:::route --> GalleryPage:::page
R20["/galerie/album/:id"]:::route --> AlbumDetailPage:::page
R21["/videa"]:::route --> VideosPage:::page
R22["/obleceni"]:::route --> ClothingPage:::page
%% Legal
R23["/pravidla-cookies"]:::route --> CookiePolicyPage:::page
R24["/obchodni-podminky"]:::route --> TermsPage:::page
R25["/zasady-ochrany-osobnich-udaju"]:::route --> PrivacyPolicyPage:::page
%% Articles and matches
R26["/news"]:::route --> RedirectToBlog((Redirect -> /blog))
R27["/news/:slug"]:::route --> ArticleDetailPage:::page
R28["/articles/slug/:slug"]:::route --> ArticleDetailPage:::page
R29["/articles/:id"]:::route --> ArticleDetailPage:::page
R30["/zapas/:id"]:::route --> MatchDetailPage:::page
R31["/aktivita/:id"]:::route --> ActivityDetailPage:::page
%% Setup & Auth
R32["/setup"]:::route --> SetupPage:::page
R33["/setup/styl"]:::route --> StylePreviewPage:::page
R34["/login"]:::route --> AuthPage:::page
R35["/register"]:::route --> RegisterPage:::page
R36["/forgot-password"]:::route --> ForgotPasswordPage:::page
R37["/reset-password"]:::route --> ResetPasswordPage:::page
R38["/newsletter/unsubscribe/:email"]:::route --> NewsletterUnsubscribePage:::page
R39["/newsletter/preferences"]:::route --> NewsletterPreferencesPage:::page
R40["/403"]:::route --> ForbiddenPage:::page
%% Not found
R99["*"]:::route --> NotFoundRoute:::route --> NotFoundPage:::page
end
subgraph AdminRoutes[Admin Routes - guarded by ProtectedRoute]
A0["/admin"]:::route --> AdminDashboardPage:::page
A1["/admin/docs"]:::route --> AdminDocsPage:::page
A2["/admin/o-klubu"]:::route --> AboutAdminPage:::page
A3["/admin/videa"]:::route --> AdminVideosPage:::page
A4["/admin/galerie"]:::route --> GalleryAdminPage:::page
A5["/admin/obleceni"]:::route --> AdminMerchPage:::page
A6["/admin/sponzori"]:::route --> SponsorsAdminPage:::page
A7["/admin/zapasy"]:::route --> MatchesAdminPage:::page
A8["/admin/hraci"]:::route --> PlayersAdminPage:::page
A9["/admin/tymy"]:::route --> TeamsAdminPage:::page
A10["/admin/uzivatele"]:::route --> UsersAdminPage:::page
A11["/admin/bannery"]:::route --> BannersAdminPage:::page
A12["/admin/zpravy"]:::route --> MessagesAdminPage:::page
A13["/admin/nastaveni"]:::route --> SettingsAdminPage:::page
A14["/admin/newsletter"]:::route --> NewsletterAdminPage:::page
A15["/admin/ankety"]:::route --> PollsAdminPage:::page
A16["/admin/aliasy-soutezi"]:::route --> CompetitionAliasesAdminPage:::page
A17["/admin/prefetch"]:::route --> PrefetchAdminPage:::page
A18["/admin/users/send-reset"]:::route --> AdminResetPasswordPage:::page
A19["/admin/scoreboard"]:::route --> ScoreboardAdminPage:::page
A20["/admin/scoreboard/remote"]:::route --> MobileScoreboardControlPage:::page
A21["/admin/analytika"]:::route --> AnalyticsAdminPage:::page
A22["/admin/errors"]:::route --> ErrorsAdminPage:::page
A23["/admin/soubory"]:::route --> FilesAdminPage:::page
A24["/admin/kontakty"]:::route --> ContactsAdminPage:::page
A25["/admin/navigace"]:::route --> NavigationAdminPage:::page
A26["/admin/komentare"]:::route --> CommentsAdminPage:::page
A27["/admin/shortlinks"]:::route --> ShortlinksAdminPage:::page
A28["/admin/engagement"]:::route --> EngagementAdminPage:::page
A29["/admin/sweepstakes"]:::route --> SweepstakesAdminPage:::page
A30["/admin/sweepstakes/:id/visual"]:::route --> SweepstakeVisualPage:::page
end
-72
View File
@@ -1,72 +0,0 @@
# Frontpage Data Map
```mermaid
graph TB
Home[Homepage sections]
News[News (Articles)]
Matches[Upcoming & recent matches]
TableSec[Standings / Tables]
Activities[Activities (Events)]
GallerySec[Gallery]
VideosSec[Videos]
PlayersSec[Players]
SponsorsSec[Sponsors]
MerchSec[Merch]
PollsSec[Polls]
MapSec[Club Map]
NewsletterSec[Newsletter box]
BannersSec[Banners]
Home --> News
Home --> Matches
Home --> TableSec
Home --> Activities
Home --> GallerySec
Home --> VideosSec
Home --> PlayersSec
Home --> SponsorsSec
Home --> MerchSec
Home --> PollsSec
Home --> MapSec
Home --> NewsletterSec
Home --> BannersSec
ARTICLES["Articles"]
FACR_API["FACR API"]
MATCH_OVERRIDES["Match overrides"]
COMPETITION_ALIASES["Competition aliases"]
EVENTS["Events"]
SETTINGS["Settings"]
ZONERAMA["Zonerama"]
YOUTUBE["YouTube"]
TEAMS["Teams"]
DB_PLAYERS["Players"]
SPONSORS["Sponsors"]
CLOTHING["Clothing"]
POLLS["Polls"]
POLL_OPTIONS["Poll options"]
GOOGLE_MAPS["Google Maps"]
NEWSLETTER_SUBSCRIPTIONS["Newsletter subscriptions"]
BANNERS["Banners"]
News -->|DB| ARTICLES
Matches -->|Source| FACR_API
Matches -->|Overrides| MATCH_OVERRIDES
TableSec -->|Source| FACR_API
TableSec -->|Aliases| COMPETITION_ALIASES
Activities -->|DB| EVENTS
GallerySec -->|Profile URL| SETTINGS
GallerySec --> ZONERAMA
VideosSec -->|Config| SETTINGS
VideosSec --> YOUTUBE
PlayersSec --> TEAMS
PlayersSec --> DB_PLAYERS
SponsorsSec -->|DB| SPONSORS
MerchSec -->|DB| CLOTHING
PollsSec -->|DB| POLLS
PollsSec --> POLL_OPTIONS
MapSec -->|lat/lng + style| SETTINGS
MapSec --> GOOGLE_MAPS
NewsletterSec -->|subscribe| NEWSLETTER_SUBSCRIPTIONS
BannersSec -->|DB| BANNERS
```
-68
View File
@@ -1,68 +0,0 @@
graph TB
Home[Homepage sections]
News[News Articles]
Matches[Upcoming and recent matches]
TableSec[Standings Tables]
Activities[Activities Events]
GallerySec[Gallery]
VideosSec[Videos]
PlayersSec[Players]
SponsorsSec[Sponsors]
MerchSec[Merch]
PollsSec[Polls]
MapSec[Club Map]
NewsletterSec[Newsletter]
BannersSec[Banners]
Home --> News
Home --> Matches
Home --> TableSec
Home --> Activities
Home --> GallerySec
Home --> VideosSec
Home --> PlayersSec
Home --> SponsorsSec
Home --> MerchSec
Home --> PollsSec
Home --> MapSec
Home --> NewsletterSec
Home --> BannersSec
ARTICLES[Articles]
FACR_API[FACR API]
MATCH_OVERRIDES[Match overrides]
COMPETITION_ALIASES[Competition aliases]
EVENTS[Events]
SETTINGS[Settings]
ZONERAMA[Zonerama]
YOUTUBE[YouTube]
TEAMS[Teams]
DB_PLAYERS[Players]
SPONSORS[Sponsors]
CLOTHING[Clothing]
POLLS[Polls]
POLL_OPTIONS[Poll options]
GOOGLE_MAPS[Google Maps]
NEWSLETTER_SUBSCRIPTIONS[Newsletter subscriptions]
BANNERS[Banners]
News -->|DB| ARTICLES
Matches -->|Source| FACR_API
Matches -->|Overrides| MATCH_OVERRIDES
TableSec -->|Source| FACR_API
TableSec -->|Aliases| COMPETITION_ALIASES
Activities -->|DB| EVENTS
GallerySec -->|Profile URL| SETTINGS
GallerySec --> ZONERAMA
VideosSec -->|Config| SETTINGS
VideosSec --> YOUTUBE
PlayersSec --> TEAMS
PlayersSec --> DB_PLAYERS
SponsorsSec -->|DB| SPONSORS
MerchSec -->|DB| CLOTHING
PollsSec -->|DB| POLLS
PollsSec --> POLL_OPTIONS
MapSec -->|lat lng and style| SETTINGS
MapSec --> GOOGLE_MAPS
NewsletterSec -->|subscribe| NEWSLETTER_SUBSCRIPTIONS
BannersSec -->|DB| BANNERS
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

+43
View File
@@ -0,0 +1,43 @@
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
autonumber
participant Admin as Admin (GalleryAdminPage)
participant FE as Frontend Services (gallery)
participant BE as Backend API (GalleryController)
participant Z as Zonerama API
participant DB as Postgres
Note over Admin,BE: Profile & refresh
FE->>BE: GET /api/v1/admin/gallery/profile
BE-->>FE: Zonerama profile (cached)
FE->>BE: POST /api/v1/admin/gallery/refresh
BE->>Z: Fetch albums/photos
Z-->>BE: Payload
BE->>DB: Upsert albums/photos
BE-->>FE: 200 OK
rect rgba(230,255,230,0.2)
Note over Admin,BE: Fetch single album
FE->>BE: POST /api/v1/admin/gallery/albums/fetch {id}
BE->>Z: GET album by id
Z-->>BE: Album data
BE->>DB: Upsert album
BE-->>FE: 200 OK
end
rect rgba(230,230,255,0.2)
Note over FE,BE: Public endpoints
FE->>BE: GET /api/v1/gallery/albums
BE->>DB: List albums
BE-->>FE: Albums
FE->>BE: GET /api/v1/gallery/albums/:id
BE->>DB: Album+photos
BE-->>FE: Album detail
FE->>BE: GET /api/v1/gallery/proxy-image?url=...
BE->>Z: Download image (proxy)
Z-->>BE: Binary
BE-->>FE: Image
end
+260
View File
@@ -0,0 +1,260 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fotbal Club — All Diagrams</title>
<style>
:root{
--bg:#0f1115;--panel:#0f131f;--text:#e8eaf0;--muted:#9aa3b2;--primary:#0b5cff;--border:#212736;--badge:#1b2440;
}
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
header{position:sticky;top:0;z-index:10;background:#0d1017;border-bottom:1px solid var(--border);padding:12px 16px}
header h1{margin:0;font-size:16px;font-weight:700}
.filters{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:8px}
.filters input[type="search"], .filters select{background:#0f1420;color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 10px}
.filters .chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:16px;padding:6px 10px;background:#0f1420;cursor:pointer}
.filters .chip input{margin:0}
.filters .sp{flex:1}
.btn{appearance:none;border:1px solid var(--border);background:#11182a;color:var(--text);padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600;font-size:12px}
.btn.primary{background:var(--primary);border-color:var(--primary);color:#fff}
.btn.ghost{background:transparent}
main{padding:16px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:16px}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
.card header{display:flex;align-items:center;gap:8px;justify-content:space-between;background:#0f131f;border-bottom:1px solid var(--border);padding:10px 12px;position:static}
.title{display:flex;flex-direction:column;gap:4px}
.title h2{margin:0;font-size:14px}
.meta{color:var(--muted);font-size:11px}
.badge{background:var(--badge);border:1px solid var(--border);border-radius:999px;padding:2px 8px;font-size:10px;color:#bcd}
.diagram-wrap{position:relative;overflow:auto;min-height:320px;background:#fff}
.diagram{padding:16px;min-height:320px}
.toolbar{display:flex;gap:8px;align-items:center;padding:8px 12px;border-top:1px solid var(--border);background:#0f131f;flex-wrap:wrap}
.toolbar .sp{flex:1}
.diagram svg{max-width:100%;height:auto;background:#fff}
.diagram svg text{fill:#111827 !important}
.diagram svg .edgePath path, .diagram svg .flowchart-link{stroke:#334155 !important}
.diagram svg .node > * { transition: filter .2s ease }
.diagram svg .node:hover { filter: drop-shadow(0 0 6px var(--primary)) }
</style>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({ startOnLoad:false, securityLevel:'loose', theme:'dark', flowchart:{ curve:'basis', useMaxWidth:true } });
async function renderMermaidFile(mmdPath, container){
try{
container.innerHTML = '<div style="padding:16px;color:#9aa3b2">Loading '+mmdPath+'…</div>';
const res = await fetch(mmdPath, { cache: 'no-store' });
if(!res.ok) throw new Error('Failed to load '+mmdPath+': '+res.status);
const code = await res.text();
const id = 'm-'+Math.random().toString(36).slice(2);
const { svg } = await mermaid.render(id, code);
container.innerHTML = svg;
const svgEl = container.querySelector('svg');
if(svgEl){
svgEl.style.maxWidth = '100%';
svgEl.style.width = '100%';
svgEl.style.height = 'auto';
svgEl.removeAttribute('width');
svgEl.removeAttribute('height');
}
}catch(e){ container.innerHTML = '<div style="padding:16px;color:#ef4444">Error: '+(e && e.message ? e.message : e)+'</div>'; }
}
function downloadSVGOf(container, filename){
const svg = container.querySelector('svg');
if(!svg) return;
const serializer = new XMLSerializer();
let source = serializer.serializeToString(svg);
if(!source.match(/^<svg[^>]+xmlns=/)) source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
source = '<?xml version="1.0" standalone="no"?>\n'+source;
const blob = new Blob([source], { type:'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = filename; a.click();
setTimeout(() => URL.revokeObjectURL(url), 4000);
}
function openSVGInNewTab(container){
const svg = container.querySelector('svg');
if(!svg) return;
const serializer = new XMLSerializer();
let source = serializer.serializeToString(svg);
if(!source.match(/^<svg[^>]+xmlns=/)) source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
source = '<?xml version="1.0" standalone="no"?>\n'+source;
// Inject white background and readable styles for new tab view
const firstGt = source.indexOf('>');
if(firstGt > 0){
const inject = '<rect width="100%" height="100%" fill="#ffffff"/><style>text{fill:#111827}.edgePath path,.flowchart-link{stroke:#334155}</style>';
source = source.slice(0, firstGt+1) + inject + source.slice(firstGt+1);
}
const blob = new Blob([source], { type:'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
setTimeout(() => URL.revokeObjectURL(url), 10000);
}
const ALL_DIAGRAMS = [
// System & DB
{ id:'system-clean', label:'System Overview (Clean)', file:'system-overall-clean.mmd', cat:'System', tags:['overview','recommended','big'] },
{ id:'system', label:'System Overview (Classic)', file:'system-overall.mmd', cat:'System', tags:['overview','big'], defaultWires:'faint' },
{ id:'db-er', label:'Database ER', file:'db-er.mmd', cat:'System', tags:['db'] },
{ id:'db-models', label:'Database Models', file:'db-models.mmd', cat:'System', tags:['db'] },
// Backend
{ id:'be-routes', label:'Backend Routes Overview', file:'backend-routes-overview.mmd', cat:'Backend', tags:['routes'] },
{ id:'be-packages', label:'Backend Packages', file:'backend-packages.mmd', cat:'Backend', tags:['packages'] },
{ id:'be-mw', label:'Backend Middleware Pipeline', file:'backend-middleware-pipeline.mmd', cat:'Backend', tags:['middleware'] },
{ id:'be-jobs', label:'Backend Background Jobs', file:'backend-jobs.mmd', cat:'Backend', tags:['jobs'] },
{ id:'auth', label:'Auth Flow', file:'auth-flow.mmd', cat:'Backend', tags:['auth','flow'] },
{ id:'err-flow', label:'Error Tracking Flow', file:'error-tracking-flow.mmd', cat:'Backend', tags:['errors','flow'] },
// Frontend
{ id:'fe-everything', label:'Frontend — Everything (Big)', file:'frontend-everything.mmd', cat:'Frontend', tags:['overview','big'], defaultWires:'faint' },
{ id:'fe-overall', label:'Frontend — Overall', file:'frontend-overall.mmd', cat:'Frontend', tags:['architecture'] },
{ id:'fe-routes', label:'Frontend — Routes', file:'frontend-routes.mmd', cat:'Frontend', tags:['routes'] },
{ id:'fe-home', label:'Frontend — Homepage', file:'frontend-homepage.mmd', cat:'Frontend', tags:['homepage'] },
{ id:'fe-modules', label:'Frontend — Modules', file:'frontend-modules.mmd', cat:'Frontend', tags:['modules'] },
{ id:'fe-arch', label:'Frontend — Provider Tree', file:'frontend-architecture.mmd', cat:'Frontend', tags:['providers'] },
{ id:'fe-api', label:'Frontend — API Map', file:'frontend-api-map.mmd', cat:'Frontend', tags:['api'] },
// Admin
{ id:'admin-overall', label:'Admin — Overall', file:'admin-overall.mmd', cat:'Admin', tags:['admin','overview'], defaultWires:'faint' },
{ id:'scoreboard', label:'Scoreboard Flow', file:'scoreboard-flow.mmd', cat:'Admin', tags:['scoreboard','flow'] },
{ id:'newsletter', label:'Newsletter Flow', file:'newsletter-flow.mmd', cat:'Admin', tags:['newsletter','flow'] },
{ id:'comments', label:'Comments Flow', file:'comments-flow.mmd', cat:'Admin', tags:['comments','flow'] },
{ id:'gallery-zonerama', label:'Gallery (Zonerama) Flow', file:'gallery-zonerama-flow.mmd', cat:'Admin', tags:['gallery','flow'] },
{ id:'shortlinks', label:'Shortlinks Flow', file:'shortlinks-flow.mmd', cat:'Admin', tags:['shortlinks','flow'] },
{ id:'upload-flow', label:'Upload Flow', file:'upload-flow.mmd', cat:'Admin', tags:['upload','flow'] },
];
function createCard(d){
const sec = document.createElement('section');
sec.className = 'card';
sec.id = 'card-'+d.id;
sec.dataset.cat = d.cat;
sec.dataset.tags = (d.tags||[]).join(',');
// No default wires styling; keep full visibility
const h = document.createElement('header');
const title = document.createElement('div'); title.className = 'title';
const h2 = document.createElement('h2'); h2.textContent = d.label; title.appendChild(h2);
const meta = document.createElement('div'); meta.className='meta'; meta.textContent = d.file + ' • ' + d.cat; title.appendChild(meta);
const right = document.createElement('div');
const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = d.cat; right.appendChild(badge);
h.appendChild(title); h.appendChild(right);
const wrap = document.createElement('div'); wrap.className='diagram-wrap';
const diag = document.createElement('div'); diag.className='diagram'; diag.dataset.file = d.file; wrap.appendChild(diag);
const tb = document.createElement('div'); tb.className='toolbar';
tb.innerHTML = `
<label><input type="checkbox" class="fit" checked> Fit width</label>
<a class="btn ghost src" href="${d.file}" target="_blank">Source</a>
<span class="sp"></span>
<button class="btn open">Open SVG in new tab</button>
<button class="btn refresh">Refresh</button>
<button class="btn primary download">Download SVG</button>`;
sec.appendChild(h); sec.appendChild(wrap); sec.appendChild(tb);
return sec;
}
function applyFitZoomFor(card){
const container = card.querySelector('.diagram');
const svg = container?.querySelector('svg');
if(!svg) return;
const fit = card.querySelector('.fit');
if(fit && fit.checked){ svg.style.width='100%'; svg.style.height='auto'; } else { svg.style.width=''; svg.style.height=''; }
svg.style.transformOrigin = '';
svg.style.transform = '';
}
function wireCardControls(card, file){
const diag = card.querySelector('.diagram');
card.dataset.file = file;
const fit = card.querySelector('.fit');
const openBtn = card.querySelector('.open');
const refresh = card.querySelector('.refresh');
const download = card.querySelector('.download');
fit.addEventListener('change', () => applyFitZoomFor(card));
openBtn.addEventListener('click', () => openSVGInNewTab(diag));
refresh.addEventListener('click', async () => { diag.dataset.rendered=''; await renderMermaidFile(file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card); });
download.addEventListener('click', () => downloadSVGOf(diag, (file.replace('.mmd','')||'diagram')+'.svg'));
}
let observer;
function setupObserver(){
if(observer) observer.disconnect();
observer = new IntersectionObserver(entries => {
entries.forEach(async e => {
if(e.isIntersecting){
const card = e.target; const diag = card.querySelector('.diagram');
if(diag && !diag.dataset.rendered){
await renderMermaidFile(diag.dataset.file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card);
}
}
});
}, { root:null, rootMargin:'200px', threshold:0 });
document.querySelectorAll('.card').forEach(c => observer.observe(c));
}
function buildGrid(){
const grid = document.getElementById('grid');
grid.innerHTML='';
for(const d of ALL_DIAGRAMS){ const card = createCard(d); grid.appendChild(card); wireCardControls(card, d.file); }
setupObserver();
}
function applyFilters(){
const q = (document.getElementById('search').value || '').toLowerCase();
const cats = Array.from(document.querySelectorAll('input[name="cat-filter"]:checked')).map(i=>i.value);
document.querySelectorAll('.card').forEach(card => {
const label = card.querySelector('h2').textContent.toLowerCase();
const file = card.dataset.file || '';
const cat = card.dataset.cat;
const okCat = cats.length===0 || cats.includes(cat);
const okText = !q || label.includes(q) || file.toLowerCase().includes(q) || (card.dataset.tags||'').toLowerCase().includes(q);
card.style.display = (okCat && okText) ? '' : 'none';
});
}
function globalAction(action){
if(action==='refresh-visible'){
document.querySelectorAll('.card').forEach(async card => {
if(card.offsetParent!==null){
const diag = card.querySelector('.diagram');
if(diag){ diag.dataset.rendered=''; await renderMermaidFile(diag.dataset.file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card); }
}
});
}
if(action==='expand-all'){ document.querySelectorAll('.diagram-wrap').forEach(w => w.style.maxHeight=''); }
if(action==='collapse-all'){ document.querySelectorAll('.diagram-wrap').forEach(w => w.style.maxHeight='200px'); }
}
window.addEventListener('DOMContentLoaded', () => {
buildGrid();
document.querySelectorAll('input[name="cat-filter"]').forEach(i=> i.addEventListener('change', applyFilters));
document.getElementById('search').addEventListener('input', applyFilters);
document.getElementById('refreshVisible').addEventListener('click', () => globalAction('refresh-visible'));
document.getElementById('expandAll').addEventListener('click', () => globalAction('expand-all'));
document.getElementById('collapseAll').addEventListener('click', () => globalAction('collapse-all'));
});
</script>
</head>
<body>
<header>
<h1>Fotbal Club — Unified Diagrams</h1>
<div class="filters">
<input id="search" type="search" placeholder="Search diagrams, files, tags…" />
<label class="chip"><input type="checkbox" name="cat-filter" value="System" /> System</label>
<label class="chip"><input type="checkbox" name="cat-filter" value="Backend" /> Backend</label>
<label class="chip"><input type="checkbox" name="cat-filter" value="Frontend" /> Frontend</label>
<label class="chip"><input type="checkbox" name="cat-filter" value="Admin" /> Admin</label>
<span class="sp"></span>
<button class="btn" id="refreshVisible">Refresh visible</button>
<button class="btn ghost" id="expandAll">Expand all</button>
<button class="btn ghost" id="collapseAll">Collapse all</button>
</div>
</header>
<main>
<div id="grid" class="grid"></div>
</main>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
autonumber
participant Admin as Admin (NewsletterAdminPage)
participant FE as Frontend Services (admin/newsletter.ts)
participant BE as Backend /api/v1/admin/newsletter
participant Email as EmailService (SMTP)
Note over Admin,BE: Manual send/test/preview/status
Admin->>FE: Click Send/Preview/Test
FE->>BE: POST /newsletter/send | /preview | /test
BE->>BE: Build content (newsletter_content)
BE->>Email: Send via SMTP (per-recipient)
Email-->>BE: Accepted/Failed
BE-->>FE: Result + stats
Note over FE,BE: Status & stats
FE->>BE: GET /newsletter/status
BE-->>FE: Enabled/automation state
FE->>BE: GET /newsletter/stats/recent
BE-->>FE: List recent sends, events
rect rgba(225,255,225,0.2)
Note over BE,Email: Automation (weekly, match alerts, blog notifications, results)
BE->>BE: NewsletterScheduler tick
BE->>BE: NewsletterAutomation decides digests/reminders
BE->>Email: Send batches
Email-->>BE: Delivery responses
end
+2 -1
View File
@@ -1,3 +1,4 @@
{
"args": ["--no-sandbox", "--disable-setuid-sandbox"]
"args": ["--no-sandbox", "--disable-setuid-sandbox"],
"headless": true
}
+3
View File
@@ -0,0 +1,3 @@
{
"args": ["--no-sandbox", "--disable-setuid-sandbox"]
}
+101
View File
@@ -0,0 +1,101 @@
%%{init: {'theme': 'forest', 'flowchart': { 'curve': 'linear' }}}%%
flowchart LR
classDef page fill:#e0f2fe,stroke:#0369a1,color:#0c4a6e;
classDef admin fill:#dcfce7,stroke:#16a34a,color:#065f46;
classDef api fill:#eef2ff,stroke:#6366f1,color:#312e81;
classDef pub fill:#f1f5f9,stroke:#334155,color:#0f172a;
classDef db fill:#fde68a,stroke:#ca8a04,color:#7c2d12;
classDef ext fill:#faf5ff,stroke:#a855f7,color:#6b21a8;
subgraph FE_Pages["Frontend Pages"]
direction TB
p_admin["Admin Scoreboard Page"]:::page
p_mobile["Mobile Scoreboard Control Page"]:::page
p_overlay_sb["Overlay Scoreboard Page"]:::page
p_overlay_sp["Overlay Sponsors Page"]:::page
end
subgraph BE_API["Backend API /api/v1"]
direction TB
a_pub["Public"]:::api
a_admin["Admin"]:::api
%% Public endpoints
e_sb_get["GET /scoreboard"]:::pub
e_sb_colors["GET /scoreboard/colors/derive"]:::pub
e_sb_sponsors["GET /scoreboard/sponsors"]:::pub
e_sb_qr_get["GET /scoreboard/qr"]:::pub
%% Admin endpoints
e_admin_get["GET /admin/scoreboard"]:::admin
e_admin_put["PUT /admin/scoreboard"]:::admin
e_timer_start["POST /admin/scoreboard/timer/start"]:::admin
e_timer_pause["POST /admin/scoreboard/timer/pause"]:::admin
e_timer_reset["POST /admin/scoreboard/timer/reset"]:::admin
e_swap_sides["POST /admin/scoreboard/swap-sides"]:::admin
e_second_half["POST /admin/scoreboard/second-half"]:::admin
e_save["POST /admin/scoreboard/save"]:::admin
e_saves_list["GET /admin/scoreboard/saves"]:::admin
e_load["POST /admin/scoreboard/load"]:::admin
e_sp_list["GET /admin/scoreboard/sponsors"]:::admin
e_sp_upload["POST /admin/scoreboard/sponsors/upload"]:::admin
e_sp_prefill["POST /admin/scoreboard/sponsors/prefill"]:::admin
e_sp_delete["DELETE /admin/scoreboard/sponsors"]:::admin
e_qr_get["GET /admin/scoreboard/qr"]:::admin
e_qr_upload["POST /admin/scoreboard/qr"]:::admin
end
subgraph DATA["Data Storage"]
direction TB
d_state["ScoreboardState (DB)"]:::db
d_sponsor_files["UploadedFile + FileUsage (Sponsors)"]:::db
end
%% FE -> Public
p_overlay_sb --> e_sb_get
p_overlay_sp --> e_sb_sponsors
p_overlay_sb -. derive colors .-> e_sb_colors
p_overlay_sb -. QR overlay uses .-> e_sb_qr_get
%% FE -> Admin (both pages use same admin endpoints)
p_admin --> e_admin_get
p_admin --> e_admin_put
p_admin --> e_timer_start
p_admin --> e_timer_pause
p_admin --> e_timer_reset
p_admin --> e_swap_sides
p_admin --> e_second_half
p_admin --> e_save
p_admin --> e_saves_list
p_admin --> e_load
p_admin --> e_sp_list
p_admin --> e_sp_upload
p_admin --> e_sp_prefill
p_admin --> e_sp_delete
p_admin --> e_qr_get
p_admin --> e_qr_upload
p_mobile --> e_timer_start
p_mobile --> e_timer_pause
p_mobile --> e_timer_reset
p_mobile --> e_swap_sides
p_mobile --> e_second_half
%% API -> Data
e_admin_put --> d_state
e_timer_start --> d_state
e_timer_pause --> d_state
e_timer_reset --> d_state
e_swap_sides --> d_state
e_second_half --> d_state
e_save --> d_state
e_load --> d_state
e_sp_upload --> d_sponsor_files
e_sp_delete --> d_sponsor_files
e_sp_prefill --> d_sponsor_files
%% Public reads from DB-backed state/caches
e_sb_get -. read .- d_state
e_sb_sponsors -. read .- d_state
e_sb_qr_get -. read .- d_state
+38
View File
@@ -0,0 +1,38 @@
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
autonumber
participant Visitor as Visitor
participant Editor as Editor/Admin
participant FE as Frontend
participant BE as Backend API
participant DB as Postgres
Note over Visitor,BE: Public create (same-site only)
FE->>BE: POST /api/v1/shortlinks/public {url, note}
BE->>BE: RateLimit(30/min) + origin checks
BE->>DB: Insert ShortLink
DB-->>BE: id, code
BE-->>FE: 201 Created {code, short_url}
rect rgba(220,255,220,0.2)
Note over Editor,BE: Editor/Admin management
FE->>BE: POST /api/v1/shortlinks (editor)
BE->>DB: Insert ShortLink
BE-->>FE: 201 Created
FE->>BE: GET /api/v1/shortlinks (editor)
BE->>DB: List
BE-->>FE: 200 OK [shortlinks]
FE->>BE: GET /api/v1/admin/shortlinks/:id/stats (admin)
BE->>DB: Aggregate LinkClick
BE-->>FE: 200 OK {stats}
end
rect rgba(230,230,255,0.2)
Note over FE,BE: Public redirect
FE->>BE: GET /s/:code (root)
BE->>DB: Lookup ShortLink by code
BE->>DB: Insert LinkClick
BE-->>FE: 302 Redirect to target
end
-56
View File
@@ -1,56 +0,0 @@
# System Architecture
```mermaid
graph LR
subgraph Clients
A["Public site React SPA"]
B["Admin SPA"]
C["Scoreboard Overlay"]
end
subgraph Frontend
FE["React 18 + Chakra UI; Router + Query"]
end
subgraph Backend
BE["Go Gin REST API api v1; GORM services"]
JOBS["Background jobs; Prefetcher; Newsletter automation"]
end
subgraph Data
DB["PostgreSQL"]
UP["uploads static dist"]
end
subgraph Integrations_optional
FACR["FACR API"]
YT["YouTube API"]
ZON["Zonerama"]
SMTP["SMTP email"]
MAPS["Google Maps"]
UMAMI["Umami Analytics"]
end
A --> FE
B --> FE
C --> FE
FE -->|REST JSON| BE
FE -->|uploads static| UP
BE --> DB
BE --> UP
%% External calls
BE --> FACR
BE --> YT
BE --> ZON
BE --> SMTP
BE -. "telemetry" .-> UMAMI
BE --> MAPS
%% Jobs
JOBS --> BE
JOBS --> DB
JOBS --> SMTP
```
-52
View File
@@ -1,52 +0,0 @@
graph LR
subgraph Clients
A["Public site React SPA"]
B["Admin SPA"]
C["Scoreboard Overlay"]
end
subgraph Frontend
FE["React 18 + Chakra UI; Router + Query"]
end
subgraph Backend
BE["Go Gin REST API api v1; GORM services"]
JOBS["Background jobs; Prefetcher; Newsletter automation"]
end
subgraph Data
DB["PostgreSQL"]
UP["uploads static dist"]
end
subgraph Integrations_optional
FACR["FACR API"]
YT["YouTube API"]
ZON["Zonerama"]
SMTP["SMTP email"]
MAPS["Google Maps"]
UMAMI["Umami Analytics"]
end
A --> FE
B --> FE
C --> FE
FE -->|REST JSON| BE
FE -->|uploads static| UP
BE --> DB
BE --> UP
%% External calls
BE --> FACR
BE --> YT
BE --> ZON
BE --> SMTP
BE -. "telemetry" .-> UMAMI
BE --> MAPS
%% Jobs
JOBS --> BE
JOBS --> DB
JOBS --> SMTP
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

+25
View File
@@ -0,0 +1,25 @@
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
flowchart LR
classDef client fill:#f1f5f9,stroke:#334155,color:#0f172a;
classDef fe fill:#fff7ed,stroke:#f59e0b,color:#7c2d12;
classDef be fill:#ecfdf5,stroke:#16a34a,color:#065f46;
classDef db fill:#e3f2fd,stroke:#1e88e5,color:#0c4a6e;
classDef ext fill:#f5f3ff,stroke:#8b5cf6,color:#4c1d95;
classDef stat fill:#e2e8f0,stroke:#475569,color:#111827;
U((User Browser)):::client
FE[Frontend (React app)]:::fe
API[Backend API (Go + Gin)\n/api/v1]:::be
DB[(PostgreSQL DB)]:::db
STATIC[Static & Uploads\n/assets, /uploads]:::stat
EXT[External Services\n(SMTP, Error Receiver, Umami, FACR, Zonerama, YouTube)]:::ext
U --> FE
FE ==>|HTTP| API
API --> DB
API <-->|read/write| STATIC
API --> EXT
%% Optional: public static reads
U -.->|static files| STATIC
+378
View File
@@ -0,0 +1,378 @@
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":"svg { font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; } .edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } .animated-edge path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } } .ext > rect, .ext > polygon, .ext > path { stroke: #7e57c2; } .db > rect, .db > polygon, .db > path { fill: #e3f2fd; stroke: #1e88e5; } .svc > rect, .svc > polygon, .svc > path { fill: #e8f5e9; stroke: #43a047; } .fe > rect, .fe > polygon, .fe > path { fill: #fff8e1; stroke: #f9a825; } .ctrl > rect, .ctrl > polygon, .ctrl > path { fill: #f3e5f5; stroke: #8e24aa; } .mid > rect, .mid > polygon, .mid > path { fill: #e0f2f1; stroke: #00897b; } .model > rect, .model > polygon, .model > path { fill: #ede7f6; stroke: #5e35b1; } .route > rect, .route > polygon, .route > path { fill: #e8eaf6; stroke: #3f51b5; }" }}%%
flowchart TB
%% ========================= Docker & Runtime =========================
subgraph DOCKER["Docker Compose (Local Dev/Prod)"]
direction TB
docker_net([Bridge Network: fotbal-network]):::svc
docker_vol1["Volume: postgres_data"]:::svc
docker_vol2["Bind: ./uploads -> /app/uploads"]:::svc
docker_vol3["Bind: ./cache -> /app/cache"]:::svc
subgraph docker_backend["backend (Go) container"]
direction TB
be_8080["Expose 8080:8080"]
be_env[".env + overrides"]
be_cmd["command: ./main"]
end
subgraph docker_frontend["frontend (Nginx) container"]
direction TB
fe_3000["Expose 3000:80"]
fe_env[".env.frontend"]
end
subgraph docker_db["postgres:15-alpine"]
direction TB
db_5432["Expose 5432:5432"]
db_env["POSTGRES_* env"]
end
docker_net --- docker_backend
docker_net --- docker_frontend
docker_net --- docker_db
docker_vol1 --- docker_db
docker_vol2 --- docker_backend
docker_vol3 --- docker_backend
end
user_browser((User Browser)):::ext
user_browser ==>|HTTP 80| docker_frontend:::animated-edge
user_browser -.->|dev direct (HTTP 8080)| docker_backend
%% ========================= Backend (Go/Gin) =========================
subgraph BACKEND["Backend Service (Golang + Gin) :8080"]
direction TB
cfg[Config (internal/config.Config)\n- APP_ENV/PORT/DEBUG\n- DATABASE_URL (GORM)\n- JWT_SECRET/EXP\n- ALLOWED_ORIGINS (CORS)\n- UPLOAD_DIR/MAX_UPLOAD_SIZE\n- SMTP_* (Email)\n- FRONTEND_BASE_URL\n- PUBLIC_API_BASE_URL\n- ERROR_INGEST_URL/TOKEN\n- FACR_SCRAPER_BASE_URL\n- UMAMI_*\n- CLAMAV_* (optional)]
logger[Logger (pkg/logger)]
db_init[[InitDB() + AutoMigrate()]]:::db
email_svc[EmailService (pkg/email)]:::svc
subgraph middleware[Middleware]
direction TB
mw_reqid[RequestID]
mw_logger[RequestLogger]
mw_recovery[CustomRecoveryWithReporter]
mw_errstatus[ErrorStatusReporter]
mw_sanitize[SanitizeHeaders]
mw_dbctx[DBContext (req ctx timeout)]
mw_size[RequestSizeLimit (2MB)]
mw_ct[ValidateContentType]
mw_csrf[CSRFProtection (protected)]
mw_rate[RateLimit (per-route)]
mw_sec[SecurityHeaders + AssetCacheControl]
end
prometheus["GET /metrics (promhttp)"]
static1["Static: /uploads -> UPLOAD_DIR"]
static2["Static: /cache -> ./cache"]
static3["Static: /dist -> ./static"]
static4["Static: /premium-assets -> ./pro"]
subgraph router["Router"]
direction TB
api_grp["/api/v1"]:::route
root_grp["/root/"]:::route
end
subgraph controllers[Controllers]
direction TB
c_auth[AuthController\n/login,/logout,/register,/me\n/password-reset]
c_contact[ContactController\n/contact + newsletter + admin forwarding]
c_pass[PasswordController]
c_ai[AIController\n/ai/blog,/ai/about,/ai/css,/ai/instagram]
c_score[ScoreboardController\n/public + admin timer/sponsors/qr]
c_about[AboutController]
c_gallery[GalleryController\n/Zonerama profile/albums/picks]
c_files[FilesController\n/list/unused/duplicates/usage\n/scan/refresh-tracking/delete]
c_notify[NotificationsController]
c_email[EmailController\n/open.gif/click/unsubscribe/stats]
c_prefetch[PrefetchController\n/status/trigger]
c_seo[SEOController\n/seo (public) + robots.txt + sitemap]
c_nav[NavigationController\n/navigation + social-links + admin CRUD]
c_poll[PollController\n/public vote/results + admin]
c_sw[SweepstakesController\n/public current/visual + admin CRUD/finalize]
c_cloth[ClothingController\n/public + admin CRUD]
c_pec[PageElementConfigController\n/public + admin CRUD/batch]
c_article[ArticleController\n/create + match-link]
c_base[BaseController\n/health, uploads, categories, teams, players, matches, standings, zonerama, settings, shortlinks(public)]
c_myu[MyUIbrixController\n/validate,/preview,/optimize]
c_editor[EditorPreviewController\n/preview state + variants]
c_short[ShortLinkController\n/public create + admin + redirect /s/:code]
c_comment[CommentController\n/public list + CRUD + reactions\nban/unban/report (admin)]
c_eng[EngagementController\n/rewards/leaderboard/profile/actions]
c_facr[FACRController\n/facr club search/info/table]
c_yt[YouTubeController\n/youtube/videos]
c_umami[UmamiController\n/config + admin initialize/stats]
c_error[ErrorController\n/errors ingest + admin + external]
end
subgraph services[Services & Jobs]
direction TB
s_errrep[ErrorReporter]
s_prefetch[Prefetcher\nStartPrefetcher(target)]
s_nlsched[NewsletterScheduler]
s_nlauto[NewsletterAutomation\nweekly, reminders, results]
s_sweep[SweepstakesScheduler]
s_umami[UmamiService]
s_facr[FACRService]
s_cache[CacheService]
s_logo[LogoCache]
s_filetrk[FileTracker]
s_imgopt[ImageOptimizer]
s_bad[BadWords/Spam]
s_setup[SetupService]
end
subgraph models[Models (GORM)]
direction LR
m_settings[Settings]
m_user[User]
m_article[Article]
m_scoreboard[ScoreboardState]
m_compalias[CompetitionAlias]
m_team[Team]
m_player[Player]
m_contact_cat[ContactCategory]
m_contact[Contact]
m_contact_msg[ContactMessage]
m_news[NewsletterSubscription]
m_sponsor[Sponsor]
m_cloth[Clothing]
m_poll[Poll + PollOption + PollVote]
m_nav[NavigationItem + SocialLink]
m_pageel[PageElementConfig]
m_short[ShortLink + LinkClick]
m_comment[Comment + Reaction + Ban + UnbanRequest + Report]
m_profile[UserProfile]
m_points[PointsTransaction]
m_ach[Achievement + UserAchievement]
m_reward[RewardItem + RewardRedemption]
m_over[MatchOverride + TeamLogoOverride]
m_sweep[Sweepstake + Prize + Entry + Winner]
m_up[UploadedFile + FileUsage]
m_error[ErrorEvent]
end
%% wiring inside backend
cfg --> db_init
cfg --> email_svc
router --> middleware
api_grp --> controllers
root_grp --> c_seo
root_grp --> c_short
api_grp --> c_auth
api_grp --> c_contact
api_grp --> c_pass
api_grp --> c_ai
api_grp --> c_score
api_grp --> c_about
api_grp --> c_gallery
api_grp --> c_files
api_grp --> c_notify
api_grp --> c_email
api_grp --> c_prefetch
api_grp --> c_seo
api_grp --> c_nav
api_grp --> c_poll
api_grp --> c_sw
api_grp --> c_cloth
api_grp --> c_pec
api_grp --> c_article
api_grp --> c_base
api_grp --> c_myu
api_grp --> c_editor
api_grp --> c_short
api_grp --> c_comment
api_grp --> c_eng
api_grp --> c_facr
api_grp --> c_yt
api_grp --> c_umami
api_grp --> c_error
%% controllers -> models
controllers -->|GORM| models
db_init ==>|Postgres conn| DB["PostgreSQL :5432 / fotbal_club"]:::db
models ==>|tables| DB
%% services wiring
s_prefetch -.->|GET public endpoints| api_grp
s_nlsched --> s_nlauto
s_nlauto --> email_svc
s_sweep --> email_svc
s_filetrk --> m_up
s_imgopt --> m_up
s_errrep --> c_error
s_umami --> c_umami
s_facr --> c_facr
email_svc -->|SMTP| smtp[(SMTP Provider)]:::ext
%% externals
facr_ext["FACR Scraper :8081"]:::ext
errors_ingest["Error Receiver: errors.tdvorak.dev/api/v1/errors or local :8083"]:::ext
errors_admin["Error Review Admin UI/API: errors.tdvorak.dev"]:::ext
umami_ext["Umami Analytics server"]:::ext
s_facr <---> facr_ext:::animated-edge
s_errrep --> errors_ingest:::animated-edge
c_error <---> errors_admin
s_umami <---> umami_ext
%% static serving
static1 --- user_browser
static2 --- user_browser
static3 --- user_browser
static4 --- user_browser
%% metrics
prometheus --- user_browser
end
user_browser ==>|HTTP /api/v1| api_grp:::animated-edge
user_browser ==>|HTTP /robots.txt, /sitemap.xml, /s/:code| root_grp
%% ========================= Frontend (React) =========================
subgraph FRONTEND[Frontend (React + ChakraUI)]
direction TB
fe_router[React Router (src/App.tsx)]:::fe
subgraph fe_public[Public Pages]
direction LR
p_home[HomePage /]
p_blog[BlogPage /blog]
p_newslist[ArticlesListPage]
p_article[ArticleDetailPage /news/:slug | /articles/:id]
p_about[AboutPage /o-klubu]
p_club[ClubPage /klub]
p_calendar[CalendarPage /kalendar]
p_actcal[ActivitiesCalendarPage /aktivity]
p_tables[TablesPage /tabulky]
p_matches[MatchesPage /zapasy]
p_match[MatchDetailPage /zapas/:id]
p_players[PlayersPage /hraci]
p_player[PlayerDetailPage /hraci/:id]
p_sponsors[SponsorsPage /sponzori]
p_contact[ContactPage /kontakt]
p_gallery[GalleryPage /galerie]
p_album[AlbumDetailPage /galerie/album/:id]
p_videos[VideosPage /videa]
p_clothing[ClothingPage /obleceni]
p_polls[PollsPage /ankety]
p_search[SearchPage /hledat]
p_short[ShortRedirectPage /s/:code]
p_over_sb[OverlayScoreboardPage /overlay/scoreboard]
p_over_sp[OverlaySponsorsPage /overlay/sponsors]
p_cookies[CookiePolicyPage]
p_terms[TermsPage]
p_privacy[PrivacyPolicyPage]
p_notfound[NotFoundPage *]
end
subgraph fe_auth[Auth & Setup]
direction LR
p_login[AuthPage /login]
p_register[RegisterPage /register]
p_forgot[ForgotPasswordPage /forgot-password]
p_reset[ResetPasswordPage /reset-password]
p_setup[SetupPage /setup]
p_style[StylePreviewPage /setup/styl]
p_news_unsub[NewsletterUnsubscribePage]
p_news_prefs[NewsletterPreferencesPage]
end
subgraph fe_admin[Admin Pages]
direction LR
a_dashboard[AdminDashboardPage]
a_docs[AdminDocsPage]
a_about[AboutAdminPage]
a_videos[AdminVideosPage]
a_gallery[GalleryAdminPage]
a_merch[AdminMerchPage]
a_sponsors[SponsorsAdminPage]
a_matches[MatchesAdminPage]
a_players[PlayersAdminPage]
a_teams[TeamsAdminPage]
a_users[UsersAdminPage]
a_banners[BannersAdminPage]
a_messages[MessagesAdminPage]
a_settings[SettingsAdminPage]
a_newsletter[NewsletterAdminPage]
a_polls[PollsAdminPage]
a_comp[CompetitionAliasesAdminPage]
a_prefetch[PrefetchAdminPage]
a_scoreboard[ScoreboardAdminPage]
a_score_remote[MobileScoreboardControlPage]
a_analytics[AnalyticsAdminPage]
a_shortlinks[ShortlinksAdminPage]
a_files[FilesAdminPage]
a_contacts[ContactsAdminPage]
a_navigation[NavigationAdminPage]
a_comments[CommentsAdminPage]
a_engagement[EngagementAdminPage]
a_sweep[SweepstakesAdminPage]
a_sweep_visual[SweepstakeVisualPage]
a_adminreset[AdminResetPasswordPage]
end
%% FE -> BE API mappings (high level)
fe_router -->|services/api.ts| api_grp:::animated-edge
p_blog -->|GET /articles| api_grp
p_article -->|GET /articles/slug/:slug, /articles/:id\nPOST /articles/:id/read| api_grp
p_home -->|GET /articles/featured, /matches, /standings, /settings, /navigation| api_grp
p_matches -->|GET /matches,/standings| api_grp
p_match -->|GET /matches/:id| api_grp
p_players -->|GET /players| api_grp
p_player -->|GET /players/:id| api_grp
p_gallery -->|GET /gallery/albums| api_grp
p_album -->|GET /gallery/albums/:id| api_grp
p_videos -->|GET /youtube/videos| api_grp
p_clothing -->|GET /clothing| api_grp
p_polls -->|GET /polls| api_grp
p_contact -->|POST /contact| api_grp
p_over_sb -->|GET /scoreboard (public)| api_grp
p_over_sp -->|GET /scoreboard/sponsors| api_grp
p_short -->|GET /s/:code (root)| root_grp
%% Admin flows
a_articles[ArticlesAdminPage] -->|POST/PUT/DELETE /articles\n/link-match| api_grp
a_matches -->|GET /admin/matches| api_grp
a_comments -->|GET/PATCH /admin/comments| api_grp
a_navigation -->|CRUD /admin/navigation| api_grp
a_files -->|GET/DELETE /admin/files| api_grp
a_scoreboard -->|GET/PUT /admin/scoreboard + timer| api_grp
a_score_remote -->|POST timer controls| api_grp
a_newsletter -->|send/test/preview/status| api_grp
a_sweep -->|CRUD /admin/sweepstakes| api_grp
a_sweep_visual -->|GET /admin/sweepstakes/:id/visual| api_grp
a_analytics -->|/admin/umami| api_grp
%% FE error reporting & analytics
fe_router -->|POST /errors (ErrorReporter)| api_grp:::animated-edge
fe_router -->|GET /umami/config| api_grp
end
%% ========================= Ports & CORS =========================
subgraph PORTS[Ports & CORS]
direction LR
port_be[Backend :8080]
port_fe[Frontend :3000 -> :80]
port_db[Postgres :5432]
cors[CORS AllowedOrigins\n- http://localhost:3000\n- http://localhost:8080\n+ FrontendBaseURL origin\n+ "*" optional in dev]
end
port_be --- docker_backend
port_fe --- docker_frontend
port_db --- docker_db
cors -. controls .- router
%% Legend
subgraph LEGEND[Legend]
direction LR
L1[External Service]:::ext
L2[Database/Table]:::db
L3[Service/Daemon]:::svc
L4[Controller]:::ctrl
L5[Middleware]:::mid
L6[Model]:::model
L7[Route Group]:::route
end
+25
View File
@@ -0,0 +1,25 @@
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
autonumber
participant U as Editor/User
participant FE as Frontend (React)
participant BE as Backend API
participant FS as File Storage (uploads dir)
participant DB as Postgres
Note over U,FE: Drag & drop / select image
U->>FE: Choose file
FE->>BE: POST /api/v1/upload (multipart/form-data)
BE->>BE: RateLimit(30/min), size/type validation
BE->>FS: Save file to UPLOAD_DIR
BE->>DB: Insert UploadedFile + FileUsage (if provided)
BE-->>FE: 200 OK {url, id}
rect rgba(230,255,230,0.2)
Note over U,BE: Quick edits / crop (editor role required)
FE->>BE: POST /api/v1/image-processing/process | crop-upload | quick-edit
BE->>BE: JWTAuth + CSRF + RoleAuth(editor)
BE->>FS: Read/transform/write
BE->>DB: Track usages/updates
BE-->>FE: 200 OK {url}
end