@@ -39,12 +39,6 @@ root.addEventListener('click', (event) => {
3939 if ( ! actionTarget ) return
4040
4141 const action = actionTarget . dataset . action
42- if ( action === 'navigate-home' ) {
43- navigate ( { view : 'home' } )
44- }
45- if ( action === 'select-product' ) {
46- navigate ( { view : 'project' , productId : actionTarget . dataset . productId ?? firstProductId ( ) } )
47- }
4842 if ( action === 'select-repo' ) {
4943 selectedRepo = actionTarget . dataset . repo ?? selectedRepo
5044 selectedEnvVar = ''
@@ -61,27 +55,60 @@ render()
6155function render ( ) : void {
6256 const summary = dashboard . companySummary [ 0 ] ?? emptySummary ( )
6357 const product = selectedProduct ( )
58+ const activeSummary = route . view === 'home' ? summary : product . summary
6459
6560 root . innerHTML = `
66- <header class="topbar">
67- <div class="brand-block">
68- <p class="eyebrow">wanman FinOps</p>
69- <h1>${ escapeHtml ( dashboard . company . name ?? dashboard . company . id ) } </h1>
61+ <a class="skip-link" href="#main">Skip to main content</a>
62+ <div class="app-frame">
63+ <aside class="sidebar" aria-label="FinOps navigation">
64+ <div class="brand-block">
65+ <p class="eyebrow">wanman FinOps</p>
66+ <h1>${ escapeHtml ( dashboard . company . name ?? dashboard . company . id ) } </h1>
67+ </div>
68+ <div class="status-pill">
69+ <span>Sanitized demo data</span>
70+ <strong>${ dashboard . inventory . references . length } credential refs / no key values</strong>
71+ </div>
72+ <nav class="view-nav" aria-label="FinOps views">
73+ <a class="nav-item${ route . view === 'home' ? ' is-active' : '' } " href="${ routeToHash ( { view : 'home' } ) } "${ route . view === 'home' ? ' aria-current="page"' : '' } >
74+ <span>Dashboard</span>
75+ <small>Company rollup</small>
76+ </a>
77+ ${ dashboard . products . map ( ( item ) => {
78+ const active = route . view === 'project' && item . productId === product . productId
79+ return `
80+ <a class="nav-item${ active ? ' is-active' : '' } " href="${ routeToHash ( { view : 'project' , productId : item . productId } ) } "${ active ? ' aria-current="page"' : '' } >
81+ <span>${ escapeHtml ( item . name ) } </span>
82+ <small>${ formatMoney ( item . summary . grossProfit , item . summary . currency ) } gross profit</small>
83+ </a>
84+ `
85+ } ) . join ( '' ) }
86+ </nav>
87+ <div class="sidebar-note">
88+ <small>Pricing feeds</small>
89+ <strong>${ dashboard . pricing . sources . filter ( ( source ) => source . ok ) . length } /${ dashboard . pricing . sources . length } healthy</strong>
90+ </div>
91+ </aside>
92+ <div class="workspace">
93+ <header class="topbar">
94+ <div>
95+ <p class="eyebrow">${ route . view === 'home' ? 'Company console' : 'Project console' } </p>
96+ <h2>${ route . view === 'home' ? 'Cost, revenue, and ROI control plane' : escapeHtml ( product . name ) } </h2>
97+ </div>
98+ <div class="topbar-metrics" aria-label="Current view totals">
99+ <span><small>Revenue</small><strong>${ formatMoney ( activeSummary . revenue , activeSummary . currency ) } </strong></span>
100+ <span><small>Cost</small><strong>${ formatMoney ( activeSummary . cost , activeSummary . currency ) } </strong></span>
101+ <span><small>ROI</small><strong>${ formatRoi ( activeSummary . roi ) } </strong></span>
102+ </div>
103+ </header>
104+ <main id="main" class="shell" tabindex="-1">
105+ ${ route . view === 'home' ? homeView ( summary ) : projectView ( product ) }
106+ </main>
70107 </div>
71- <div class="status-pill">Sanitized demo data · ${ dashboard . inventory . references . length } credential refs · no key values</div>
72- </header>
73- <nav class="view-nav" aria-label="FinOps views">
74- <button class="nav-item${ route . view === 'home' ? ' is-active' : '' } " data-action="navigate-home">Dashboard</button>
75- ${ dashboard . products . map ( ( item ) => `
76- <button class="nav-item${ route . view === 'project' && item . productId === product . productId ? ' is-active' : '' } " data-action="select-product" data-product-id="${ escapeAttr ( item . productId ) } ">
77- ${ escapeHtml ( item . name ) }
78- </button>
79- ` ) . join ( '' ) }
80- </nav>
81- <main class="shell">
82- ${ route . view === 'home' ? homeView ( summary ) : projectView ( product ) }
83- </main>
108+ </div>
84109 `
110+
111+ root . querySelector ( '.view-nav .is-active' ) ?. scrollIntoView ( { block : 'nearest' , inline : 'nearest' } )
85112}
86113
87114function homeView ( summary : AccountingSummary ) : string {
@@ -146,7 +173,7 @@ function projectView(product: ProductDashboardSummary): string {
146173 return `
147174 <section class="project-heading">
148175 <div>
149- <p class="eyebrow">Project View</p>
176+ <p class="eyebrow"><a href=" ${ routeToHash ( { view : 'home' } ) } ">Dashboard</a> / Project View</p>
150177 <h2>${ escapeHtml ( product . name ) } </h2>
151178 <p>${ escapeHtml ( product . description ?? 'Sanitized FinOps project' ) } </p>
152179 </div>
@@ -237,14 +264,27 @@ function profitabilityChart(points: ProfitabilityTrendPoint[], currency: string)
237264 const profitPath = buildLineChartPath ( points . map ( ( point ) => point . grossProfit ) , dimensions , domain )
238265 const zeroY = zeroLineY ( domain , dimensions )
239266 const latest = points . at ( - 1 )
267+ const gridLines = [ 0.25 , 0.5 , 0.75 ] . map ( ( ratio ) => {
268+ const y = dimensions . padding + ratio * ( dimensions . height - dimensions . padding * 2 )
269+ return `<line x1="${ dimensions . padding } " y1="${ y } " x2="${ dimensions . width - dimensions . padding } " y2="${ y } " class="chart-gridline" />`
270+ } ) . join ( '' )
271+ const revenueMarkers = chartMarkers ( points . map ( ( point ) => point . revenue ) , dimensions , domain , 'revenue-marker' )
272+ const costMarkers = chartMarkers ( points . map ( ( point ) => point . cost ) , dimensions , domain , 'cost-marker' )
273+ const profitMarkers = chartMarkers ( points . map ( ( point ) => point . grossProfit ) , dimensions , domain , 'profit-marker' )
240274
241275 return `
242276 <div class="chart-wrap">
243- <svg class="trend-chart" viewBox="0 0 ${ dimensions . width } ${ dimensions . height } " role="img" aria-label="Revenue cost and gross profit trend">
277+ <svg class="trend-chart" viewBox="0 0 ${ dimensions . width } ${ dimensions . height } " role="img" aria-labelledby="profitability-chart-title profitability-chart-desc">
278+ <title id="profitability-chart-title">Revenue, cost, and gross profit trend</title>
279+ <desc id="profitability-chart-desc">Line chart comparing revenue, provider cost, and gross profit across the selected accounting periods.</desc>
280+ ${ gridLines }
244281 <line x1="${ dimensions . padding } " y1="${ zeroY } " x2="${ dimensions . width - dimensions . padding } " y2="${ zeroY } " class="zero-line" />
245282 <path d="${ revenuePath } " class="chart-line revenue-line" />
246283 <path d="${ costPath } " class="chart-line cost-line" />
247284 <path d="${ profitPath } " class="chart-line profit-line" />
285+ ${ revenueMarkers }
286+ ${ costMarkers }
287+ ${ profitMarkers }
248288 </svg>
249289 <div class="chart-legend">
250290 ${ legendItem ( 'Revenue' , 'revenue' ) }
@@ -269,10 +309,32 @@ function zeroLineY(domain: { min: number, max: number }, dimensions: { height: n
269309 return Math . round ( y * 100 ) / 100
270310}
271311
312+ function chartMarkers (
313+ values : number [ ] ,
314+ dimensions : { width : number , height : number , padding : number } ,
315+ domain : { min : number , max : number } ,
316+ className : string ,
317+ ) : string {
318+ const innerWidth = dimensions . width - dimensions . padding * 2
319+ const innerHeight = dimensions . height - dimensions . padding * 2
320+ const xStep = values . length > 1 ? innerWidth / ( values . length - 1 ) : 0
321+ const range = domain . max - domain . min || 1
322+
323+ return values . map ( ( value , index ) => {
324+ const x = dimensions . padding + index * xStep
325+ const y = dimensions . padding + ( 1 - ( ( value - domain . min ) / range ) ) * innerHeight
326+ return `<circle cx="${ roundChartValue ( x ) } " cy="${ roundChartValue ( y ) } " r="3.4" class="chart-marker ${ className } " />`
327+ } ) . join ( '' )
328+ }
329+
330+ function roundChartValue ( value : number ) : number {
331+ return Math . round ( value * 100 ) / 100
332+ }
333+
272334function portfolioRow ( product : ProductDashboardSummary ) : string {
273335 const latest = product . profitabilityTrend . at ( - 1 )
274336 return `
275- <button class="portfolio-row" data-action="select- product" data-product-id=" ${ escapeAttr ( product . productId ) } ">
337+ <a class="portfolio-row" href=" ${ routeToHash ( { view : 'project' , productId : product . productId } ) } " aria-label="Open ${ escapeAttr ( product . name ) } project ">
276338 <span>
277339 <strong>${ escapeHtml ( product . name ) } </strong>
278340 <small>${ escapeHtml ( product . owner ?? 'Demo' ) } · ${ escapeHtml ( product . lifecycle ?? 'Demo' ) } </small>
@@ -281,7 +343,7 @@ function portfolioRow(product: ProductDashboardSummary): string {
281343 <b>${ formatMoney ( product . summary . grossProfit , product . summary . currency ) } </b>
282344 <small>${ latest ? `${ formatPeriod ( latest . period ) } ${ formatRoi ( latest . roi ) } ` : formatRoi ( product . summary . roi ) } </small>
283345 </span>
284- </button >
346+ </a >
285347 `
286348}
287349
@@ -364,19 +426,6 @@ function normalizeRoute(next: FinopsRoute): FinopsRoute {
364426 return { view : 'home' }
365427}
366428
367- function navigate ( next : FinopsRoute ) : void {
368- const normalized = normalizeRoute ( next )
369- const hash = routeToHash ( normalized )
370- if ( window . location . hash === hash ) {
371- route = normalized
372- selectedRepo = ''
373- selectedEnvVar = ''
374- render ( )
375- return
376- }
377- window . location . hash = hash
378- }
379-
380429function firstProductId ( ) : string {
381430 return dashboard . products [ 0 ] ?. productId ?? ''
382431}
@@ -517,7 +566,7 @@ function metadataPill(label: string, value: string): string {
517566}
518567
519568function legendItem ( label : string , tone : 'revenue' | 'cost' | 'profit' ) : string {
520- return `<span class="legend-item ${ tone } "><i></i>${ escapeHtml ( label ) } </span>`
569+ return `<span class="legend-item ${ tone } "><i aria-hidden="true" ></i>${ escapeHtml ( label ) } </span>`
521570}
522571
523572function trendRange ( points : ProfitabilityTrendPoint [ ] ) : string {
0 commit comments