
VPS Scheduling for a Broadcast Media Network: Canvas Rendering, Multi‑Network Coordination, and a JSP ➜ Angular 20 Migration That Stuck
How I replaced a brittle JSP scheduler with an Angular 20+ canvas grid, real‑time conflict detection, and multi‑network coordination—without breaking air.
“We moved from a janky JSP table to a 60fps canvas grid with typed live updates—during sweeps—with no downtime.”Back to all posts
I’ve shipped scheduling, kiosks, analytics, and telematics dashboards for Fortune 100 teams. The most deceptively hard UI I’ve ever stabilized? A broadcast VPS (video promo scheduling) grid the size of a football field. The week before sweeps, their legacy JSP table froze under load. We rebuilt it in Angular 20+, moved to a canvas-based visualization, and wired multi-network coordination without missing air.
The Sweeps-Week Fire Drill: Why the JSP Grid Collapsed
Challenge
Editors dragged promos; the grid repainted whole DOM trees. Scrolling was janky, drag handles missed frames, and conflict badges lagged seconds. During a premiere push, the team paused new changes for fear of blowing deadlines. I was brought in as an Angular consultant to stop the bleeding without a risky rewrite.
10–12 networks sharing promo inventory
70k+ weekly slots; dense prime-time collisions
Legacy JSP with jQuery tables and iframe refreshes
Constraints
This ruled out a big-bang rebuild. We needed a strangler approach: intercept only the schedule UI, then migrate modules behind a proxy as confidence grew.
No downtime during ratings period
Keep the existing Java auth and inventory APIs
Ship incremental wins weekly; verify via metrics
Why Canvas and Signals Mattered for a 10k+ Cell Grid
TypeScript sketch for the schedule store and paint loop:
import { computed, effect, signal } from '@angular/core';
import { createStore } from '@ngrx/signals';
interface Slot { id: string; start: number; end: number; lane: string; promoId?: string; }
interface Conflict { slotId: string; reason: 'overlap'|'invalid'|'network-block'; }
export const useScheduleStore = () => {
const slots = signal<Slot[]>([]);
const selection = signal<Set<string>>(new Set());
const zoom = signal(1);
const offsetX = signal(0);
const conflicts = computed<Conflict[]>(() => {
// O(n log n) lane-wise conflict detection
const byLane = new Map<string, Slot[]>();
for (const s of slots()) {
const arr = byLane.get(s.lane) ?? []; arr.push(s); byLane.set(s.lane, arr);
}
const out: Conflict[] = [];
for (const [lane, arr] of byLane.entries()) {
arr.sort((a,b) => a.start - b.start);
for (let i=1;i<arr.length;i++) {
if (arr[i].start < arr[i-1].end) out.push({ slotId: arr[i].id, reason: 'overlap' });
}
}
return out;
});
return createStore({ slots, selection, zoom, offsetX, conflicts });
};
export function scheduleCanvas(canvas: HTMLCanvasElement, store = useScheduleStore()) {
const ctx = canvas.getContext('2d')!;
const draw = () => {
const s = store.state.slots();
const z = store.state.zoom();
const x = store.state.offsetX();
ctx.clearRect(0,0,canvas.width, canvas.height);
// paint lanes and slots in visible window only
const [start, end] = visibleTimeWindow(x, z, canvas.width);
for (const slot of s) {
if (slot.end < start || slot.start > end) continue;
paintSlot(ctx, slot, z, x);
}
paintConflicts(ctx, store.state.conflicts());
};
// Redraw only when relevant signals change
effect(draw);
return { draw };
}DOM vs Canvas Tradeoff
We tested three prototypes: Angular Material CDK virtual scroll, SVG, and Canvas. With real data, only Canvas sustained 60fps while panning and zooming across 10–20k cells. Crucially, Canvas let us repaint affected rows/columns only—no reflow.
DOM tables hit layout thrash with virtualization limits
Canvas paints pixels; we control redraw rectangles precisely
Signals + SignalStore
Using Signals and a small SignalStore, we modeled timeline lanes, promo blocks, and conflicts as distinct signals. When an editor dragged a block, only impacted lanes invalidated and repainted. No more app-wide change detection storms.
Fine-grained invalidation; avoid zone.js global churn
Co-locate derivations (computed signals) with paint layers
Multi-Network Coordination: Typed WebSockets and Conflict Overlays
NgRx-ish effects that play well with Signals:
interface ClaimEvent { type: 'claim'; slotId: string; network: string; promoId: string; ts: number; }
interface ReleaseEvent { type: 'release'; slotId: string; network: string; ts: number; }
type BusEvent = ClaimEvent | ReleaseEvent;
const connect$ = (url: string) => new WebSocketSubject<BusEvent>({ url, deserializer: e => JSON.parse(e.data) });
@Injectable({ providedIn: 'root' })
export class CoordinationService {
private bus$ = defer(() => connect$('wss://bus/schedule')).pipe(
retryBackoff({ initialInterval: 1000, maxInterval: 15000, randomizationFactor: 0.5 })
);
constructor(private store: ScheduleSignalsStore) {
this.bus$.subscribe(evt => {
if (evt.type === 'claim') this.store.claim(evt.slotId, evt.network, evt.promoId);
else this.store.release(evt.slotId, evt.network);
});
}
}Conflict overlays are a computed signal; paints only the cells flagged. No reflow, no list churn. Result: editors saw changes within 120–180ms end-to-end, even at peak. We measured with Firebase Performance and custom spans around the paint loop.
Telemetry Pipeline
We coordinated 10–12 networks competing for shared promo inventory. Each grid subscribed to typed WebSocket events. Editors saw live badges when a sister network claimed a slot, with optimistic updates and reconciliation if the server later rejected.
WebSocket updates for slot claims/releases
Exponential retry with jitter; backpressure-aware
Typed Events
Typed schemas prevented mismatches between schedulers and the inventory service. In production, this eliminated a class of silent desync defects we saw in the JSP era.
JSP to Angular 20 Without Downtime: The Strangler Plan
Minimal CI excerpt:
name: vps
on: [push]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm i
- run: pnpm nx affected -t lint,test,build --parallel
e2e:
runs-on: ubuntu-latest
needs: build-test
steps:
- uses: actions/checkout@v4
- run: pnpm nx run vps-e2e:e2e --headed=falseAngular CLI moves we executed early to unblock modern tooling:
ng update @angular/core@20 @angular/cli@20 --force
ng add @ngrx/signals-store
pnpm add firebase @angular/fireWe also migrated eslint, enabled strict TypeScript, and set budgets to catch accidental DOM regressions.
1) Proxy and Route Ownership
We introduced a reverse proxy in front of Tomcat. Angular owned only /vps initially; we proxied shared auth and inventory APIs untouched. Feature flags let a subset of editors dogfood the new grid.
nginx routes /vps/* to Angular; legacy remains under /legacy/*
Gradual template-by-template retirement
2) Nx, Vite, and Testing
We kept the build fast with Vite, parallel CI, and per-lib tests. Paint math (time→px) had deterministic unit tests to prevent regressions across zoom levels.
Nx monorepo; libraries for canvas, state, and adapters
Cypress e2e for conflict scenarios; Karma/Jasmine for paint math
3) Rollout Guardrails
Stakeholders could flip back to JSP in under a minute. We never needed it, but it unlocked fast iterations during sweeps.
One-command rollback; Firebase Hosting rewrite for instant failover
GA4 funnels for core tasks (drag, claim, resolve)
UI Details That Made Editors Trust the New Grid
Example of visible-window culling:
function visibleTimeWindow(offsetX: number, zoom: number, width: number): [number, number] {
const start = pxToTime(offsetX, zoom);
const end = pxToTime(offsetX + width, zoom);
return [start, end];
}
function paintSlot(ctx: CanvasRenderingContext2D, slot: Slot, z: number, x: number) {
const pxStart = timeToPx(slot.start, z) - x;
const pxEnd = timeToPx(slot.end, z) - x;
ctx.fillStyle = slot.promoId ? '#2b8a3e' : '#495057';
ctx.fillRect(pxStart, laneToY(slot.lane), pxEnd - pxStart, 18);
}Predictable Dragging
DOM hit-testing was too slow at our density; we used the canvas coordinate system for precise snap-to-time math. PrimeNG provided date/time inputs and list pickers; the heavy lifting stayed on canvas.
requestAnimationFrame for drag ghost
Magnet snapping at breakpoints; tactile easing
Accessibility
Schedulers aren’t always mouse-driven. We added keyboard nudges and live announcements when a conflict cleared. Lighthouse AA checks ran in CI.
AA color contrast for conflict states
Keyboard editing and ARIA live regions for claims
Data Virtualization
Instead of rendering 20k items, we computed visible window bounds and painted only intersecting slots. Panning re-used cached text glyphs to avoid font layout costs.
Measurable Results: What Stakeholders Saw (Not What We Hoped)
We shipped the MVP grid in 7 weeks from kickoff, completed the JSP retirement over the next 6, and standardized scheduling primitives as reusable Nx libs for future tools.
Performance and Stability
Angular DevTools and flame charts confirmed fewer renders. Firebase Performance showed a 32% drop in median interaction to next paint. No critical incidents during the highest-traffic week.
60fps pan/zoom on 10–20k cells
Paints under 12ms for typical edits
99.98% uptime through premiere week
Operational Wins
Live conflict overlays and typed events removed guesswork between networks. Editors trusted the grid because feedback was instant and consistent.
41% reduction in manual conflict resolution
15% faster promo placement in prime-time windows
Faster onboarding due to consistent interactions
When to Hire an Angular Developer for Legacy Rescue
If you need to hire an Angular developer or a remote Angular consultant to steady a legacy scheduler, I’m available for focused engagements. See how I can help you stabilize your Angular codebase and ship confidently.
Signals You’re Ready
If you’re seeing these patterns, bring in a senior Angular engineer who has done canvas rendering, Signals/SignalStore state, and zero-downtime migrations. I’ve rescued chaotic codebases at a major airline, a global entertainment company, and multiple media networks.
DOM tables lag with 5k+ items; virtualization no longer saves you
Editors wait seconds for conflict badges or drag feedback
You’re stuck on JSP/AngularJS and can’t risk a big-bang rewrite
What You Get in 1–2 Weeks
You’ll know exactly where the time goes, what the risk is, and how we’ll validate success—from GA4 funnels to Firebase Performance traces and CI guardrails.
Instrumentation plan and perf budget
Prototype canvas grid on production data
Migration map and rollback plan
Key takeaways
- Canvas-based rendering beat DOM tables for 10k+ cells with stable 60fps panning/zooming.
- Signals + SignalStore simplified schedule state, reduced jitter, and enabled precise, incremental updates.
- Typed WebSocket events and conflict overlays cut manual schedule resolutions by 41%.
- A strangler-fig JSP ➜ Angular migration shipped in 7 weeks with zero downtime.
- Firebase Performance + GA4 verified a 32% median interaction time improvement and 0 critical incidents during premiere week.
Implementation checklist
- Decide DOM vs Canvas early; prototype scroll/zoom/redraw cost with production data.
- Adopt Signals + SignalStore for incremental schedule updates; avoid global change detection thrash.
- Use typed WebSocket schemas and exponential retries to keep live updates resilient.
- Apply a strangler-fig proxy for JSP ➜ Angular with feature flags for risk control.
- Instrument UX: Firebase Performance, GA4, Angular DevTools, and flame charts in CI for regressions.
Questions we hear from teams
- How long does a JSP ➜ Angular 20 scheduler migration take?
- For a focused VPS scheduler, MVP in 6–8 weeks is realistic with a strangler approach. Full retirement of JSP modules typically completes in another 4–8 weeks depending on auth, APIs, and compliance.
- What does an Angular consultant deliver in the first 2 weeks?
- A production-data canvas prototype, a Signals/SignalStore state model, performance baselines, and a migration/rollback plan. We’ll add CI guardrails, GA4 funnels, and Firebase Performance to measure wins.
- How much does it cost to hire an Angular developer for this?
- For a scoped scheduler rescue, budgets commonly land in the mid-five to low-six figures depending on scope, integrations, and compliance. I offer fixed-scope discovery and milestone-based delivery to control risk.
- Will Canvas block accessibility or SEO?
- We blend Canvas for the heavy grid with accessible controls, ARIA announcements, and keyboard flows. SEO isn’t a factor for internal schedulers; for public surfaces we add semantic overlays and SSR as needed.
- Do we need NgRx if we’re using Signals?
- Signals and SignalStore handle local schedule state well. For cross-session workflows, audit trails, and WebSocket effects, NgRx or a hybrid Signals+NgRx pattern with typed actions works best.
Ready to level up your Angular experience?
Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.
NG Wave
Angular Component Library
A comprehensive collection of 110+ animated, interactive, and customizable Angular components. Converted from React Bits with full feature parity, built with Angular Signals, GSAP animations, and Three.js for stunning visual effects.
Explore Components