TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
Manage a personal schedule and budget, and read or post on forum boards after logging in
Translation pending
AI summary · revised during review
ProductivityC# · TypeScriptGitHub @IkhyeonJoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Introduction
Maroik is a modern web application built with ASP.NET Core MVC, featuring a comprehensive set of tools for personal and business management. It includes features such as expense tracking, calendar management, bulletin boards, and role-based user management.
This repository is the open-source portfolio edition of Maroik: the full application source and test suites, a local Docker Compose stack, and a seed database that contains only the default admin and demo accounts. Production deployment scripts, CI/CD pipelines, and production data are not included.Read the full README ↗Init.sql)| Role | Password | |
|---|---|---|
| Admin | [email protected] | demoO12!! |
| User (public demo) | [email protected] | demoO12!! |
The server runs Ubuntu Server 26.04 and Maroik lives in /home/ubuntu/Maroik. Run every command below as ubuntu,
not with sudo: Deploy.sh refuses to run as root, because the folder must belong to ubuntu (it calls sudo itself
where it needs it). It asks how to deploy; without a terminal, pass the choice (bash Maroik.DeployOps/Deploy.sh local
or github):
| Choice | What it deploys |
|---|---|
1 / local — build here | The source in /home/ubuntu/Maroik as it is (a clone on any branch, or source copied by hand). Builds the three app images on the server (release build, as CI does), asks how the site is reached (below) and starts the stack for it. No GitHub, no ghcr.io, no CI check; does not install AutoDeploy.sh (and removes it from the cron jobs). |
2 / github — GitHub | The newest main whose CI publish job succeeded: checks it out, pulls its three images from ghcr.io, starts the stack and installs AutoDeploy.sh, so merged changes reach production by themselves. |
3 — quit | Nothing. |
Choice 1 then asks whether the site has a domain and, if so, how it is reached. The answers are asked on every deploy (never saved); without a terminal pass them as arguments:
| Answer | What runs | Arguments |
|---|---|---|
| No domain | The website on a port of this server (default 80) over plain HTTP — for a home network: passwords and sessions travel unencrypted. Also asks the address the mails link to (default http://<LAN IP>[:port]/). | local nodomain [port] [address] |
| Domain, Cloudflare Tunnel | cloudflared, no port opened (as choice 2). Needs TUNNEL_TOKEN. | local tunnel <domain> |
| Domain, port forwarding | Caddy on 80 / 443 with a Let's Encrypt certificate for <domain> and www.<domain> (asks the e-mail Let's Encrypt writes to). The router forwards 80 / 443 here and the domain's DNS points at the public IP. | local portforward <domain> <e-mail> |
The website follows the answer (ServerSetting:HostingMode): HTTPS, HSTS and the www redirect only with a domain, and
cookies a browser keeps over plain HTTP without one. Choice 2 always runs the Cloudflare Tunnel with .env.prod's domain.
Both need three server-only things the repository does not carry (all git-ignored). It checks for them before it does anything and lists whatever is missing:
| Item | What it is |
|---|---|
.env.prod | Passwords and settings: copy .env.prod.example and fill in the database, RabbitMQ and SMTP credentials, TUNNEL_TOKEN (Cloudflare Zero Trust > Networks > Tunnels > connector token; the tunnel only), EXTERNAL_DRIVE_UUID (sudo blkid) and BACKUP_BASE_DIR. |
Maroik.DB/PostgreSQL/SQL_Init_Script/Production/Init.sql | The database the stack starts from: a backup's pg_dump, or Debugging/Init.sql (same schema, demo data) on a new server. |
Maroik.FileStorage/upload/ | Uploaded attachments. Empty is allowed only on a new server: Deploy.sh asks first (ALLOW_EMPTY_UPLOAD=1 skips the question). |
Every backup Maroik.Crontab/Backup.sh makes (Maroik-Backup-YYYY-MM-DD-HH-MM-SS.tar.xz, hourly) holds exactly these
three, laid out like the clone, so extracting it into the clone puts each one where Deploy.sh looks. The archives are
on the backup drive in BACKUP_BASE_DIR (/home/ubuntu/512GB/Maroik-Backup by default), and the newest one is also kept
next to the clone, in /home/ubuntu.
Either choice then starts the whole stack, mounts the backup drive, installs the cron jobs (hourly backup, resource
monitoring, and with choice 2 the auto deploy) and closes every inbound port but the mode's (80 / 443 for port forwarding,
the chosen port without a domain). In the commands below, answer 1 to build
on the server or 2 when production should follow GitHub's main.
# 1. Mount the backup drive to reach the archives (Deploy.sh mounts it for good later; `sudo blkid` shows the UUID)
sudo mkdir -p /home/ubuntu/512GB
sudo mount UUID=<backup drive UUID> /home/ubuntu/512GB
ls /home/ubuntu/512GB/Maroik-Backup/ # pick the newest Maroik-Backup-YYYY-MM-DD-HH-MM-SS.tar.xz
# 2. Clone, then extract the backup over the clone: .env.prod, the Production Init.sql and the uploads land in place
git clone https://github.com/IkhyeonJo/Maroik.git /home/ubuntu/Maroik
tar xJf /home/ubuntu/512GB/Maroik-Backup/Maroik-Backup-YYYY-MM-DD-HH-MM-SS.tar.xz --strip-components=1 -C /home/ubuntu/Maroik
# 3. Deploy
cd /home/ubuntu/Maroik && bash Maroik.DeployOps/Deploy.sh
On a different machine, check EXTERNAL_DRIVE_UUID in the restored .env.prod against sudo blkid before step 3.
git clone https://github.com/IkhyeonJo/Maroik.git /home/ubuntu/Maroik
cd /home/ubuntu/Maroik
cp .env.prod.example .env.prod # then edit it: replace every dummy value (see the table above)
cp Maroik.DB/PostgreSQL/SQL_Init_Script/Debugging/Init.sql Maroik.DB/PostgreSQL/SQL_Init_Script/Production/Init.sql
ALLOW_EMPTY_UPLOAD=1 bash Maroik.DeployOps/Deploy.sh
A release that changes Maroik.DB/ is not deployed automatically (AutoDeploy.sh logs it and waits; Deploy.sh deploys
whatever source it finds, so the same steps apply before it), because the database
has to be rebuilt from Init.sql. Undeploy.sh removes every Docker volume, the database's included, so take a fresh
backup right before it:
cd /home/ubuntu/Maroik
# 1. A fresh backup of the running server (the newest archive is also left in /home/ubuntu)
bash Maroik.Crontab/Backup.sh
# 2. Take the stack down: containers, volumes (the database too), images and cron jobs; unmounts the backup drive
bash Maroik.DeployOps/Undeploy.sh
# 3. Put the backup back in place
tar xJf /home/ubuntu/Maroik-Backup-YYYY-MM-DD-HH-MM-SS.tar.xz --strip-components=1 -C /home/ubuntu/Maroik
# 4. Bring Maroik.DB/PostgreSQL/SQL_Init_Script/Production/Init.sql up to the new schema: make the same change the release
# made to the tracked Init.sql variants (the backup's dump still has the old schema)
# 5. Deploy the new release
bash Maroik.DeployOps/Deploy.sh
Maroik is a modern web application built with ASP.NET Core MVC, featuring a comprehensive set of tools for personal and business management. It includes features such as expense tracking, spreadsheet-style budgets, calendar management with e-mail reminders, bulletin boards, a personal Kanban board, a personal file drive, and role-based user management.
numeric(20,4)); a fifth decimal is rejected instead of silently rounded, so no recorded amount is ever changed by roundingyyyy-MM-dd HH:mm:ss in the viewer's time zone=SUM(B2:B13), =ACTUAL(...), references to other sheets and last year's sheet), with a
formula bar that colours references, matches parentheses and suggests functions#REF!, a circular reference #CYCLE!DomainError, and when a project is missing from the solution.DateTime utcNow argument; each use case reads TimeProvider once and passes the same instant everywhere, so Created == Updated on creation and token expiry is tested at its exact boundaries with FakeTimeProvider.Email, Money, CurrencyCode, AccountRole, …) replace primitive strings; admin actions are intent-revealing aggregate operations (ChangeRole, Lock/Unlock, ForceConfirmEmail/RevokeEmailConfirmation, AcceptServiceTerms/RevokeServiceTerms, SoftDelete/Restore, AdminResetPassword) instead of a field-overwriting update.FormulaFunctions catalog.catch logs or says why not, no secrets in
templates) are checked over every source with the Roslyn parser; memory-safety, security, culture and banned-API
(direct clock reads, sync-over-async) analyzers are build errors; one .NET SDK is pinned for global.json, every
Dockerfile and CI.Maroik has been built by one developer since May 2021. It was developed in a private repository until September 2026, when the project moved to this public repository; all development now happens here. The earlier history — about 4,300 commits — stays in the private repository; those commits show on the contribution graph of the author's GitHub profile.
Authorship. Through 2025 the code was written by hand (C#, JavaScript). From 2026 the large mechanical refactors — the DDD + Clean Architecture restructuring, the test suites and the TypeScript migration — were done with AI assistance (Claude Code) under the author's design and review; the feature design and the original implementation are the author's own. Commits made with AI assistance carry a Co-Authored-By: Claude trailer.
| When | Milestone |
|---|---|
| 2021-05 | Project starts as MyLaboratory: ASP.NET Core (2.1 → .NET 5) with MariaDB, run under docker-compose from the first week |
| 2021-06 | AdminLTE 3 layout, NonFactors MVC grid |
| 2021-07 | Excel export, registration with e-mail confirmation, password reset, separate admin / user dashboards, English / Korean localization (resx) |
| 2021-08 | Account book: assets, income, expenditure, fixed income / expenditure with deposit-day notices |
| 2021-12 | MariaDB 10.6 |
| 2022-07 | User dashboard backend and charts for income and expenditure |
| 2022-08 | Public anonymous dashboard and landing pages; role-based navigation menu (Admin / User / Anonymous) stored in the database |
| 2022-09 | .NET 6 |
| 2022-10 | Renamed to Maroik; Free Forum with Summernote editor and HTML sanitizing |
| 2022-11 | Forum comments, locked and notice posts; security headers |
| 2023-03 | .NET 7 |
| 2023-04 | Income and expenditure statistics |
| 2023-08 | Global AJAX error handling; debugging docker-compose in VS Code |
| 2024-02 | .NET 8; dashboard breakdowns of each income / expenditure class |
| 2024-04 – 09 | Calendar (FullCalendar): calendars, events, reminders, sharing, browsing others' calendars |
| 2024-05 | Private notes, per-account time zones, default monetary unit, dashboard year / month selection |
| 2024-09 | Maroik.FileStorage service |
| 2025-01 | Attachments and inline images for forum posts, calendar events and private notes; server resource monitoring on the admin dashboard; Let's Encrypt certificates renewed automatically with Certbot |
| 2025-03 | .NET 9 |
| 2025-04 | MariaDB → PostgreSQL |
| 2025-05 | DNS moved from AWS Route 53 to Cloudflare; ClamAV virus scanning of uploads; public demo account |
| 2025-07 | Database transactions for account-book writes |
| 2025-08 | BCrypt password hashing |
| 2025-11 |
Solid arrows show which project calls which, always through an interface in Maroik.Core.Contract
(interfaces and DTOs only, so it calls nothing and is not drawn). The Website calls the Service layer and nothing
else. Maroik.Worker and Maroik.FileStorage are separate processes: when a use case sends mail or stores a file,
the Service hands it to Client, and Client puts the mail job on RabbitMQ for the Worker (which sends it over SMTP)
or uploads / downloads the file over HTTP to FileStorage — the dotted lines. Repository and Client implementations
are registered only in each host's composition root (Program.cs, WorkerHost), never called from controllers,
views or handlers.
flowchart TB
Website["Maroik.Website<br/>MVC controllers · views · TypeScript"]
Service["Maroik.Core.Service<br/>use cases · transactions · logging"]
Domain["Maroik.Core.Domain<br/>aggregates · value objects · policies"]
Repository["Maroik.Core.Repository<br/>EF Core repositories"]
PostgreSQL["Maroik.Core.PostgreSQL<br/>DbContext · mapped models"]
Client["Maroik.Core.Client<br/>file storage · RabbitMQ · SMTP · ClamAV"]
Worker["Maroik.Worker<br/>e-mail consumer · reminder scheduler"]
FileStorage["Maroik.FileStorage<br/>internal file API"]
Website --> Service
Service --> Domain
Service --> Repository
Service -- "files · mail body · mail queue" --> Client
Repository -- "rows ↔ aggregates" --> Domain
Repository --> PostgreSQL
Client -. "mail jobs (RabbitMQ)" .-> Worker
Client -. "HTTP upload / download" .-> FileStorage
Worker -- "mail status · due reminders" --> Service
Worker -- "send mail (SMTP)" --> Client
FileStorage -- "virus scan (ClamAV)" --> Client
Email, Money, CurrencyCode, …), policies
(finance class taxonomies, reminder limits and schedules, fixed schedules, passwords, uploads, File Manager quotas
and names, Kanban lanes) and the budget formula engine. It calls no other
project, logs nothing, never reads the clock (the current time is passed in) and reports every error through
DomainError, so the outer layers can translate it.BeginAsync / CommitAsync / RollbackAsync), maps to DTOs and logs. It
calls Repository and Client only through their Contract interfaces.{ result, error }
JSON reply; views only render. The navigation menu doubles as the access-control list (AuthorizationFilter).All of this is enforced by architecture tests that fail the build, among them:
Domain_ShouldNot_DependOnAnyOtherLayer, Domain_ShouldNot_Log, Domain_ShouldNot_ReadTheClock,
Domain_ErrorMessages_MustGoThrough_DomainError, BoundedContext_ShouldNot_DependOnAnotherBoundedContext,
ServiceLayer_MustNotProjectReference_AnyInfrastructureOrPersistenceProject,
InfrastructureLayers_MustBuildOnContractOnly_NotOnEachOtherOrService, ProjectReferenceGraph_MustBeAcyclic,
Controllers_ShouldNot_DependOnRepositoryOrClientOrServiceNamespace,
PostActions_MustCarry_AntiforgeryAndRoleAttributes and ServiceResults_TakeTheirMessageKey_FromServiceErrorKeys.
flowchart LR
Browser --> Cloudflare["Cloudflare"]
cloudflared["cloudflared<br/>(outbound tunnel)"] -- "connects out" --> Cloudflare
cloudflared -- "HTTP :80" --> Website["Maroik.Website"]
Website --> PostgreSQL[("PostgreSQL 17")]
Website --> Valkey[("Valkey<br/>session · Data Protection keys")]
Website -- "HTTP upload / download" --> FileStorage["Maroik.FileStorage"]
FileStorage -- "scan (TCP 3310)" --> ClamAV["ClamAV"]
Website -- "publish mail jobs" --> RabbitMQ[("RabbitMQ")]
RabbitMQ -- "deliver mail jobs" --> Worker["Maroik.Worker"]
Worker -- "mail status · due reminders" --> PostgreSQL
Worker -- "send mail" --> SMTP["SMTP relay"]
Every service runs as a container under Docker Compose. Uploads are scanned by ClamAV before they are stored (File
Manager uploads in the background; an administrator can turn the scan off for one account's File Manager), the file
storage is reachable only on the internal network, and outgoing mail is queued in RabbitMQ and sent by the worker,
which also sends the due calendar reminders once a minute. In production the site is reached through a Cloudflare Tunnel, so the host publishes no port (a build deployed with
Deploy.sh choice 1 can instead use port forwarding behind Caddy, or plain HTTP without a domain).
The page scripts are written in TypeScript (Maroik.Website/TypeScripts/**/site.ts) and compiled 1:1 by tsc
into plain global scripts — no bundler, no runtime framework. They are type-checked with strict plus
noUncheckedIndexedAccess, exactOptionalPropertyTypes and the other strict options, and contain no any and no
type assertion. Anything the compiler cannot prove is checked at run time instead: each role's layout script
provides a small toolkit (fieldValue, parseJson, byId, required, onReply, …), every AJAX reply is
validated against a check that mirrors the controller's JSON field for field, and a reply of the wrong shape shows a
generic error instead of failing half-way through a handler.
Maroik.Website/TypeScripts/{admin,anonymous,user}/custom/**/site.ts and compiled 1:1 by tsc to Maroik.Website/wwwroot/**/custom/**/site.js (no bundler, no Babel). The release build (npm run build, used by the Docker image) then minifies each site.js in place with terser; npm run build:debug leaves it readable with a source map.--strict; each file is an IIFE-wrapped global script (no import/export).Maroik.Core.* (DDD + Clean Architecture). Client-side rule checks are UX mirrors; the server always re-validates.wwwroot/**/site.js is build output and git-ignored. Docker Compose builds generate it in the Dockerfile's Node stage; running the site on the host, or running the client-script tests, needs Node 22 + npm ci in Maroik.Website/.tsc on dotnet build when node_modules/ is present; Docker builds run it in a dedicated node:22-alpine stage.Maroik.Website/TypeScripts.Tests/ (one *.test.ts per script, exercising the compiled site.js).Maroik.Website/TypeScripts/README.md.Ssl, Http), while two-letter acronyms stay uppercase (e.g. DB, IO).Maroik.SSL, Maroik.DB, Maroik.Log, Maroik.Crontab, Maroik.DeployOps — intentionally keep their existing casing. These names are referenced by docker-compose volume mounts, deployment scripts, and live directories on the production server, so they are kept stable by design rather than renamed for stylistic consistency.The goal is that a test fails whenever the behaviour it describes breaks — not that lines are merely executed.
WebApplicationFactory against a real
database; and the whole site in a real browser with Playwright.BeginAsync / CommitAsync / RollbackAsync), the exact error code / key / type, and every catch's log entry.ModelMatchesSchemaTests); every message key a controller can receive must exist in its en-US and ko-KR
resources (ServiceErrorKeyResxTests); every localized text a page script reads must be rendered by its view.site.js together with the real layout
script, against fixtures that carry what the real views and replies carry. Guard tests keep any and type
assertions out of the TypeScript, and a compile probe proves that untyped library values are rejected.StylesheetFlowTests read the computed style of the
elements each of the site's own CSS rules targets on the real pages, after the AdminLTE / Bootstrap / FullCalendar
stylesheets they override, so a rule that is removed, outranked or no longer loaded fails a test.Every source project has a matching *.Tests project (xUnit v3 on Microsoft.Testing.Platform), plus the client-script suite and an end-to-end suite — about 3,700 .NET tests and 500 client-script tests. Maroik.slnx builds them all; CI runs each with coverage (.github/workflows/ci-cd.yml, gate: line >= 80 %, branch >= 65 %).
| Tests | What they cover | Needs Docker |
|---|---|---|
Maroik.Core.Domain.Tests, Maroik.Core.Contract.Tests | entities, policies, value objects, DTOs, the layering / package-reference architecture rules | no |
Maroik.Core.Service.Tests | use-case orchestration (mocked repositories) | no |
Maroik.Core.Repository.Tests | EF Core repositories against a real PostgreSQL 17 loaded from Maroik.DB/.../Debugging/Init.sql | yes |
Maroik.Core.PostgreSQL.Tests | the EF model against the real schema of both init scripts (tables, columns, keys, foreign keys, indexes) | yes |
Maroik.Core.Client.Tests | mail (in-process SMTP server), file storage, ClamAV, RabbitMQ publisher and health check (real RabbitMQ) | yes |
Maroik.Worker.Tests | the e-mail consumer and the worker host, against a real RabbitMQ and an SMTP relay | yes |
Maroik.FileStorage.Tests | the file-storage service | no |
Maroik.Website.Tests | controllers, filters, views and start-up through WebApplicationFactory against a real PostgreSQL | yes |
Maroik.Website/TypeScripts.Tests | every client script (Vitest + jsdom), run with npm test in Maroik.Website/ | no |
Maroik.E2E.Tests | the running site in a real browser (Playwright), including the computed styles of the site's own stylesheets over the vendor CSS | yes |
dotnet build Maroik.slnx -p:RunClientScriptTests=false
dotnet test --project Maroik.Core.Service.Tests # any single project
(cd Maroik.Website && npm ci && npm test) # client scripts
global.json (rollForward: disable, so exactly that version)npm ci in Maroik.Website/) for the client scripts and their tests when building on the hostflowchart LR
PR["Pull request"] --> Test["build-and-test<br/>build · Vitest · .NET tests · E2E · coverage gate"]
Main["Push to main"] --> Test
Test -- "main only" --> Publish["publish<br/>version · build 3 images · push · tag"]
Publish --> GHCR[("ghcr.io<br/>maroik-website / -filestorage / -worker")]
Publish --> Tag["git tag vX.Y.Z"]
Cron["Server cron<br/>AutoDeploy.sh, every 5 min"] -- "pull image tag sha-commit" --> GHCR
Cron --> Apps["Replace the app containers<br/>(DB, Valkey, RabbitMQ keep running)"]
One workflow, .github/workflows/ci-cd.yml, with two jobs.
build-and-test runs on every pull request and every push to main: a warning-free build, the client-script
suite, every .NET test project one after another, then the E2E suite on its own (run side by side, the other projects'
Testcontainers change the Docker network under the browser), and the coverage gate (line >= 80 %, branch >= 65 %). It
also builds the Dockerfile's client-script stage both ways, so a pull request that breaks the image build fails before
it is merged: the release build must ship minified site.js without source maps, the debug build the opposite.publish runs only on main, after build-and-test passed. It computes the version, builds the Website,
FileStorage and Worker images with that version stamped into the assemblies, checks that the website image ships
minified scripts and no source maps, pushes the images to GHCR and tags the commit.Versioning. Semantic Versioning from Conventional Commits since the last
vX.Y.Z tag, one step per release — the largest change wins: type!: or BREAKING CHANGE: raises MAJOR, feat: MINOR,
fix: PATCH; docs:, refactor:, test:, chore: and the like leave the version as it is. The image tags are
sha-<commit>, latest and, when the version moved, X.Y.Z. The assemblies carry X.Y.Z+<commit> as their
InformationalVersion; a local or debug build, which gets no version from CI, keeps the one in Directory.Build.props.
Deployment. Maroik.DeployOps/Deploy.sh asks how to deploy (see How to Deploy): 1 builds the three images from the
source on the server, with no GitHub; 2 deploys what CI published, and keeps it current: every five minutes
Maroik.Crontab/AutoDeploy.sh (installed by choice 2 only) looks at the newest commit on main; once its publish job has succeeded it pulls that commit's three images and replaces only the app containers, so
the database, Valkey (sessions) and RabbitMQ keep running. A deploy whose pull fails leaves the running containers alone
and is not retried. A release that changes Maroik.DB/ (the schema) is not deployed automatically: it is logged, and the
full deploy in How to Deploy (above) is run by hand, because it rebuilds the database from Init.sql and restores the backup.
git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature')git push origin feature/AmazingFeature)For issues and questions, please use the GitHub issue tracker.
This project is licensed under the MIT License - see the LICENSE file for details.
ServerSetting__SmtpOptions__* in .env.prod) and, for the Cloudflare Tunnel, its token
(TUNNEL_TOKEN); .env.prod.example lists every setting with dummy values.| .NET 10; data access split into repositories and clients |
| 2025-12 | Project split into Core layers (Repository, Service, Client, PostgreSQL) |
| 2026-04 | Maroik.Core.Contract; first unit tests |
| 2026-05 – 08 | DDD + Clean Architecture refactor, ending with the Maroik.Core.Domain project; CI/CD with a coverage gate; Serilog |
| 2026-07 | Valkey (replacing Redis), RabbitMQ and the Maroik.Worker mail service |
| 2026-09 | Client scripts migrated to TypeScript; repository and E2E tests on real PostgreSQL (Testcontainers); a series of whole-solution reviews fixing race conditions and security gaps; strict TDD and logging rules; mutation testing (Domain 78 % → 92 %) |
| 2026-09-28 | Moved from the private repository to this public repository, where all development continues |
| 2026-10 | Service mutation score 70 % → 96 %; TLS ended at Cloudflare through a Cloudflare Tunnel; RSA replaced with ASP.NET Core Data Protection; memory-safety, logging-template, security and banned-API analyzers as build errors; admin and menu safeguards against locking administrators out; GitHub Actions CI (coverage gate, isolated E2E run), release-only minified client scripts, semver from Conventional Commits, images published to GHCR and pulled by the server; calendar reminders delivered by e-mail (scheduled by the domain, sent once a minute by the worker through the mail queue); one pinned .NET SDK with a weekly bump pull request; personal Kanban board; personal File Manager (chunked uploads, quotas, background virus scan, download progress); Income and Expenditure Budget sheets with an Excel-compatible formula engine; Deploy.sh hosting modes (Cloudflare Tunnel, port forwarding behind Caddy, no domain) |