Integration Architecture
"HEAT integrates without intrusion. A sidecar, not a replacement."
HEAT uses a Sidecar Architecture to work alongside your existing project management tools without modifying them or requiring API access. Like a cormorant diving alongside a fishing fleet, HEAT observes activity without disrupting the existing workflow.
Architecture Layers
Layer 1: Existing Systems (Read-Only)
HEAT never writes to your PM system. It only reads public metadata visible in the DOM.
What HEAT Reads
| Data Point | Source | Example |
|---|---|---|
| Task ID | DOM element (data attribute or text) | PROJ-1234 |
| Task Title | DOM element (h1, title, aria-label) | "Fix payment gateway timeout" |
| Task Status | DOM class or status badge | "In Progress" |
| Assignee | Current user session | [email protected] |
What HEAT Does NOT Read
❌ Task descriptions ❌ Comments or activity logs ❌ Attachments ❌ Watchers or subscribers ❌ Custom fields (unless explicitly configured)
Integration Examples
Jira Cloud
// HEAT Browser Extension reads from DOM
const taskId = document.querySelector('[data-testid="issue.views.issue-base.foundation.breadcrumbs.current-issue.item"]')?.textContent;
const taskTitle = document.querySelector('h1[data-testid="issue.views.issue-base.foundation.summary.heading"]')?.textContent;
// Example detected values
// taskId: "HEAT-42"
// taskTitle: "Implement pain streak algorithm"Azure DevOps
// HEAT reads from Azure DevOps DOM
const taskId = document.querySelector('.work-item-form-id')?.textContent;
const taskTitle = document.querySelector('.work-item-form-title input')?.value;
// Example detected values
// taskId: "12345"
// taskTitle: "Optimize SQL query performance"GitHub Issues
// HEAT reads from GitHub Issues
const taskId = document.querySelector('.gh-header-number')?.textContent;
const taskTitle = document.querySelector('.js-issue-title')?.textContent;
// Example detected values
// taskId: "#123"
// taskTitle: "Bug: Session timeout on checkout"Why Read-Only Matters
Resilience:
- If HEAT breaks, your PM system continues unchanged
- No risk of corrupting task data
- No dependency on API keys or webhooks
Privacy:
- Developers control what's tagged (opt-in, not surveillance)
- No data scraped without explicit user action
- PM admins don't need to grant access
Simplicity:
- No OAuth flows required
- No webhook configurations
- Works with any PM system (even custom tools)
Layer 2: HEAT Collector (Browser Extension)
The HEAT Collector is a lightweight browser extension that:
- Detects active task context
- Provides tagging UI
- Caches tasks locally for offline resilience
- Syncs tags to HEAT API
Component Architecture
┌─────────────────────────────────────────────────────────────┐
│ HEAT Browser Extension │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐│
│ │ DOM Observer │ │ Tagging UI │ │ Local Cache ││
│ │ │ │ │ │ ││
│ │ • Detects task │ │ • Tag picker │ │ • IndexedDB ││
│ │ • Reads ID │ │ • Intensity │ │ • 30-day TTL ││
│ │ • Reads title │ │ • Quick save │ │ • Offline mode ││
│ └────────────────┘ └────────────────┘ └────────────────┘│
│ │ │ │ │
│ └───────────────────┴───────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Sync Controller │ │
│ │ • Queue tags │ │
│ │ • Batch upload │ │
│ │ • Retry logic │ │
│ └─────────┬─────────┘ │
└──────────────────────────────┼───────────────────────────────┘
│
▼
HEAT API (HTTPS)DOM Observer (Context Detection)
Automatically detects which task you're working on:
// HEAT Extension: DOM Observer
class TaskContextDetector {
private config: SelectorConfig;
private currentTask: TaskContext | null = null;
constructor(platform: 'jira' | 'ado' | 'github') {
this.config = getPlatformSelectors(platform);
}
observe(): void {
// Watch for URL changes (SPA navigation)
window.addEventListener('popstate', () => this.detectContext());
// Watch for DOM updates (task switches)
const observer = new MutationObserver(() => this.detectContext());
observer.observe(document.body, { childList: true, subtree: true });
// Initial detection
this.detectContext();
}
private detectContext(): void {
const taskId = this.extractTaskId();
const taskTitle = this.extractTaskTitle();
if (taskId && taskTitle) {
this.currentTask = { taskId, taskTitle, timestamp: Date.now() };
this.notifyUI(this.currentTask);
} else {
this.currentTask = null;
}
}
private extractTaskId(): string | null {
const element = document.querySelector(this.config.taskIdSelector);
return element?.textContent?.trim() || null;
}
private extractTaskTitle(): string | null {
const element = document.querySelector(this.config.taskTitleSelector);
return element?.textContent?.trim() || element?.value?.trim() || null;
}
getCurrentTask(): TaskContext | null {
return this.currentTask;
}
}
// Platform-specific selectors
function getPlatformSelectors(platform: string): SelectorConfig {
const configs = {
jira: {
taskIdSelector: '[data-testid="issue.views.issue-base.foundation.breadcrumbs.current-issue.item"]',
taskTitleSelector: 'h1[data-testid="issue.views.issue-base.foundation.summary.heading"]'
},
ado: {
taskIdSelector: '.work-item-form-id',
taskTitleSelector: '.work-item-form-title input'
},
github: {
taskIdSelector: '.gh-header-number',
taskTitleSelector: '.js-issue-title'
}
};
return configs[platform];
}Tagging UI (30-Second Experience)
Minimal-friction tagging interface:
// HEAT Extension: Tagging UI Component
class TaggingUI {
private taskContext: TaskContext;
private tagHistory: Tag[] = [];
render(): HTMLElement {
return `
<div class="heat-tag-panel">
<div class="heat-task-context">
<strong>${this.taskContext.taskId}</strong>
<span>${this.truncate(this.taskContext.taskTitle, 40)}</span>
</div>
<div class="heat-tag-selector">
<label>Work Type:</label>
<select id="heat-work-type">
<option value="Feature">Feature</option>
<option value="Bug">Bug</option>
<option value="Blocker">Blocker</option>
<option value="Support">Support</option>
<option value="Config">Config</option>
<option value="Research">Research</option>
</select>
</div>
<div class="heat-intensity-selector">
<label>Intensity:</label>
<div class="heat-intensity-buttons">
${[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `
<button class="heat-intensity-btn" data-intensity="${i}">${i}</button>
`).join('')}
</div>
<div class="heat-intensity-helper">
<span class="x1-3">🟦 Routine</span>
<span class="x4-6">🟨 Focused</span>
<span class="x7-10">🟥 Intense</span>
</div>
</div>
<div class="heat-quick-save">
<button id="heat-save-tag" class="primary">Save Tag</button>
<button id="heat-save-continue" class="secondary">Save & Continue</button>
</div>
<div class="heat-recent-tags">
<label>Recent tags:</label>
${this.renderRecentTags()}
</div>
</div>
`;
}
private renderRecentTags(): string {
// Show last 3 tags for quick re-use
return this.tagHistory.slice(-3).map(tag => `
<button class="heat-recent-tag-btn" data-tag="${JSON.stringify(tag)}">
${tag.workType} × ${tag.intensity}
</button>
`).join('');
}
async saveTag(tag: Tag): Promise<void> {
// Save to local cache first (instant feedback)
await this.cacheTag(tag);
// Queue for sync to API (background)
await this.queueForSync(tag);
// Update UI
this.showSuccessFeedback();
}
}Local Cache (Offline Resilience)
Tasks cached locally for 30 days:
// HEAT Extension: Local Cache (IndexedDB)
class HeatCache {
private db: IDBDatabase;
async cacheTask(task: TaskContext): Promise<void> {
const tx = this.db.transaction('tasks', 'readwrite');
const store = tx.objectStore('tasks');
await store.put({
taskId: task.taskId,
taskTitle: task.taskTitle,
lastSeen: Date.now(),
expiresAt: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days
});
}
async getCachedTask(taskId: string): Promise<TaskContext | null> {
const tx = this.db.transaction('tasks', 'readonly');
const store = tx.objectStore('tasks');
const result = await store.get(taskId);
if (!result || result.expiresAt < Date.now()) {
return null; // Expired
}
return {
taskId: result.taskId,
taskTitle: result.taskTitle,
timestamp: result.lastSeen
};
}
async cacheTags(tags: Tag[]): Promise<void> {
const tx = this.db.transaction('tags', 'readwrite');
const store = tx.objectStore('tags');
for (const tag of tags) {
await store.put({
...tag,
synced: false,
createdAt: Date.now()
});
}
}
async getUnsyncedTags(): Promise<Tag[]> {
const tx = this.db.transaction('tags', 'readonly');
const store = tx.objectStore('tags');
const index = store.index('synced');
return await index.getAll(false); // Get all unsynced tags
}
}Sync Controller (Background Upload)
Batches tags and syncs to API:
// HEAT Extension: Sync Controller
class SyncController {
private syncInterval: number = 5 * 60 * 1000; // 5 minutes
private cache: HeatCache;
private api: HeatAPI;
start(): void {
setInterval(() => this.sync(), this.syncInterval);
// Also sync on visibility change (tab focus)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.sync();
}
});
}
async sync(): Promise<void> {
const unsyncedTags = await this.cache.getUnsyncedTags();
if (unsyncedTags.length === 0) {
return; // Nothing to sync
}
try {
// Batch upload (max 100 tags per request)
const batches = this.chunk(unsyncedTags, 100);
for (const batch of batches) {
await this.api.uploadTags(batch);
await this.markAsSynced(batch);
}
console.log(`[HEAT] Synced ${unsyncedTags.length} tags`);
} catch (error) {
console.error('[HEAT] Sync failed, will retry:', error);
// Tags remain in cache, will retry next interval
}
}
private async markAsSynced(tags: Tag[]): Promise<void> {
const tx = this.cache.db.transaction('tags', 'readwrite');
const store = tx.objectStore('tags');
for (const tag of tags) {
const record = await store.get(tag.id);
if (record) {
record.synced = true;
record.syncedAt = Date.now();
await store.put(record);
}
}
}
private chunk<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
}Layer 3: HEAT API (Processing Engine)
The API layer ingests tags, calculates patterns, and serves dashboards.
API Endpoints
// HEAT API Routes
const routes = {
// Tag ingestion
'POST /api/v1/tags': uploadTags,
'GET /api/v1/tags/:userId': getUserTags,
// Dashboard data
'GET /api/v1/dashboard/developer/:userId': getDeveloperView,
'GET /api/v1/dashboard/manager/:teamId': getManagerView,
'GET /api/v1/dashboard/analytics/:teamId': getTagAnalytics,
// Alerts
'GET /api/v1/alerts/:userId': getUserAlerts,
'GET /api/v1/alerts/team/:teamId': getTeamAlerts,
// Configuration
'GET /api/v1/config/thresholds': getThresholds,
'PUT /api/v1/config/thresholds': updateThresholds
};Tag Ingestion
// HEAT API: Tag ingestion endpoint
async function uploadTags(req: Request): Promise<Response> {
const { tags, userId } = await req.json();
// Validate tags
const validTags = tags.filter(tag => validateTag(tag));
if (validTags.length === 0) {
return new Response(JSON.stringify({ error: 'No valid tags' }), { status: 400 });
}
// Store in database
await db.tags.insertMany(
validTags.map(tag => ({
...tag,
userId,
ingestedAt: Date.now(),
processed: false
}))
);
// Trigger async processing (streak detection, aggregation)
await queueProcessing(userId, validTags);
return new Response(JSON.stringify({
accepted: validTags.length,
rejected: tags.length - validTags.length
}), { status: 200 });
}
function validateTag(tag: Tag): boolean {
const validWorkTypes = ['Feature', 'Bug', 'Blocker', 'Support', 'Config', 'Research'];
const validIntensity = tag.intensity >= 1 && tag.intensity <= 10;
const hasTaskId = !!tag.taskId;
return validWorkTypes.includes(tag.workType) && validIntensity && hasTaskId;
}Streak Detection Engine
// HEAT API: Pain Streak Detection
class StreakEngine {
async detectStreaks(userId: string, newTags: Tag[]): Promise<Streak[]> {
const streaks: Streak[] = [];
for (const tag of newTags) {
const streak = await this.checkStreak(userId, tag);
if (streak) {
streaks.push(streak);
// Alert if streak >= 3 days
if (streak.count >= 3) {
await this.createAlert(userId, streak);
}
}
}
return streaks;
}
private async checkStreak(userId: string, currentTag: Tag): Promise<Streak | null> {
// Get yesterday's tags for same task
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const yesterdayTags = await db.tags.find({
userId,
taskId: currentTag.taskId,
workType: currentTag.workType,
createdAt: {
$gte: yesterday,
$lt: new Date()
}
});
if (yesterdayTags.length === 0) {
// No streak, reset to 1
return { taskId: currentTag.taskId, workType: currentTag.workType, count: 1 };
}
// Check if we already have a streak record
const existingStreak = await db.streaks.findOne({
userId,
taskId: currentTag.taskId,
workType: currentTag.workType,
active: true
});
if (existingStreak) {
// Increment existing streak
existingStreak.count += 1;
existingStreak.lastSeen = Date.now();
await db.streaks.update(existingStreak);
return existingStreak;
} else {
// Create new streak (day 2)
const newStreak = {
userId,
taskId: currentTag.taskId,
workType: currentTag.workType,
count: 2, // Day 2 of streak
startedAt: yesterday.getTime(),
lastSeen: Date.now(),
active: true
};
await db.streaks.insert(newStreak);
return newStreak;
}
}
private async createAlert(userId: string, streak: Streak): Promise<void> {
await db.alerts.insert({
userId,
type: 'pain_streak',
severity: streak.count >= 5 ? 'critical' : 'warning',
message: `🔥 Pain Streak: ${streak.count} days on ${streak.taskId} (${streak.workType})`,
taskId: streak.taskId,
streakCount: streak.count,
createdAt: Date.now(),
dismissed: false
});
}
}Intensity Aggregation
// HEAT API: Intensity Aggregation
class IntensityAggregator {
async calculateDailyIntensity(userId: string, date: Date): Promise<DailyIntensity> {
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
const tags = await db.tags.find({
userId,
createdAt: {
$gte: startOfDay,
$lte: endOfDay
}
});
const totalIntensity = tags.reduce((sum, tag) => sum + tag.intensity, 0);
const color = this.getHeatmapColor(totalIntensity);
return {
date: date.toISOString(),
totalIntensity,
color,
tagCount: tags.length,
breakdown: this.calculateBreakdown(tags)
};
}
private getHeatmapColor(intensity: number): string {
if (intensity < 5) return 'cool'; // 🟦 Blue
if (intensity < 15) return 'normal'; // 🟩 Green
if (intensity < 30) return 'warm'; // 🟨 Amber
return 'critical'; // 🟥 Red
}
private calculateBreakdown(tags: Tag[]): Record<string, number> {
const breakdown: Record<string, number> = {};
for (const tag of tags) {
breakdown[tag.workType] = (breakdown[tag.workType] || 0) + tag.intensity;
}
return breakdown;
}
}Layer 4: HEAT Dashboard (Visualization)
The dashboard presents three views from the same underlying data.
Dashboard Architecture
// HEAT Dashboard: Data flow
class DashboardController {
async loadDeveloperView(userId: string, dateRange: DateRange): Promise<DeveloperView> {
// Fetch daily intensities
const dailyData = await api.get(`/dashboard/developer/${userId}`, { dateRange });
// Fetch active streaks
const streaks = await api.get(`/alerts/${userId}`, { type: 'pain_streak' });
// Fetch tag distribution
const tagDistribution = await api.get(`/tags/${userId}/distribution`, { dateRange });
return {
heatmap: this.buildHeatmap(dailyData),
streaks: this.buildStreakCards(streaks),
distribution: this.buildDistributionChart(tagDistribution)
};
}
async loadManagerView(teamId: string, dateRange: DateRange): Promise<ManagerView> {
// Fetch team heatmap
const teamData = await api.get(`/dashboard/manager/${teamId}`, { dateRange });
// Fetch team alerts
const alerts = await api.get(`/alerts/team/${teamId}`);
// Fetch bus factor analysis
const busFactor = await api.get(`/analytics/bus-factor/${teamId}`, { dateRange });
return {
teamHeatmap: this.buildTeamHeatmap(teamData),
alerts: this.buildAlertPanel(alerts),
busFactor: this.buildBusFactorMap(busFactor)
};
}
async loadTagAnalytics(teamId: string, dateRange: DateRange): Promise<TagAnalytics> {
// Fetch global tag aggregation
const tagData = await api.get(`/dashboard/analytics/${teamId}`, { dateRange });
return {
globalTrends: this.buildTrendChart(tagData),
drillDown: this.buildDrillDownTable(tagData),
comparisons: this.buildComparisonView(tagData)
};
}
}Data Flow Example (End-to-End)
Let's trace a single tag from creation to dashboard:
Step 1: Developer Tags Work
Developer: Alice
Task: HEAT-42 "Implement pain streak algorithm"
Action: Clicks "Save Tag" in browser extension
Extension captures:
{
taskId: "HEAT-42",
taskTitle: "Implement pain streak algorithm",
workType: "Feature",
intensity: 5,
userId: "[email protected]",
timestamp: 1701360000000
}Step 2: Local Cache + Sync
Browser Extension:
1. Writes to IndexedDB (instant)
2. Shows success feedback to Alice
3. Queues for sync to API
Background sync (5 min later):
1. Detects 1 unsynced tag
2. POSTs to /api/v1/tags
3. Marks as synced in IndexedDBStep 3: API Processing
API receives tag:
1. Validates (workType valid? intensity 1-10?)
2. Stores in database
3. Triggers async processing:
- Streak detection (is this day 2+ on same task?)
- Intensity aggregation (what's Alice's total today?)
- Alert generation (any warnings?)Step 4: Streak Detection
Streak Engine checks:
- Did Alice tag HEAT-42 yesterday? → YES (Feature, x4)
- Does she have an active streak? → YES (count: 2)
- Increment: count = 3
Alert created:
{
type: "pain_streak",
severity: "warning",
message: "🔥 Streak: 3 days on HEAT-42 (Feature)",
userId: "[email protected]"
}Step 5: Dashboard Updates
Alice's Developer View:
- Heatmap: Today shows 🟨 Amber (15 intensity total)
- Streaks: "🔥 Streak: 3 days on HEAT-42"
- Tag Distribution: Feature: 60%, Bug: 30%, Support: 10%
Manager's View:
- Team Heatmap: Alice row shows 🟨 Amber
- Alerts Panel: "Alice has 3-day streak on HEAT-42"
- Action suggestion: "Consider pairing or check-in"
Tag Analytics View:
- Global Feature intensity: +5 from Alice
- Team trend: Feature work stable
- No systemic spikes detectedDeployment Architecture
HEAT can be deployed in multiple configurations:
Option A: SaaS (Hosted)
┌─────────────────────────────────────────────────────────────┐
│ Browser Extension (All users) │
│ └─ Points to: https://api.heat.yourdomain.com │
├─────────────────────────────────────────────────────────────┤
│ Cloudflare Workers (API Layer) │
│ └─ Handles tag ingestion, processing, dashboard API │
├─────────────────────────────────────────────────────────────┤
│ D1 Database (SQLite at edge) │
│ └─ Stores tags, streaks, alerts │
├─────────────────────────────────────────────────────────────┤
│ Cloudflare Pages (Dashboard UI) │
│ └─ Serves https://heat.yourdomain.com │
└─────────────────────────────────────────────────────────────┘Advantages:
- Zero infrastructure management
- Global edge deployment (low latency)
- Scales automatically
- Cost-effective (<$50/month for small teams)
Option B: Self-Hosted (On-Premises)
┌─────────────────────────────────────────────────────────────┐
│ Browser Extension (All users) │
│ └─ Points to: https://heat.internal.company.com │
├─────────────────────────────────────────────────────────────┤
│ Docker Container (Node.js API) │
│ └─ Runs on internal Kubernetes cluster │
├─────────────────────────────────────────────────────────────┤
│ PostgreSQL Database │
│ └─ Stores tags, streaks, alerts │
├─────────────────────────────────────────────────────────────┤
│ Static Site (Dashboard UI) │
│ └─ Served via internal CDN or nginx │
└─────────────────────────────────────────────────────────────┘Advantages:
- Full data control (never leaves network)
- Compliance-friendly (GDPR, HIPAA, SOC2)
- Customizable (modify source code)
- No external dependencies
Security & Privacy
Data Minimization
HEAT only collects work metadata, never actual work content:
| Collected | NOT Collected |
|---|---|
| Task ID (PROJ-123) | Task description |
| Task Title (visible in PM tool) | Comments or activity logs |
| Work type tag (Feature, Bug, etc.) | Code commits or diffs |
| Intensity (x1-x10) | Time spent on task |
| Timestamp (when tagged) | Surveillance metrics |
User Control
Opt-in tagging: Developers choose what to tag (not automatic) Local-first: Tags cached locally before sync Visibility: Developers see their own data first No surveillance: HEAT measures team health, not individual productivity
Data Encryption
// HEAT API: Data at rest encryption
const encryptedTag = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: generateIV() },
encryptionKey,
JSON.stringify(tag)
);
// HEAT Extension: HTTPS-only communication
const response = await fetch('https://api.heat.yourdomain.com/tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`
},
body: JSON.stringify({ tags: encryptedTags })
});Performance Characteristics
Browser Extension
| Metric | Target | Actual (measured) |
|---|---|---|
| DOM detection latency | <100ms | ~50ms |
| UI render time | <50ms | ~30ms |
| Tag save (local) | <10ms | ~5ms (IndexedDB write) |
| Memory footprint | <10MB | ~3-5MB |
| CPU usage | <1% idle | ~0.5% average |
API Layer
| Metric | Target | Actual (measured) |
|---|---|---|
| Tag ingestion (single) | <200ms | ~100ms (p95) |
| Tag ingestion (batch 100) | <500ms | ~300ms (p95) |
| Dashboard load (Developer View) | <1s | ~600ms |
| Dashboard load (Manager View) | <2s | ~1.2s |
| Streak detection | <50ms | ~30ms per tag |
Database
| Metric | Target | Notes |
|---|---|---|
| Tags per user per day | ~10-20 | Based on typical usage |
| Storage per tag | ~200 bytes | JSON compressed |
| Retention period | 90 days default | Configurable |
| Total storage (100 users) | ~36MB/year | Highly efficient |
Scaling Considerations
Small Teams (1-20 developers)
Architecture: Cloudflare Workers + D1 Cost: <$10/month Complexity: Low (serverless, zero ops)
Mid-Size (20-100 developers)
Architecture: Cloudflare Workers + D1 or Self-hosted Cost: $20-50/month (SaaS) or compute costs (self-hosted) Complexity: Low-medium
Large (100-500 developers)
Architecture: Self-hosted Kubernetes + PostgreSQL Cost: Compute + storage costs Complexity: Medium (requires DevOps) Considerations:
- Sharding by team or geography
- Read replicas for dashboards
- Background job queue for processing
Enterprise (500+ developers)
Architecture: Multi-region deployment Cost: Infrastructure + dedicated support Complexity: High Considerations:
- Regional data residency (GDPR)
- SSO integration (SAML, OIDC)
- Custom SLAs
- Dedicated instances
Integration Patterns
Pattern 1: Browser Extension Only
Best for: Quick pilots, individual adoption Setup: Install extension, configure API endpoint Time to value: 5 minutes
Pattern 2: Extension + Dashboard
Best for: Team rollout, manager visibility Setup: Deploy dashboard, configure auth Time to value: 1 hour
Pattern 3: Extension + Dashboard + Alerts
Best for: Production use, proactive intervention Setup: Configure alert rules, integrate Slack/email Time to value: 2-4 hours
Pattern 4: Full Platform Integration
Best for: Enterprise deployment, custom workflows Setup: SSO, custom reports, API integrations Time to value: 1-2 weeks