Commit Graph

41 Commits

Author SHA1 Message Date
cesnimda e981c63a4d fix(security): patch vulnerable NuGet dependencies flagged by CI gate
CI / backend (pull_request) Successful in 1m13s
CI / frontend (pull_request) Successful in 29s
Security / secrets (pull_request) Successful in 6s
Security / dependencies (pull_request) Successful in 1m13s
The new dependency scan correctly failed on real advisories. Bump MailKit
4.13.0->4.17.0 (clears MailKit+MimeKit moderates) and add transitive security
pins for the .NET 8.0.0 High-severity advisories: System.Text.Json 8.0.6,
Microsoft.Extensions.Caching.Memory 8.0.1 (with DependencyInjection.Abstractions
8.0.2), System.Security.Cryptography.Xml 8.0.3. Verified locally: clean vuln
scan, Release build OK, all 39 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:38:08 +02:00
cesnimda 98e94a8163 ci(security): run gitleaks binary + audit prod deps only
CI / backend (pull_request) Successful in 1m12s
CI / frontend (pull_request) Successful in 28s
Security / secrets (pull_request) Successful in 6s
Security / dependencies (pull_request) Failing after 1m8s
The container-mode runner has no Docker socket, so the gitleaks 'docker run'
step failed; download and run the binary instead. Scope npm audit to production
dependencies (--omit=dev) so dev-toolchain advisories don't block merges, and
match dotnet's own vulnerable-packages line to avoid severity-word false hits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:26:25 +02:00
cesnimda 87d44537b9 ci: add secret/vuln scanning + staging & production deploy pipelines
CI / backend (pull_request) Successful in 1m3s
CI / frontend (pull_request) Successful in 22s
Security / secrets (pull_request) Failing after 4s
Security / dependencies (pull_request) Failing after 1m1s
security.yml: gitleaks secret scan + NuGet/npm vulnerability gate on PRs and
pushes to main/develop (detective backstop to the pre-commit hook).
deploy-staging.yml: on merge to develop, re-verify then rebuild the isolated
local staging stack (needs a self-hosted Windows runner).
deploy-prod.yml: tag-gated production promotion (the tag is the approval), ready
to activate once the Linux server + its runner exist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:49:08 +02:00
cesnimda 101deb9546 chore: add version-controlled git hooks + installer
pre-commit runs fast checks (block committed .env/secrets, verify dotnet
format); pre-push mirrors CI (Release build + tests + frontend build) to catch
failures before they leave the machine. install-hooks.ps1 wires core.hooksPath
so the hooks are shared and reviewable rather than living in un-tracked
.git/hooks. Bypass with --no-verify; CI still enforces the gate server-side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:14:37 +02:00
cesnimda 80fc813b61 build: add production-parity staging environment on Docker
Layer a staging overlay (docker-compose.staging.yml) on the base compose file:
same Linux images and Production runtime as prod, differing only in the dev
banner, capped sync, shifted ports (18080/18081), and an isolated project
namespace so it never touches prod data. Wire -Staging into deploy/up.ps1 and
deploy/down.ps1, add .env.staging.example, and track that template in git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:14:26 +02:00
cesnimda 0d3a5fe2fe docs: define Git workflow, versioning, and environment strategy
Add the canonical workflow reference (docs/WORKFLOW.md): branch model
(main/develop + feature/fix/hotfix), Conventional Commits, SemVer, tagging,
and the three-environment strategy (native dev, Docker staging, Linux prod).
Seed VERSION (0.1.0) as the single source of truth and a Keep-a-Changelog
CHANGELOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:14:15 +02:00
cesnimda 8c2e52ad60 Merge security/hardening-phase4: Phase 4-7 security hardening + CI
CI / backend (push) Successful in 1m4s
CI / frontend (push) Successful in 20s
Phase 1-5 security remediation (SSRF egress guard, systemic IDOR global query
filters, cookie Secure, forwarded-header trust, error-leak/ProblemDetails, pagination
clamps, security headers/HSTS, open-redirect, info-leak, non-root container, Postgres
lockdown), the Critical destructive-action UX safety fix, and a Gitea CI pipeline.
Re-validated safe to merge; 39 tests green. Deferred: CSRF token, key encryption,
CSP enforce, AI-egress governance. Ops follow-up: rotate Google OAuth secret, set
strong POSTGRES_PASSWORD.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.1.0
2026-07-01 00:33:20 +02:00
cesnimda d38bcf766c docs: track project rules (AGENTS.md) + accent-swappable UI note
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:33:19 +02:00
cesnimda 94529df775 ci: add Gitea Actions build + test pipeline
Real CI at .gitea/workflows/ci.yml (Gitea reads .gitea/workflows, not the empty
untracked .git/workflows/ci.yml cruft): backend dotnet build + dotnet test (39 tests)
and frontend npm ci + build, on pushes to main/develop and all PRs. Establishes the
merge gate recommended in the Phase 7 git-workflow review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:33:19 +02:00
cesnimda 1e15b84dce fix(search): move pagination clamp to controller (fixes cleanup regression)
Phase 5 re-validation caught a functional regression: the V-10 clamp in
SearchService.SearchAsync also capped CleanupService's internal target resolution
(pageSize 10000 -> 200), silently limiting bulk cleanup-by-query to 200 emails.

The clamp belongs at the user-facing trust boundary, not the shared service: move
MaxPageSize (200) enforcement into SearchController (both the POST body and GET query
paths). Internal callers of ISearchService now request large pages unhindered, while
user requests are still bounded. Adds a regression test proving SearchService returns
a 250-row page uncapped. No security regressions per Phase 5. All 39 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:29:45 +02:00
cesnimda c3bca051ea fix(ux): confirm + feedback for destructive bulk actions
Resolves the Critical destructive-action safety gap (and the CSRF-adjacent risk of
frictionless Gmail mutation):

- Bulk Trash now requires an explicit confirmation dialog (count + 30-day-recovery
  note) instead of firing on one click.
- Every bulk action surfaces a success / partial-failure / error toast.
- Rows are removed from the list only when the server confirms the whole batch
  succeeded; partial failures leave the list intact so the user can retry, instead
  of optimistically hiding failed items.

Built on the new Dialog/Toast primitives (also restyles the toolbar to the new
system). Real cross-session Undo is deferred to the activity-log backend (specced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:22:43 +02:00
cesnimda e6a0239436 fix(security): systemic IDOR safeguard via EF global query filters
Defense-in-depth tenant isolation: every user-owned entity (Email, Sender,
MailThread, MailDomain, Attachment, Label, SyncState, AnalyticsAggregate,
WidgetLayout, UnsubscribeItem) gets a global query filter restricting reads to the
authenticated user. AppDbContext takes an optional ICurrentUser; CurrentUserId is
Guid.Empty for background workers / design-time, which DISABLES the filter so sync
and tooling (which already scope by an explicit userId) are unaffected. On the HTTP
attack surface a forgotten manual `WHERE UserId ==` can no longer leak another
tenant's rows.

Phase 1 confirmed no active IDOR; this is preventive, and prioritised now because the
upcoming automation engine will add many new queries.

Also: moved the Npgsql-only tsvector FTS mapping out of EmailConfiguration into
AppDbContext.OnModelCreating, guarded by Database.IsRelational() (Ignored otherwise),
so non-relational test providers work — honouring the existing Email.SearchVector
comment. Production (Npgsql) model is unchanged; no migration needed.

Adds 3 cross-user tenant-isolation integration tests. All 38 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:20:56 +02:00
cesnimda 626a9f8454 fix(security): Phase 4 edge hardening + SSRF egress guard
Backend security fixes from the Phase 1 register / Phase 2 roadmap (PR1 + V-01):

- V-01 SSRF: new SafeHttpGuard validates outbound unsubscribe URLs (scheme allowlist
  + DNS-resolve-and-block private/loopback/link-local/ULA/metadata ranges), wired into
  UnsubscribeService; the "unsubscribe" HttpClient now disables auto-redirect so a
  validated external URL can't 3xx into an internal target. +33 unit tests.
- V-04: session cookie SecurePolicy=Always in non-dev (SameAsRequest in dev).
- V-06: UseExceptionHandler/ProblemDetails in prod; Cleanup/Unsubscribe no longer
  echo ex.Message to clients (logged server-side, generic message returned).
- V-08: ForwardedHeaders trusted only from configurable KnownNetworks (default private
  ranges) + ForwardLimit, instead of trusting any client. New ForwardedHeaders config.
- V-09: returnUrl validated with Url.IsLocalUrl (no open redirect via OAuth flow).
- V-10: SearchService clamps Page/PageSize (<=200); Analytics clamps take/days.
- V-11: baseline security headers (nosniff, X-Frame-Options DENY, Referrer-Policy,
  COOP) + HSTS in prod.
- V-13: /app/info discloses only devMode to anonymous callers unless dev mode is on.
- V-12: API container runs as non-root 'app' user (keys dir pre-owned).
- V-03: Postgres + API ports bound to 127.0.0.1; POSTGRES_PASSWORD now required (no
  weak default fallback).

API compatibility preserved (clamps not rejections; error-body shape changes only on
failure paths). No DB migrations. Build + all 33 unit tests green. V-15 (MailKit
NU1902) persists across versions and the SMTP path is default-off — tracked, not bumped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:15:04 +02:00
cesnimda 9ae61432ba feat(ui): F1+F2 — component primitives + rebuilt app shell
F1: shadcn-style primitive set in components/ui (Button, Card, Badge, Input,
Switch, Separator, Skeleton, Tooltip, DropdownMenu, Dialog, Sheet, Tabs, Table,
Toast+useToast, ThemeToggle, PageHeader/StatCard/EmptyState) on Radix + cva.

F2: Layout rebuilt on the new system — sidebar with primary nav (lucide icons)
+ favorites/saved-searches/mailbox/smart-folder sections, sticky topbar with
search, sync status, theme toggle, digest toggle, and an account dropdown. All
prior behavior preserved (auth, sync, collapse/section persistence, "/" search
focus). App wrapped in ToastProvider + TooltipProvider. Build green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 23:51:45 +02:00
cesnimda 1bd8143a87 feat(ui): F0 — Tailwind + design tokens + theme infrastructure
Install Tailwind v3, Radix primitives, cva/tailwind-merge/clsx, lucide-react.
Add HSL design tokens (light + dark) with the Indigo #5b5bf0 accent isolated
to a single --primary token (swappable for a future accent picker), darkMode
'class', anti-flash inline script, useTheme hook, and cn() helper. styles.css
still loads for not-yet-migrated pages. Build green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 23:11:51 +02:00
cesnimda 6af03ea807 docs: switch Privacy Monitor default to XposedOrNot (free, keyless)
Email breach endpoints need no API key (2 req/s, cached), so Privacy Monitor
ships enabled by default. HIBP kept as a swappable key-based alternative;
provider chosen via Privacy:Provider config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 23:07:43 +02:00
cesnimda be6cbf90d7 docs: full specs for automation engine, sender policy, activity log, privacy monitor, UI overhaul
Clean.Email-parity feature build-out plus a Stripe/Notion-style UI rebuild on
Tailwind + shadcn-style primitives. Locks the hybrid-automation, Gmail-only,
light+dark, incremental-rollout decisions and lays out the backend/frontend
build sequence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 23:04:52 +02:00
cesnimda 64f835f719 feat: AI categorisation fallback, unsubscribe confidence scoring, one-line summaries
Heuristic classifier falls back to AI only when it can't determine a category;
unsubscribe confidence blends method reliability with an AI safety opinion and
surfaces it in the Unsubscribe Manager; emails get an on-demand AI one-line
summary in the detail pane. All AI calls degrade gracefully when AI is disabled
or the provider errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 22:21:10 +02:00
cesnimda 0163165beb feat: notification digests via SMTP
Adds a periodic inbox-summary email per opted-in user. SMTP is
configured via appsettings (Smtp section); the digest is built from
existing analytics (health score, top senders, recommendations) and
sent by a new hourly DigestWorker, mirroring the GmailSyncWorker
pattern. Frequency and send hour are configurable (Digest section).

Backend: IEmailSender (SmtpEmailSender, MailKit), IDigestService,
DigestWorker, new User.DigestEnabled/LastDigestSentUtc fields +
migration, SettingsController for the per-user toggle and a
send-now test endpoint, all scoped to the authenticated UserId.

Frontend: SettingsApi + a bell/no-bell toggle button in the topbar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 22:11:39 +02:00
cesnimda 9bd3799fbc feat: saved searches
Lets users name and pin a search query from the SearchResults page;
saved searches persist to localStorage and show as quick links in a new
sidebar section, similar to Favorites. Clicking re-runs the query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 22:00:12 +02:00
cesnimda c7ead8caae feat: keyboard shortcuts on Senders email list
Wires the shared useListKeyboardNav hook into SenderEmails so j/k/e/#
work there too, matching FolderView and SearchResults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 21:58:40 +02:00
cesnimda 7710d49c77 feat: keyboard shortcuts for email navigation
'/' focuses the search bar from anywhere (Layout-level listener).
j/k move focus up/down the email list in FolderView and SearchResults;
e archives the focused email, # trashes it. All shortcuts are ignored
while an input/textarea/select has focus, so typing is never hijacked.

A small hint footer in the bottom-right reminds users of the shortcuts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 21:55:36 +02:00
cesnimda c11d747919 feat: bulk actions for email lists
Select multiple rows (checkbox per row) in FolderView, SearchResults,
and the Senders email panel. A toolbar appears with mark read/unread,
star, archive, and trash, applied to the whole selection via
CleanupService.ExecuteAsync (already scoped to UserId).

Shared via useSelection hook and BulkToolbar component to avoid
duplicating selection state across the three list views.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 21:53:43 +02:00
cesnimda 8c92263cc3 feat: sync status indicator, unsubscribe queue UI, email body preview
SyncStatus widget polls /sync/status and shows a live progress bar in
the topbar while syncing, or last-synced time / error otherwise.

Unsubscribe page reworked into a proper queue: status filter tabs
(All/Pending/Succeeded/Failed), select-all, sorted by volume, status
badges, and a result toast after processing.

GET /email/{id} returns full BodyText; Senders detail view fetches
and renders it instead of just the snippet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 21:50:59 +02:00
cesnimda a24c48179a feat: senders master-detail layout with inline email view
Left panel: scrollable sender list with filter, email count, unread badge.
Right panel: infinite-scroll email list, quick actions, click row to view
detail (subject, meta, snippet, open in Gmail). Back button returns to list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 21:40:01 +02:00
cesnimda 0dfd739430 feat: hide empty smart folders in sidebar
Smart folders with zero matching emails are not rendered once
sidebar counts have loaded. Folders still show while counts are
pending (avoids flash of empty sidebar on load).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:45:48 +02:00
cesnimda 21606c5f91 feat: add 14 new smart folder categories (locale-agnostic)
New EmailCategory enum values: Travel, Subscriptions, Parcels,
Recruitment, Events, SecurityAlerts, Healthcare, Education,
NewsMedia, PropertyUtilities, Charity, Government, CryptoInvesting,
FamilySchool.

HeuristicClassifier: locale-agnostic rules using global brand
domains + TLD patterns (.gov.*, .edu, .ac.*) + English subject
keywords — works for international users without relying on
country-specific domains (e.g. gov.uk).

Priority order ensures SecurityAlerts and Government take
precedence over Finance, and Travel/RideSharing/FoodDelivery
fire before Finance to prevent receipt mis-classification.

AnalyticsService: maps all 14 new slugs to category counts.
client.js: folderToRequest() handles all 14 new slugs.
Layout.jsx: SMART_FOLDERS extended with icons and labels.
widgets.jsx: CAT_SLUG updated for dashboard drillthrough.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:44:21 +02:00
cesnimda 95dc835651 feat: expanded smart folder category coverage (Gaming, RideSharing, FoodDelivery, SeasonalSales, Wellness, Shopping)
- Added 6 new EmailCategory enum values
- HeuristicClassifier rewritten with domain and subject-keyword rules:
  - RideSharing: Uber, Lyft, Bolt, Free Now, Grab etc.
  - FoodDelivery: DoorDash, Uber Eats, Deliveroo, Just Eat, Grubhub etc.
  - Gaming: Steam, Epic, EA, Xbox, PlayStation, Nintendo, Blizzard, Riot etc.
  - Shopping: Amazon, eBay, Etsy, ASOS, Zalando, Shein etc.
  - SeasonalSales: subject keywords (sale, % off, promo, black friday etc.)
  - Wellness: Strava, Garmin, Peloton, MyFitnessPal, Headspace, Nike etc. + subject hints
  - Priority ordering prevents ride-share/food receipts misclassifying as Finance
- SidebarCounts maps all new slugs to their EmailCategory
- frontend folderToRequest() and CAT_SLUG map all new categories
- Existing emails will pick up new categories on next sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:24:07 +02:00
cesnimda 2c9b402b08 feat: inline unsubscribe button on email rows
- ✉✕ button appears on rows where the email has a List-Unsubscribe header
- One-click POST / HTTP GET executed automatically; shows ✓ on success
- mailto: targets open the user mail client (we never auto-send email)
- POST /email/{id}/unsubscribe: detects sender, then processes — UserId scoped
- EmailSummaryDto gains IsStarred, HasListUnsubscribe, SupportsOneClick fields
- Both SearchService and CleanupService updated to populate new DTO fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:22:28 +02:00
cesnimda 80c2167b89 feat: dashboard drillthrough — widgets navigate to filtered views
- Total Emails stat card -> /app/folder/allmail
- Unread stat card -> /app/folder/unread
- Top Senders rows -> /app/search?q=from:{address}
- Category heatmap labels -> matching folder or search (Finance, Social, Notification, Promotional, Spam, Newsletter, Personal)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:20:09 +02:00
cesnimda 2f6d6abdd3 feat: quick actions on email rows (read/unread, star, trash)
- EmailRow shared component with hover action buttons: mark read/unread, star/unstar, trash
- Optimistic UI — local state patches immediately on click; row fades and becomes non-interactive during the API call
- Trashed rows disappear from the current folder view via onRemove callback
- Backend: EmailController single-email endpoints (POST /email/{id}/read|unread|star|unstar|trash|untrash)
- Star/Unstar added to CleanupActionType enum and CleanupService (maps to STARRED Gmail label via BatchModifyAsync)
- UserId scope enforced in ResolveTargetsAsync — a user can only act on their own emails
- action buttons use stopPropagation so clicking them does not open Gmail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:18:44 +02:00
cesnimda 498536451e feat: search bar in topbar with Gmail-style query syntax
- Search form in topbar routes to /app/search?q=...
- SearchResults page with infinite scroll, same row style as FolderView
- Supports Gmail-like operators: from:, is:unread, has:attachment, after:, before:
- SearchApi.query() calls GET /search which runs through the existing GmailQueryParser
- Input sanitised via encodeURIComponent on submit; external links use noopener/noreferrer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:15:45 +02:00
cesnimda 4d15a3a8fb feat: click email row opens it in Gmail (noopener/noreferrer)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:14:06 +02:00
cesnimda 4b1d1b1b6e feat: infinite scroll for folder view
Replace page buttons with IntersectionObserver sentinel — scrolling near
the bottom automatically fetches and appends the next page of results.
Shows a loading indicator while fetching and a total count footer when
all emails are loaded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:05:17 +02:00
cesnimda 20d51b2c15 feat: folder view — clicking sidebar items shows filtered email list
- New FolderView page at /app/folder/:slug renders a paginated email table
- Sidebar links now route to their respective folder views
- Extended SearchRequestDto with IsInInbox, IsStarred, IsTrashed, GmailLabel, Category, MinSizeBytes
- SearchService applies the new filters; GmailLabel does a label-id lookup (SENT/DRAFT/SPAM)
- Frontend folderToRequest() maps every slug to the correct search payload
- Email rows show sender, subject+snippet, attachment icon, size, date; bold for unread
- Pagination controls for large folders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 19:59:01 +02:00
cesnimda ca02f40841 feat: Smart Folders sidebar, category heatmap, sidebar counts API
- Replace plain activity heatmap with CategoryHeatmapWidget on dashboard
- Add collapsible Smart Folders sidebar with Favorites, Mailbox, Smart Folders sections
- Favorites: customisable pinned shortcuts (default: Inbox, Unread, Large, Old, Read Later); pin/unpin smart folders via buttons; persists to localStorage
- Mailbox section: 11 Gmail mailbox items with icons and live count badges
- Smart Folders section: 10 category folders sorted by count desc
- Live count badges via new GET /api/v1/analytics/sidebar-counts endpoint
- Backend: SidebarCountsDto, GetSidebarCountsAsync with label lookups for SENT/DRAFT/SPAM, size/age filters, EmailCategory mapping
- Sidebar collapses to icon-only; section states persist to localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 19:40:54 +02:00
cesnimda fe5920919f feat: landing page + logo, dev-mode banner + sync cap, non-blocking sync with progress splash 2026-06-30 16:58:35 +02:00
cesnimda dcb939e4f2 deploy: one-command scripts to build+run from deploy/.env 2026-06-30 16:16:16 +02:00
cesnimda a387d31f62 chore: add .gitattributes to normalize line endings (LF) 2026-06-30 16:09:52 +02:00
cesnimda 29a4aa2212 scaffold: EF design ref + initial migration 2026-06-30 16:08:26 +02:00
cesnimda f43ef5f945 chore: init project 2026-06-30 15:53:32 +02:00