Making the JADA Maintenance Dashboard's Task Card Clickable: Frontend Navigation in S3-Hosted Static Sites

What Was Done

The JADA Maintenance Hub dashboard displays a "Total Tasks" card showing an aggregated count of active maintenance items. Previously, this card was static—displaying the number but providing no interaction. Users had to manually navigate to the Tasks tab to view the underlying task list. This session made the card clickable to instantly switch the dashboard view to the Tasks tab, improving UX and reducing friction in the maintenance workflow.

The change required modifying the S3-hosted HTML file, updating CSS for interactive states, adding JavaScript event handlers, and invalidating the CloudFront distribution cache to push the update live.

Technical Details

File Location and Architecture

The maintenance hub lives as a static HTML/CSS/JavaScript application in an S3 bucket rather than being served from a traditional web server. This decision trades dynamic backend rendering for:

  • Lower latency: S3 + CloudFront edge caching means content is served from geographically distributed endpoints
  • Zero infrastructure overhead: No servers to patch, restart, or scale
  • Built-in versioning: S3 object versioning provides rollback capability if needed
  • Cost efficiency: Pay only for storage and data transfer, not compute hours

File path: s3://maintenance-hub-bucket/index.html (exact bucket name withheld for security)

The file was downloaded locally, modified, tested, and re-uploaded. CloudFront distribution configured to cache the index.html with invalidation support for cache busting on updates.

DOM Structure and Tab Navigation

The dashboard uses a tabbed interface with the following structure:

<div class="tab-content" id="systems-tab">
  <!-- Task table and system items rendered here -->
</div>

The tab-switching logic uses a JavaScript function that was already in place:

function switchTab(tabName) {
  // Hide all tabs
  document.querySelectorAll('.tab-content').forEach(tab => {
    tab.style.display = 'none';
  });
  // Show selected tab
  document.getElementById(tabName + '-tab').style.display = 'block';
  // Update active nav state
  document.querySelectorAll('[data-tab]').forEach(nav => {
    nav.classList.remove('active');
  });
  document.querySelector('[data-tab="' + tabName + '"]').classList.add('active');
}

Why this approach: The existing `switchTab()` function provided a clean, single source of truth for navigation logic. Rather than duplicating tab-switching code, we reused this abstraction by calling it from the new card click handler.

Making the Card Interactive

The Total Tasks card was originally a static <div> with class info-card. The modification involved three changes:

1. HTML (Semantic markup):

<div class="info-card info-card-link" onclick="switchTab('systems')">
  <div class="card-label">Total Tasks</div>
  <div class="card-value">82</div>
</div>

We added the info-card-link class as a modifier to signal that this card is interactive, and assigned the onclick handler directly (acceptable for single-purpose interactions in this context, though event delegation via data attributes would be preferred in larger applications).

2. CSS for interactive states:

.info-card-link {
  cursor: pointer;
  transition: all 0.2s ease-in-out;
}

.info-card-link:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
  background-color: rgba(100, 150, 255, 0.05);
}

.info-card-link:active {
  transform: translateY(0);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

Why these CSS choices:

  • cursor: pointer signals clickability to users immediately (native browser affordance)
  • translateY(-2px) lift on hover provides visual feedback without obscuring content
  • box-shadow enhancement reinforces the interactive elevation change
  • transition: 0.2s is fast enough to feel responsive (~200ms is the threshold for perceived instantaneity) but not so fast that it feels twitchy
  • :active state confirms the click is being registered

Infrastructure and Deployment

S3 Upload

The modified index.html was uploaded to the maintenance S3 bucket using the AWS CLI:

aws s3 cp ./index.html s3://maintenance-hub-bucket/index.html --metadata-directive REPLACE

The --metadata-directive REPLACE flag ensures the object's metadata is recalculated, which is important for CloudFront cache headers.

CloudFront Cache Invalidation

Simply uploading to S3 doesn't automatically update cached content at CloudFront edge locations. An explicit invalidation was needed:

aws cloudfront create-invalidation --distribution-id [DISTRIBUTION_ID] --paths "/index.html"

The specific distribution ID (withheld here) was identified via:

aws cloudfront list-distributions --query 'DistributionList.Items[?DefaultRootObject==`index.html`]'

Why invalidation is necessary: CloudFront caches based on HTTP cache headers. Without invalidation, edge nodes would continue serving the old version for up to 24 hours (depending on TTL configuration). Invalidation forces all edge nodes to refresh immediately, ensuring users see the updated code on their next page load.

Key Decisions

Why S3 + CloudFront instead of a backend service?

The maintenance hub is a read-heavy, low-update-frequency application. It pulls task data from external Lambda APIs (via REST calls embedded in the JavaScript). This architecture avoids:

  • Server-side rendering overhead
  • Database query load (data is fetched client-side from Lambda)
  • Session management complexity

The tradeoff: all business logic must live in JavaScript, which is acceptable here since the dashboard is primarily a display layer for data fetched from serverless functions.

Why modify onclick directly instead of event listeners?

For a single card interaction, inline onclick is pragmatic and reduces boilerplate. In a larger application with many interactive elements, a delegated event listener pattern would be preferable to avoid polluting the DOM with handler attributes.

Why use CSS transforms instead of repositioning?

The transform: translateY(-2px) approach is GPU-accelerated and doesn't