added petrol station project
All checks were successful
Deploy dzanan.net / deploy (push) Successful in 41s
All checks were successful
Deploy dzanan.net / deploy (push) Successful in 41s
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -30,6 +30,17 @@ import { ProjectDiagramKind } from './portfolio-project.model';
|
||||
<g class="diagram-note"><text x="440" y="40">OFFLINE QUEUE · RETRY</text><text x="110" y="270">GPS · VISITS</text><text x="440" y="390">LATEST LOCATION PER USER</text></g>
|
||||
</svg>
|
||||
}
|
||||
@case ('fuel-station-platform') {
|
||||
<svg viewBox="0 0 900 410" role="img" aria-label="Fuel station management system architecture">
|
||||
<g class="diagram-lines"><path d="M200 107H290V209H330M200 302H290V209M550 209H620V107H700M550 209H620V305H660"/></g>
|
||||
<g class="diagram-node"><rect x="30" y="60" width="170" height="95"/><text x="115" y="100">FORECOURT</text><text class="small" x="115" y="124">PUMPS · CONTROLLER</text></g>
|
||||
<g class="diagram-node"><rect x="30" y="255" width="170" height="95"/><text x="115" y="295">PAYMENT TERMINAL</text><text class="small" x="115" y="319">CARD · PIN · RECEIPT</text></g>
|
||||
<g class="diagram-node accent"><rect x="330" y="150" width="220" height="118"/><text x="440" y="198">STATION MIDDLEWARE</text><text class="small" x="440" y="224">VB.NET · STATE MACHINES</text><text class="small" x="440" y="246">JOURNAL · OUTBOX</text></g>
|
||||
<g class="diagram-node"><rect x="700" y="60" width="170" height="95"/><text x="785" y="100">STORE POS · ERP</text><text class="small" x="785" y="124">TRANSACTION FILES</text></g>
|
||||
<g class="diagram-node accent"><rect x="660" y="255" width="210" height="100"/><text x="765" y="296">CENTRAL PLATFORM</text><text class="small" x="765" y="320">.NET 10 · FLEET LIMITS</text><text class="small" x="765" y="340">SIGNALR · ADMIN WEB</text></g>
|
||||
<g class="diagram-note"><text x="440" y="40">FAIL CLOSED FOR FLEET · OPEN FOR CASH</text><text x="785" y="40">ATOMIC WRITE · RENAME CONTRACT</text><text x="440" y="390">RESERVE → AUTHORIZE → COMPLETE</text></g>
|
||||
</svg>
|
||||
}
|
||||
@case ('travel-platform') {
|
||||
<svg viewBox="0 0 900 410" role="img" aria-label="Travel operations platform architecture">
|
||||
<g class="diagram-lines"><path d="M180 100H345M555 100h165M450 158v94M180 310h165M555 310h165"/><path d="M180 100v210M720 100v210"/></g>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -16,7 +16,7 @@ import { PORTFOLIO_PROJECTS } from './portfolio-projects.data';
|
||||
<nav class="breadcrumb" aria-label="Breadcrumb"><a routerLink="/">~/</a><span>work</span></nav>
|
||||
<span class="label">// selected work</span>
|
||||
<h1>Production systems, anonymized.</h1>
|
||||
<p class="lead">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.</p>
|
||||
<p class="lead">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.</p>
|
||||
</header>
|
||||
|
||||
<section class="section tight shell" aria-label="Case studies">
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.' }
|
||||
|
||||
Reference in New Issue
Block a user