refactor: Mark legacy SSH detection as temporary migration scaffolding

Addresses user concern about technical debt: detection code exists only
to handle migration from SSH-in-container to proxy architecture, not to
serve functional purpose of the application.

Changes:
- Add PULSE_LEGACY_DETECTION env var to disable detection without redeployment
- Add explicit removal criteria: v5.0 or <1% detection rate for 30+ days
- Mark all detection code with "MIGRATION SCAFFOLDING" warnings
- Create MIGRATION_SCAFFOLDING.md to track temporary code across codebase
- Document removal instructions for when migration period ends

Backend:
- internal/api/router.go: detectLegacySSH() checks env var and has removal plan
- internal/api/types.go: HealthResponse fields documented as temporary

Frontend:
- src/components/LegacySSHBanner.tsx: Component marked with removal criteria
- src/App.tsx: Banner integration (will be removed with component)

This approach balances user safety during migration (auto-detection catches
rushed admins who skip changelogs) with long-term code cleanliness (explicit
removal plan prevents indefinite technical debt).
This commit is contained in:
rcourtman 2025-10-13 14:54:52 +00:00
parent 6d56917cd9
commit 21714fdf7a
4 changed files with 185 additions and 0 deletions

67
MIGRATION_SCAFFOLDING.md Normal file
View file

@ -0,0 +1,67 @@
# Migration Scaffolding Tracker
This document tracks temporary code added to handle migration paths between versions. All code listed here should be removed according to the specified criteria.
**Purpose**: These features exist solely to assist users in migrating from old patterns to new ones. They serve no functional purpose beyond migration assistance and represent technical debt that should be cleaned up once the migration period is complete.
---
## Active Migration Code
### Legacy SSH Detection Banner (Added: v4.23.0)
**Why it exists**: Users who set up temperature monitoring before v4.23.0 used SSH keys directly in the container. The new secure architecture uses `pulse-sensor-proxy` on the host with Unix socket communication. Users with the old setup need to remove and re-add their nodes to upgrade.
**Files involved**:
- Backend: `/opt/pulse/internal/api/router.go` - `detectLegacySSH()` function
- Backend: `/opt/pulse/internal/api/types.go` - `HealthResponse.LegacySSHDetected` fields
- Frontend: `/opt/pulse/frontend-modern/src/components/LegacySSHBanner.tsx`
- Frontend: `/opt/pulse/frontend-modern/src/App.tsx` - Banner integration
**Removal criteria** (ANY of these):
- Version reaches v5.0 or later
- Telemetry shows <1% detection rate for 30+ consecutive days
- 6 months after v4.23.0 release (whichever comes first)
**How to remove**:
1. Delete `LegacySSHBanner.tsx` component
2. Remove import and usage from `App.tsx`
3. Delete `detectLegacySSH()` function from `router.go`
4. Remove `LegacySSHDetected`, `RecommendProxyUpgrade`, `ProxyInstallScriptAvailable` from `types.go` HealthResponse
5. Remove banner logic from `handleHealth()` in `router.go`
6. Delete this entry from this document
**Manual override**: Set `PULSE_LEGACY_DETECTION=false` environment variable to disable detection without code changes.
**Telemetry notes**: Currently no telemetry implemented. Consider adding metrics to track:
- How often the banner is shown
- How many users still have legacy SSH setup
- When detection rate drops below threshold
---
## Removed Migration Code
(None yet - this document was created v4.23.0)
---
## Guidelines for Adding New Migration Code
When adding new migration scaffolding:
1. **Mark it clearly** with `⚠️ MIGRATION SCAFFOLDING - TEMPORARY CODE` comments
2. **Add it to this document** with:
- Why it exists
- Files involved
- Removal criteria
- Removal instructions
3. **Set explicit removal criteria**:
- Version number target
- Time-based target (e.g., "6 months after release")
- Data-driven target (e.g., "when <1% users affected")
4. **Add a kill switch**: Environment variable or feature flag to disable without redeployment
5. **Consider telemetry**: If removal depends on data, instrument the code to collect that data
6. **Create a removal task**: Add to issue tracker with assigned owner and date
**Remember**: Migration code is technical debt. Make it easy to find and remove later.

View file

@ -34,6 +34,7 @@ import { SettingsAPI } from './api/settings';
import { eventBus } from './stores/events';
import { updateStore } from './stores/updates';
import { UpdateBanner } from './components/UpdateBanner';
import { LegacySSHBanner } from './components/LegacySSHBanner';
import { DemoBanner } from './components/DemoBanner';
import { createTooltipSystem } from './components/shared/Tooltip';
import type { State } from '@/types/api';
@ -548,6 +549,7 @@ function App() {
<SecurityWarning />
<DemoBanner />
<UpdateBanner />
<LegacySSHBanner />
<div class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 font-sans py-4 sm:py-6">
<AppLayout
connected={connected}

View file

@ -0,0 +1,102 @@
import { Show, createSignal, createEffect, onMount } from 'solid-js';
/**
* MIGRATION SCAFFOLDING - TEMPORARY COMPONENT
*
* This banner exists only to handle migration from legacy SSH-in-container
* to the secure pulse-sensor-proxy architecture introduced in v4.23.0.
*
* REMOVAL CRITERIA: Remove after v5.0 or when backend telemetry shows <1% detection rate
* for 30+ days. This component serves no functional purpose beyond migration assistance.
*
* Can be disabled by setting PULSE_LEGACY_DETECTION=false on backend.
*/
export function LegacySSHBanner() {
const [isVisible, setIsVisible] = createSignal(false);
const [isDismissed, setIsDismissed] = createSignal(false);
// Check if previously dismissed
onMount(() => {
const dismissed = localStorage.getItem('legacySSHBannerDismissed');
if (dismissed === 'true') {
setIsDismissed(true);
setIsVisible(false);
}
});
// Check health endpoint for legacy SSH detection
createEffect(async () => {
if (isDismissed()) return;
try {
const response = await fetch('/api/health');
const health = await response.json();
if (health.legacySSHDetected && health.recommendProxyUpgrade) {
setIsVisible(true);
}
} catch (error) {
// Silently fail - health check failures shouldn't break the UI
}
});
const handleDismiss = () => {
setIsDismissed(true);
setIsVisible(false);
// Store dismissal in localStorage so it persists
localStorage.setItem('legacySSHBannerDismissed', 'true');
};
return (
<Show when={isVisible()}>
<div class="bg-orange-50 dark:bg-orange-900/20 border-b border-orange-200 dark:border-orange-800 text-orange-800 dark:text-orange-200 relative animate-slideDown">
<div class="px-4 py-2">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
{/* Warning icon */}
<svg
class="w-5 h-5 flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<div class="text-sm">
<span class="font-medium">Legacy temperature monitoring detected.</span>
{' '}
<span>Remove and re-add your nodes to upgrade to the secure proxy architecture.</span>
</div>
</div>
{/* Dismiss button */}
<button
onClick={handleDismiss}
class="p-1 hover:bg-orange-100 dark:hover:bg-orange-800/30 rounded transition-colors flex-shrink-0"
title="Dismiss"
>
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
</div>
</Show>
);
}

View file

@ -1129,7 +1129,21 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
// detectLegacySSH checks if Pulse is using legacy SSH for temperature monitoring
//
// ⚠️ MIGRATION SCAFFOLDING - TEMPORARY CODE
// This detection exists only to handle migration from legacy SSH-in-container
// to the secure pulse-sensor-proxy architecture introduced in v4.23.0.
//
// REMOVAL CRITERIA: Remove after v5.0 or when banner telemetry shows <1% fire rate
// for 30+ days. This code serves no functional purpose beyond migration assistance.
//
// Can be disabled via environment variable: PULSE_LEGACY_DETECTION=false
func (r *Router) detectLegacySSH() (legacyDetected, recommendProxy bool) {
// Check if detection is disabled via environment variable
if os.Getenv("PULSE_LEGACY_DETECTION") == "false" {
return false, false
}
// Check if running in a container
inContainer := false
if _, err := os.Stat("/.dockerenv"); err == nil {