diff --git a/Claude outputs/case-study-fuel-station.png b/Claude outputs/case-study-fuel-station.png new file mode 100644 index 0000000..8ad3630 Binary files /dev/null and b/Claude outputs/case-study-fuel-station.png differ diff --git a/public/sitemap.xml b/public/sitemap.xml index 20cd850..933d24f 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -7,6 +7,7 @@ https://dzanan.net/contact https://dzanan.net/work/secure-ai-integration-gateway https://dzanan.net/work/field-sales-mobile-platform + https://dzanan.net/work/fuel-station-management-system https://dzanan.net/work/travel-operations-platform https://dzanan.net/work/service-operations-platform https://dzanan.net/work/biodiversity-data-platform diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index 08f9f83..cfe4a16 100755 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -8,6 +8,7 @@ export const serverRoutes: ServerRoute[] = [ { path: 'contact', renderMode: RenderMode.Prerender }, { path: 'work/secure-ai-integration-gateway', renderMode: RenderMode.Prerender }, { path: 'work/field-sales-mobile-platform', renderMode: RenderMode.Prerender }, + { path: 'work/fuel-station-management-system', renderMode: RenderMode.Prerender }, { path: 'work/travel-operations-platform', renderMode: RenderMode.Prerender }, { path: 'work/service-operations-platform', renderMode: RenderMode.Prerender }, { path: 'work/biodiversity-data-platform', renderMode: RenderMode.Prerender }, diff --git a/src/app/app.routes.spec.ts b/src/app/app.routes.spec.ts index 6526661..ec9582b 100755 --- a/src/app/app.routes.spec.ts +++ b/src/app/app.routes.spec.ts @@ -10,6 +10,7 @@ describe('app routes', () => { 'contact', 'work/secure-ai-integration-gateway', 'work/field-sales-mobile-platform', + 'work/fuel-station-management-system', 'work/travel-operations-platform', 'work/service-operations-platform', 'work/biodiversity-data-platform', @@ -33,6 +34,7 @@ describe('app routes', () => { expect(projectIds).toEqual([ 'ai-integration', 'field-sales', + 'fuel-management', 'travel-operations', 'service-operations', 'biodiversity-data' diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 12c0a9e..ac8ff38 100755 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -32,6 +32,11 @@ export const routes: Routes = [ data: { projectId: 'field-sales' }, loadComponent: () => import('./features/portfolio/portfolio-page.component').then(m => m.PortfolioPageComponent) }, + { + path: 'work/fuel-station-management-system', + data: { projectId: 'fuel-management' }, + loadComponent: () => import('./features/portfolio/portfolio-page.component').then(m => m.PortfolioPageComponent) + }, { path: 'work/travel-operations-platform', data: { projectId: 'travel-operations' }, diff --git a/src/app/features/work/portfolio-project.model.ts b/src/app/features/work/portfolio-project.model.ts index 9a89dac..18e3882 100644 --- a/src/app/features/work/portfolio-project.model.ts +++ b/src/app/features/work/portfolio-project.model.ts @@ -1,6 +1,7 @@ export type PortfolioProjectId = | 'ai-integration' | 'field-sales' + | 'fuel-management' | 'travel-operations' | 'service-operations' | 'biodiversity-data'; @@ -8,6 +9,7 @@ export type PortfolioProjectId = export type ProjectDiagramKind = | 'trust-boundaries' | 'field-sales-platform' + | 'fuel-station-platform' | 'travel-platform' | 'multitenant-platform' | 'research-platform'; diff --git a/src/app/features/work/portfolio-projects.data.spec.ts b/src/app/features/work/portfolio-projects.data.spec.ts index 0b463ab..32e3747 100644 --- a/src/app/features/work/portfolio-projects.data.spec.ts +++ b/src/app/features/work/portfolio-projects.data.spec.ts @@ -2,10 +2,10 @@ import { getPortfolioProject, PORTFOLIO_PROJECTS } from './portfolio-projects.da describe('portfolio project registry', () => { it('contains four complete, uniquely addressable case studies', () => { - expect(PORTFOLIO_PROJECTS.length).toBe(5); - expect(new Set(PORTFOLIO_PROJECTS.map(project => project.id)).size).toBe(5); - expect(new Set(PORTFOLIO_PROJECTS.map(project => project.slug)).size).toBe(5); - expect(PORTFOLIO_PROJECTS.map(project => project.index)).toEqual(['01', '02', '03', '04', '05']); + expect(PORTFOLIO_PROJECTS.length).toBe(6); + expect(new Set(PORTFOLIO_PROJECTS.map(project => project.id)).size).toBe(6); + expect(new Set(PORTFOLIO_PROJECTS.map(project => project.slug)).size).toBe(6); + expect(PORTFOLIO_PROJECTS.map(project => project.index)).toEqual(['01', '02', '03', '04', '05', '06']); for (const project of PORTFOLIO_PROJECTS) { expect(project.contributions.length).toBeGreaterThan(2); @@ -23,6 +23,17 @@ describe('portfolio project registry', () => { 'WorkLogManager', 'OrderCom', 'ordc', + 'Delta Petrol', + 'DeltaPetrol', + 'Delta-Petrol', + 'Trgosped', + 'E-Line', + 'ELine', + 'DOMS', + 'Hesel', + 'Avera', + 'POSInterfejs', + 'InterfejsCentral', 'intrix-mcp', 'Unija', 'git.dzanan.net', diff --git a/src/app/features/work/portfolio-projects.data.ts b/src/app/features/work/portfolio-projects.data.ts index 22d0f96..32219a5 100644 --- a/src/app/features/work/portfolio-projects.data.ts +++ b/src/app/features/work/portfolio-projects.data.ts @@ -101,8 +101,59 @@ export const PORTFOLIO_PROJECTS: readonly PortfolioProject[] = [ 'Case study: an offline-first Ionic field sales app with a local ERP replica, order and credit-note sync, barcode and GPS, and a SignalR realtime tracking backend.' }, { - id: 'travel-operations', + id: 'fuel-management', index: '03', + slug: 'fuel-station-management-system', + title: 'Fuel Station Management System', + category: 'Ground-up system · Desktop, web & realtime', + period: '2026', + role: 'Architect and sole engineer', + tagline: 'Forecourt & payment-terminal middleware, multi-tenant fleet limits', + summary: + 'A complete fuel-management system for gas stations, built from the ground up: a Windows desktop middleware that drives forecourt controllers and outdoor payment terminals and hands every sale to the store POS, plus a multi-tenant central platform with an Angular admin app that enforces fleet-card fuel limits at the pump in real time.', + challenge: + 'Fuel dispensers, an outdoor payment terminal, bank pre-authorizations, the store POS and the operator’s ERP all had to agree on every litre sold — including at night, unattended, when the terminal is the only cashier. Fleet customers needed monthly fuel limits enforced at the pump even though the ERP’s balances lag, and nothing could ever release money or lose a transaction because a network link or the application went down.', + contributions: [ + 'Built the station desktop middleware in VB.NET: forecourt controller integration — pump status, money and volume presets, transaction lock and clear for multi-POS sites, emergency stop, price pushes — over both a serial driver and the vendor’s COM API.', + 'Implemented the outdoor payment terminal as a screen-by-screen state machine: pump selection, barcode, amount, PIN, bank pre-authorization and completion, fuelling and receipt printing, with reconnect timers and recovery of uncompleted card transactions after a restart.', + 'Defined the file-based POS contract — one transaction file per sale, written temp-then-atomic-rename, with encrypted per-pump recovery files — so the existing POS and ERP agent consume sales unchanged.', + 'Designed the central platform on .NET 10: a multi-tenant minimal API with a master database plus one database per tenant, hashed station API keys, JWT roles, EF Core, a worker service syncing companies and vehicles from the ERP, and an Angular 22 zoneless admin app.', + 'Wrote the fleet-limit engine: available = monthly limit − fuel this business month − active reservations, with an idempotent create → authorize → complete/cancel protocol, SQL Server application locks per company, and a maintenance worker that moves unresolved reservations to review instead of releasing money.', + 'Extracted a pure, unit-tested reliability core — journal state machine, ordered outbox with exponential retry, recovery planner — and wired it into the desktop so fleet fuelling is fail-closed while card and manual sales keep working when the backend is unreachable.', + 'Ran the infrastructure end to end: VPN for stations, Caddy TLS in front of IIS, Gitea CI/CD with staged deploys and rollback, a private wiki, and a security hardening pass (secrets out of the repo, fixed-time PIN checks, log redaction, firewall audit).' + ], + decisions: [ + { + title: 'Reserve before you authorize', + detail: + 'The ERP balance lags, so the platform reserves limit first and only then authorizes the pump preset — the dispenser pumps against money that is already accounted for.' + }, + { + title: 'Never release money automatically', + detail: + 'Authorized reservations that go unresolved are moved to review with an incident, not cancelled. A person decides; the system only makes the state visible.' + }, + { + title: 'Fail closed for fleet cards, open for everything else', + detail: + 'If the central platform is unreachable, fleet-card fuelling stops but card and manual sales continue — a station never goes dark because a backend did.' + }, + { + title: 'Keep the legacy contract', + detail: + 'The store POS reads the same one-line transaction files it always did; atomic writes and recovery files sit behind that contract instead of replacing it.' + } + ], + outcome: + 'The operator runs the system in production across its stations with a second company onboarding: unattended night fuelling against prepayment, fleet limits enforced at the pump in real time, remote diagnostics and pump-incident dashboards for the back office, and a codebase with a tested reliability core rather than defensive patches.', + stack: ['VB.NET', '.NET 10', 'ASP.NET Core', 'SQL Server', 'EF Core', 'SignalR', 'Angular', 'Caddy', 'Gitea CI/CD'], + diagram: 'fuel-station-platform', + seoDescription: + 'Case study: a ground-up fuel-management system for gas stations — forecourt and payment-terminal middleware, a file-based POS contract, and a multi-tenant .NET 10 platform enforcing fleet fuel limits in real time.' + }, + { + id: 'travel-operations', + index: '04', slug: 'travel-operations-platform', title: 'Travel Operations & Automation Platform', category: 'Enterprise platform · Full-stack delivery', @@ -146,7 +197,7 @@ export const PORTFOLIO_PROJECTS: readonly PortfolioProject[] = [ }, { id: 'service-operations', - index: '04', + index: '05', slug: 'service-operations-platform', title: 'Multitenant Service Operations Platform', category: 'Business platform · Product evolution', @@ -190,7 +241,7 @@ export const PORTFOLIO_PROJECTS: readonly PortfolioProject[] = [ }, { id: 'biodiversity-data', - index: '05', + index: '06', slug: 'biodiversity-data-platform', title: 'Biodiversity Data & Field Research Platform', category: 'Scientific system · Platform modernization', diff --git a/src/app/features/work/system-diagram.component.ts b/src/app/features/work/system-diagram.component.ts index 7d7936f..6aa34dd 100644 --- a/src/app/features/work/system-diagram.component.ts +++ b/src/app/features/work/system-diagram.component.ts @@ -30,6 +30,17 @@ import { ProjectDiagramKind } from './portfolio-project.model'; OFFLINE QUEUE · RETRYGPS · VISITSLATEST LOCATION PER USER } + @case ('fuel-station-platform') { + + + FORECOURTPUMPS · CONTROLLER + PAYMENT TERMINALCARD · PIN · RECEIPT + STATION MIDDLEWAREVB.NET · STATE MACHINESJOURNAL · OUTBOX + STORE POS · ERPTRANSACTION FILES + CENTRAL PLATFORM.NET 10 · FLEET LIMITSSIGNALR · ADMIN WEB + FAIL CLOSED FOR FLEET · OPEN FOR CASHATOMIC WRITE · RENAME CONTRACTRESERVE → AUTHORIZE → COMPLETE + + } @case ('travel-platform') { diff --git a/src/app/features/work/work-page.component.spec.ts b/src/app/features/work/work-page.component.spec.ts index 6840b9c..646f511 100644 --- a/src/app/features/work/work-page.component.spec.ts +++ b/src/app/features/work/work-page.component.spec.ts @@ -13,10 +13,11 @@ describe('WorkPageComponent', () => { fixture.detectChanges(); const page = fixture.nativeElement as HTMLElement; - expect(page.querySelectorAll('[data-testid="project-row"]').length).toBe(5); + expect(page.querySelectorAll('[data-testid="project-row"]').length).toBe(6); expect(page.querySelector('a[href="/work/secure-ai-integration-gateway"]')).toBeTruthy(); expect(page.textContent).toContain('Secure AI Integration Gateway'); expect(page.textContent).toContain('Offline-First Field Sales App & Realtime Tracking'); + expect(page.textContent).toContain('Fuel Station Management System'); expect(page.textContent).toContain('Travel Operations & Automation Platform'); expect(page.textContent).toContain('Multitenant Service Operations Platform'); expect(page.textContent).toContain('Biodiversity Data & Field Research Platform'); diff --git a/src/app/features/work/work-page.component.ts b/src/app/features/work/work-page.component.ts index 501f84b..4ab0612 100644 --- a/src/app/features/work/work-page.component.ts +++ b/src/app/features/work/work-page.component.ts @@ -16,7 +16,7 @@ import { PORTFOLIO_PROJECTS } from './portfolio-projects.data'; work // selected work

Production systems, anonymized.

-

Five case studies from software that is running today: ERP-grade operations platforms, an offline-first mobile app with realtime tracking, incremental modernization, and AI integration with real trust boundaries. Names and internals are withheld; the decisions are real.

+

Six case studies from software that is running today: a ground-up fuel-management system for gas stations, ERP-grade operations platforms, an offline-first mobile app with realtime tracking, incremental modernization, and AI integration with real trust boundaries. Names and internals are withheld; the decisions are real.

@@ -41,7 +41,7 @@ export class WorkPageComponent { constructor() { inject(SeoService).update({ title: 'Work — Amar Džanan', - description: 'Five anonymized case studies from production software: AI integration, ERP operations platforms, an offline-first field sales app with realtime tracking, and scientific data.', + description: 'Six anonymized case studies from production software: AI integration, a fuel-station management system, ERP operations platforms, an offline-first field sales app with realtime tracking, and scientific data.', path: '/work' }); } diff --git a/src/app/shared/config/navigation.config.ts b/src/app/shared/config/navigation.config.ts index 6a06e8c..d3df908 100755 --- a/src/app/shared/config/navigation.config.ts +++ b/src/app/shared/config/navigation.config.ts @@ -12,7 +12,7 @@ export const CV_URL = '/cv.pdf'; /** Home-page index of the section pages. */ export const PAGE_INDEX: readonly { path: string; name: string; description: string }[] = [ - { path: '/work', name: 'work/', description: 'Five anonymized case studies from production systems.' }, + { path: '/work', name: 'work/', description: 'Six anonymized case studies from production systems.' }, { path: '/experience', name: 'experience/', description: 'Eight years of roles, and the capabilities they built.' }, { path: '/cv', name: 'cv.pdf', description: 'The full CV — read it here or download it.' }, { path: '/contact', name: 'contact/', description: 'Email, LinkedIn, and where to find me.' }