Building a Real-Time Dashboard Deep Link Architecture with Hash-Based Navigation
During a recent development sprint focused on operational visibility, we discovered a critical gap in our dashboard infrastructure: there was no standardized way to deep link directly to individual kanban cards. Engineers were sharing dashboard URLs without context, forcing teammates to manually hunt through dozens of cards to find the referenced work item. This post covers how we implemented a hash-based deep link system that integrates with our card management orchestrator and gives us granular, shareable URLs for every piece of work on our progress tracking board.
The Problem: Unlinked Cards in a Distributed Workflow
Our progress dashboard at progress.queenofsandiego.com displays dozens of kanban cards generated by an orchestrator agent that monitors infrastructure, email campaigns, analytics, and operational tasks. When an urgent card appeared (like our Mother's Day blast approval alert or the Paul Simon proof deadline), there was no way to generate a direct link to that specific card. Slack messages included only the dashboard URL, forcing context switches and manual navigation.
The root cause: the dashboard JavaScript was rendering cards from a data store but had no hash-routing logic. Card state was entirely URL-agnostic.
Technical Architecture: Hash Navigation Implementation
We implemented a three-layer solution:
- Card ID Generation — Each card created by the orchestrator now receives a unique ID in the format
t-{timestamp-hash}(e.g.,t-31aa2593). This ID is persisted in the card metadata stored in our backend data layer. - Deep Link Format — We standardized on the pattern
https://progress.queenofsandiego.com/#card-{id}. Hash-based routing avoids server-side page reloads and works seamlessly with our static CDN distribution. - Client-Side Hash Routing — The dashboard JavaScript now monitors
window.location.hashon page load and route changes, extracts the card ID, and programmatically scrolls to and highlights that card in the DOM.
Implementation Details
Step 1: Update Dashboard HTML Structure
Each card in the dashboard HTML now includes a data attribute with its unique ID:
<div class="card" data-card-id="t-31aa2593" id="card-t-31aa2593">
<h3>GA Audit + Orchestrator Report</h3>
<p>Audit complete. Card is live on dashboard...</p>
</div>
Step 2: Implement Hash Routing in Dashboard JavaScript
We added hash navigation logic to the main dashboard controller (file path: /Users/cb/Documents/repos/dashboard/public/js/dashboard-router.js):
function handleHashNavigation() {
const hash = window.location.hash;
if (hash.startsWith('#card-')) {
const cardId = hash.substring(6); // Remove '#card-' prefix
const cardElement = document.getElementById('card-' + cardId);
if (cardElement) {
// Remove previous highlight
document.querySelectorAll('.card.highlighted').forEach(el => {
el.classList.remove('highlighted');
});
// Highlight and scroll to target card
cardElement.classList.add('highlighted');
cardElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
// Listen for hash changes
window.addEventListener('hashchange', handleHashNavigation);
// Handle initial page load
document.addEventListener('DOMContentLoaded', handleHashNavigation);
Step 3: Update Orchestrator Card Generation
The orchestrator agent now includes card ID generation in its card creation workflow. When cards are created (file: /Users/cb/Documents/repos/orchestrator/src/card-generator.ts), they automatically receive a unique ID:
function generateCardId(): string {
const timestamp = Date.now();
const randomHash = Math.random().toString(36).substring(2, 8);
return `t-${timestamp.toString(36)}-${randomHash}`;
}
Infrastructure and Deployment
The dashboard is distributed via CloudFront (distribution ID: E2ABCD1234EFGH) with S3 as the origin bucket (progress-dashboard-prod). Hash-based routing requires no infrastructure changes—it's entirely client-side.
Cache headers remain unchanged because the hash fragment never reaches the server:
Cache-Control: public, max-age=3600
The card data itself is fetched from our backend API at /api/v1/cards (served by Lambda@Edge), which already returns cards with IDs. No API contract changes were needed.
Memory and Documentation Updates
We captured the deep link format in our project memory system (file: /Users/cb/.claude/projects/-Users-cb-Documents-repos/memory/feedback_dashboard_deep_links.md) to ensure consistent documentation for future work. This was critical because our orchestrator agent references these links in its handoff notes, and consistency prevents broken references.
The format is now documented as:
Card {id} is live — https://progress.queenofsandiego.com/#card-{id}
Key Decisions and Rationale
- Hash-Based Over Query Parameters: We chose hash navigation over query strings (
?cardId=t-31aa2593) because hash fragments don't trigger server requests and work seamlessly with our static CDN. This also improves SEO—the page is still cached as/index.htmlregardless of the hash. - Unique ID Format: The
t-{hash}format uses a timestamp component (converted to base36 for compactness) plus a random suffix. This ensures uniqueness across distributed card generation without database coordination, and the 't' prefix makes it immediately clear this is a card ID. - Smooth Scrolling: We implemented
scrollIntoView({ behavior: 'smooth' })to improve UX. When an engineer clicks a deep link, the card animates into view rather than jumping, making it clear what they're looking at. - Highlight State: The CSS class
.highlightedapplies a subtle border and background color change so the target card visually stands out. This prevents the "where did it go?" problem in long dashboards.
What's Next
The immediate next steps are operational: approve the Mother's Day blast (card t-31aa2593, deep link: https://progress.queenofsandiego.com/#card-t-31aa2593), prepare the Paul Simon proof deadline card, and grant GA Data API access to our service account in the Google Analytics Admin console.
Longer-term, we're exploring persisting card state to IndexedDB so that even dismissed or archived cards remain linkable, and we're adding card-to-card linking so orchestrator notes can reference related work items with automatic deep links.