Skip to content

Repository files navigation

local-jobs

A personal job runner for a Mac Mini that never sleeps.

Some work doesn't fit a serverless function or a web request. It runs for hours, drives a real browser, talks to a NAS over SMB, or needs to happen at 5am whether or not anyone is watching. This is where that work lives: one daemon that keeps about twenty jobs running on schedule, a record of every run, and a dashboard to watch it all from the sofa.

It is deliberately small. SQLite, no Docker, no queue, no cloud.

How it works

launchd ──keeps alive──▶ daemon ──runs──▶ workflow ──runs──▶ job
│ (a pipeline) (one step,
│ own process)
▼
SQLite ◀──reads── dashboard

The daemon is the only long-lived process. It wakes up on a schedule, decides what should run, and records what happened. Nothing else needs to be running for work to get done.

A workflow is a pipeline. Most real tasks are several steps that depend on each other: fetch the data, check it, transform it, publish it. A workflow describes those steps and the order they need, and independent steps run at the same time.

A job is one step, and it runs in its own process. If it hangs, it gets killed. If it crashes, it takes nothing else down with it. If it fails, it can retry without redoing the work it already finished.

The dashboard is a window, not a control panel. Jobs run the same whether it is open or closed.

What you get

Everything below applies to any job you add, so a new job is mostly just the interesting part.

It remembers what it already did. Jobs that chew through hundreds of items keep a per-item ledger, so a re-run skips finished work and picks up where it stopped. Interrupt a job halfway through 5,000 files and the next run continues from 2,501.

Steps check each other's work. Where one step hands data to the next, both sides declare what that data should look like. If a website changes its layout or an export drops a column, the pipeline stops at that boundary and tells you exactly what changed, rather than quietly writing bad data to disk.

Shared APIs get shared limits. Paid or rate-limited services are declared once, with a rate limit and a monthly spend cap. Every job that calls one goes through the same meter, so caps hold no matter how many jobs run at once, and a job that hits the ceiling stops politely instead of burning through your budget.

Failures find you. Runs that fail, time out, or leave items stuck send a push notification to your phone, with a summary of what was processed.

Nothing runs twice by accident. A workflow can only have one run in flight. Ask for a second and you get told, rather than quietly getting two.

You can run a small slice. Trigger a manual run limited to a handful of items to see what a change does before letting it loose on the whole library.

Everything is inspectable. Live logs stream as jobs run, every past run is kept with its output, and the dashboard shows what each step produced.

What it currently runs

Twenty-two workflows, all included in this repo as working examples.

Looking after a Plex library

WorkflowWhat it does
plex-renameRenames every file to Plex's canonical convention, so the library can rebuild itself from disk if the database is ever lost.
plex-library-guardWatches for silent data loss and sends one urgent alert if the library shrinks or a file disappears.
plex-language-fixWorks out each title's real original language and sets the right audio and subtitle tracks.
plex-profilesWrites a markdown profile for every film and show, with cast, ratings, and technical detail.
plex-space-saverReports where the disk space actually went, biggest first.
mount-keeperKeeps the NAS shares mounted, because macOS drops them and everything else depends on them.
missing-moviesFinds collections you own part of, and tells you which films are missing.
missing-tv-seasonsSpots complete seasons of shows you follow that you haven't got yet.
movie-recommendationsReads your library's taste and suggests films worth adding, monthly.
tv-recommendationsThe same for television.

Keeping a personal archive

WorkflowWhat it does
placesTurns your Google saved places into proper written profiles of each venue.
perfumesBuilds a profile for every fragrance you own, with notes, accords, and a written summary.
media-reviewsPulls your own book, film, TV, and album reviews out of your website's database into markdown.
listening-digestA monthly record of what you actually listened to.
workouts-syncSyncs your workout history and writes a six-month progress report per exercise.
projects-syncCatalogues your GitHub repos and writes a summary of what each project is.
vault-syncCopies all of the above into your second-brain folder, with names a human would choose.

Money and housekeeping

WorkflowWhat it does
stocks-syncTakes a daily read-only snapshot of your portfolio and alerts on a big gain.
stock-digestA weekly written summary of holdings, movers, and how diversified you actually are.
vercel-daily-redeployShips a production deploy each night, so a site can't quietly go stale.
claude-warmerKeeps a usage window warm, so the jobs that need it aren't cold when they run.
overrides-auditReminds you about settings you changed in the dashboard weeks ago and forgot to make permanent.

Your own jobs stay private by default. This repo is public, and anything you add is ignored by git unless you choose to publish it. Secrets live in .env.


Running it

Two services, both kept alive by launchd, both back after a reboot.

git clone <this repo>&&cd local-jobs
npm install
bash scripts/install-launchd.sh # the enginecd dashboard && npm install && npm run build &&cd ..
bash scripts/install-dashboard-launchd.sh # the dashboard
sudo pmset -a sleep 0 disablesleep 1 # schedules can't fire while asleep

The dashboard is at http://localhost:4788. The API stays on loopback.

To reach it from your phone, put it on a Tailscale tailnet with tailscale serve --bg 4788. Never use tailscale funnel for this, which would publish it to the internet.

Day to day:

scripts/safe-restart.sh # restart the daemon (refuses while a run is in flight)
tail -f data/daemon.out.log

Settings live in .env, documented in .env.example.

Adding a job

A job is a description of itself and a function that does the work.

constjob: JobDefinition={name: 'cleanup-temp',description: 'Deletes stale temp files',timeoutMs: 600_000,maxRetries: 3,asyncrun(ctx){ctx.log('starting');ctx.progress(50,'halfway');// throw to fail the run},};

Then say when it runs, in a workflow of one step:

constworkflow: WorkflowDefinition={name: 'cleanup-temp',description: 'Nightly temp-file cleanup',schedule: '0 4 * * *',jobs: [{job: 'cleanup-temp'}],};

Drop both in src/workflows/, restart with scripts/safe-restart.sh, and it appears in the dashboard. There is no registry to update.

Testing

npm test runs the whole suite against a scratch database, so it can never touch real data or real output. Add tests as you add behaviour.

Dashboard changes have two extra checks that drive a real browser, so they run by hand rather than in CI: dashboard/scripts/mobile-check.mjs for phone widths, and dashboard/scripts/visual-check.mjs for screenshots to look at.

Reading further

CLAUDE.md covers the architecture and conventions in full. Each workflow folder has its own CLAUDE.md describing how that particular one works and why.

About

Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - RyanMKrol/LocalJobs: Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes · GitHub
Skip to content

Repository files navigation

local-jobs

A personal job runner for a Mac Mini that never sleeps.

Some work doesn't fit a serverless function or a web request. It runs for hours, drives a real browser, talks to a NAS over SMB, or needs to happen at 5am whether or not anyone is watching. This is where that work lives: one daemon that keeps about twenty jobs running on schedule, a record of every run, and a dashboard to watch it all from the sofa.

It is deliberately small. SQLite, no Docker, no queue, no cloud.

How it works

launchd ──keeps alive──▶ daemon ──runs──▶ workflow ──runs──▶ job
│ (a pipeline) (one step,
│ own process)
▼
SQLite ◀──reads── dashboard

The daemon is the only long-lived process. It wakes up on a schedule, decides what should run, and records what happened. Nothing else needs to be running for work to get done.

A workflow is a pipeline. Most real tasks are several steps that depend on each other: fetch the data, check it, transform it, publish it. A workflow describes those steps and the order they need, and independent steps run at the same time.

A job is one step, and it runs in its own process. If it hangs, it gets killed. If it crashes, it takes nothing else down with it. If it fails, it can retry without redoing the work it already finished.

The dashboard is a window, not a control panel. Jobs run the same whether it is open or closed.

What you get

Everything below applies to any job you add, so a new job is mostly just the interesting part.

It remembers what it already did. Jobs that chew through hundreds of items keep a per-item ledger, so a re-run skips finished work and picks up where it stopped. Interrupt a job halfway through 5,000 files and the next run continues from 2,501.

Steps check each other's work. Where one step hands data to the next, both sides declare what that data should look like. If a website changes its layout or an export drops a column, the pipeline stops at that boundary and tells you exactly what changed, rather than quietly writing bad data to disk.

Shared APIs get shared limits. Paid or rate-limited services are declared once, with a rate limit and a monthly spend cap. Every job that calls one goes through the same meter, so caps hold no matter how many jobs run at once, and a job that hits the ceiling stops politely instead of burning through your budget.

Failures find you. Runs that fail, time out, or leave items stuck send a push notification to your phone, with a summary of what was processed.

Nothing runs twice by accident. A workflow can only have one run in flight. Ask for a second and you get told, rather than quietly getting two.

You can run a small slice. Trigger a manual run limited to a handful of items to see what a change does before letting it loose on the whole library.

Everything is inspectable. Live logs stream as jobs run, every past run is kept with its output, and the dashboard shows what each step produced.

What it currently runs

Twenty-two workflows, all included in this repo as working examples.

Looking after a Plex library

WorkflowWhat it does
plex-renameRenames every file to Plex's canonical convention, so the library can rebuild itself from disk if the database is ever lost.
plex-library-guardWatches for silent data loss and sends one urgent alert if the library shrinks or a file disappears.
plex-language-fixWorks out each title's real original language and sets the right audio and subtitle tracks.
plex-profilesWrites a markdown profile for every film and show, with cast, ratings, and technical detail.
plex-space-saverReports where the disk space actually went, biggest first.
mount-keeperKeeps the NAS shares mounted, because macOS drops them and everything else depends on them.
missing-moviesFinds collections you own part of, and tells you which films are missing.
missing-tv-seasonsSpots complete seasons of shows you follow that you haven't got yet.
movie-recommendationsReads your library's taste and suggests films worth adding, monthly.
tv-recommendationsThe same for television.

Keeping a personal archive

WorkflowWhat it does
placesTurns your Google saved places into proper written profiles of each venue.
perfumesBuilds a profile for every fragrance you own, with notes, accords, and a written summary.
media-reviewsPulls your own book, film, TV, and album reviews out of your website's database into markdown.
listening-digestA monthly record of what you actually listened to.
workouts-syncSyncs your workout history and writes a six-month progress report per exercise.
projects-syncCatalogues your GitHub repos and writes a summary of what each project is.
vault-syncCopies all of the above into your second-brain folder, with names a human would choose.

Money and housekeeping

WorkflowWhat it does
stocks-syncTakes a daily read-only snapshot of your portfolio and alerts on a big gain.
stock-digestA weekly written summary of holdings, movers, and how diversified you actually are.
vercel-daily-redeployShips a production deploy each night, so a site can't quietly go stale.
claude-warmerKeeps a usage window warm, so the jobs that need it aren't cold when they run.
overrides-auditReminds you about settings you changed in the dashboard weeks ago and forgot to make permanent.

Your own jobs stay private by default. This repo is public, and anything you add is ignored by git unless you choose to publish it. Secrets live in .env.


Running it

Two services, both kept alive by launchd, both back after a reboot.

git clone <this repo>&&cd local-jobs
npm install
bash scripts/install-launchd.sh # the enginecd dashboard && npm install && npm run build &&cd ..
bash scripts/install-dashboard-launchd.sh # the dashboard
sudo pmset -a sleep 0 disablesleep 1 # schedules can't fire while asleep

The dashboard is at http://localhost:4788. The API stays on loopback.

To reach it from your phone, put it on a Tailscale tailnet with tailscale serve --bg 4788. Never use tailscale funnel for this, which would publish it to the internet.

Day to day:

scripts/safe-restart.sh # restart the daemon (refuses while a run is in flight)
tail -f data/daemon.out.log

Settings live in .env, documented in .env.example.

Adding a job

A job is a description of itself and a function that does the work.

constjob: JobDefinition={name: 'cleanup-temp',description: 'Deletes stale temp files',timeoutMs: 600_000,maxRetries: 3,asyncrun(ctx){ctx.log('starting');ctx.progress(50,'halfway');// throw to fail the run},};

Then say when it runs, in a workflow of one step:

constworkflow: WorkflowDefinition={name: 'cleanup-temp',description: 'Nightly temp-file cleanup',schedule: '0 4 * * *',jobs: [{job: 'cleanup-temp'}],};

Drop both in src/workflows/, restart with scripts/safe-restart.sh, and it appears in the dashboard. There is no registry to update.

Testing

npm test runs the whole suite against a scratch database, so it can never touch real data or real output. Add tests as you add behaviour.

Dashboard changes have two extra checks that drive a real browser, so they run by hand rather than in CI: dashboard/scripts/mobile-check.mjs for phone widths, and dashboard/scripts/visual-check.mjs for screenshots to look at.

Reading further

CLAUDE.md covers the architecture and conventions in full. Each workflow folder has its own CLAUDE.md describing how that particular one works and why.

About

Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - RyanMKrol/LocalJobs: Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes · GitHub
Skip to content

Repository files navigation

local-jobs

A personal job runner for a Mac Mini that never sleeps.

Some work doesn't fit a serverless function or a web request. It runs for hours, drives a real browser, talks to a NAS over SMB, or needs to happen at 5am whether or not anyone is watching. This is where that work lives: one daemon that keeps about twenty jobs running on schedule, a record of every run, and a dashboard to watch it all from the sofa.

It is deliberately small. SQLite, no Docker, no queue, no cloud.

How it works

launchd ──keeps alive──▶ daemon ──runs──▶ workflow ──runs──▶ job
│ (a pipeline) (one step,
│ own process)
▼
SQLite ◀──reads── dashboard

The daemon is the only long-lived process. It wakes up on a schedule, decides what should run, and records what happened. Nothing else needs to be running for work to get done.

A workflow is a pipeline. Most real tasks are several steps that depend on each other: fetch the data, check it, transform it, publish it. A workflow describes those steps and the order they need, and independent steps run at the same time.

A job is one step, and it runs in its own process. If it hangs, it gets killed. If it crashes, it takes nothing else down with it. If it fails, it can retry without redoing the work it already finished.

The dashboard is a window, not a control panel. Jobs run the same whether it is open or closed.

What you get

Everything below applies to any job you add, so a new job is mostly just the interesting part.

It remembers what it already did. Jobs that chew through hundreds of items keep a per-item ledger, so a re-run skips finished work and picks up where it stopped. Interrupt a job halfway through 5,000 files and the next run continues from 2,501.

Steps check each other's work. Where one step hands data to the next, both sides declare what that data should look like. If a website changes its layout or an export drops a column, the pipeline stops at that boundary and tells you exactly what changed, rather than quietly writing bad data to disk.

Shared APIs get shared limits. Paid or rate-limited services are declared once, with a rate limit and a monthly spend cap. Every job that calls one goes through the same meter, so caps hold no matter how many jobs run at once, and a job that hits the ceiling stops politely instead of burning through your budget.

Failures find you. Runs that fail, time out, or leave items stuck send a push notification to your phone, with a summary of what was processed.

Nothing runs twice by accident. A workflow can only have one run in flight. Ask for a second and you get told, rather than quietly getting two.

You can run a small slice. Trigger a manual run limited to a handful of items to see what a change does before letting it loose on the whole library.

Everything is inspectable. Live logs stream as jobs run, every past run is kept with its output, and the dashboard shows what each step produced.

What it currently runs

Twenty-two workflows, all included in this repo as working examples.

Looking after a Plex library

WorkflowWhat it does
plex-renameRenames every file to Plex's canonical convention, so the library can rebuild itself from disk if the database is ever lost.
plex-library-guardWatches for silent data loss and sends one urgent alert if the library shrinks or a file disappears.
plex-language-fixWorks out each title's real original language and sets the right audio and subtitle tracks.
plex-profilesWrites a markdown profile for every film and show, with cast, ratings, and technical detail.
plex-space-saverReports where the disk space actually went, biggest first.
mount-keeperKeeps the NAS shares mounted, because macOS drops them and everything else depends on them.
missing-moviesFinds collections you own part of, and tells you which films are missing.
missing-tv-seasonsSpots complete seasons of shows you follow that you haven't got yet.
movie-recommendationsReads your library's taste and suggests films worth adding, monthly.
tv-recommendationsThe same for television.

Keeping a personal archive

WorkflowWhat it does
placesTurns your Google saved places into proper written profiles of each venue.
perfumesBuilds a profile for every fragrance you own, with notes, accords, and a written summary.
media-reviewsPulls your own book, film, TV, and album reviews out of your website's database into markdown.
listening-digestA monthly record of what you actually listened to.
workouts-syncSyncs your workout history and writes a six-month progress report per exercise.
projects-syncCatalogues your GitHub repos and writes a summary of what each project is.
vault-syncCopies all of the above into your second-brain folder, with names a human would choose.

Money and housekeeping

WorkflowWhat it does
stocks-syncTakes a daily read-only snapshot of your portfolio and alerts on a big gain.
stock-digestA weekly written summary of holdings, movers, and how diversified you actually are.
vercel-daily-redeployShips a production deploy each night, so a site can't quietly go stale.
claude-warmerKeeps a usage window warm, so the jobs that need it aren't cold when they run.
overrides-auditReminds you about settings you changed in the dashboard weeks ago and forgot to make permanent.

Your own jobs stay private by default. This repo is public, and anything you add is ignored by git unless you choose to publish it. Secrets live in .env.


Running it

Two services, both kept alive by launchd, both back after a reboot.

git clone <this repo>&&cd local-jobs
npm install
bash scripts/install-launchd.sh # the enginecd dashboard && npm install && npm run build &&cd ..
bash scripts/install-dashboard-launchd.sh # the dashboard
sudo pmset -a sleep 0 disablesleep 1 # schedules can't fire while asleep

The dashboard is at http://localhost:4788. The API stays on loopback.

To reach it from your phone, put it on a Tailscale tailnet with tailscale serve --bg 4788. Never use tailscale funnel for this, which would publish it to the internet.

Day to day:

scripts/safe-restart.sh # restart the daemon (refuses while a run is in flight)
tail -f data/daemon.out.log

Settings live in .env, documented in .env.example.

Adding a job

A job is a description of itself and a function that does the work.

constjob: JobDefinition={name: 'cleanup-temp',description: 'Deletes stale temp files',timeoutMs: 600_000,maxRetries: 3,asyncrun(ctx){ctx.log('starting');ctx.progress(50,'halfway');// throw to fail the run},};

Then say when it runs, in a workflow of one step:

constworkflow: WorkflowDefinition={name: 'cleanup-temp',description: 'Nightly temp-file cleanup',schedule: '0 4 * * *',jobs: [{job: 'cleanup-temp'}],};

Drop both in src/workflows/, restart with scripts/safe-restart.sh, and it appears in the dashboard. There is no registry to update.

Testing

npm test runs the whole suite against a scratch database, so it can never touch real data or real output. Add tests as you add behaviour.

Dashboard changes have two extra checks that drive a real browser, so they run by hand rather than in CI: dashboard/scripts/mobile-check.mjs for phone widths, and dashboard/scripts/visual-check.mjs for screenshots to look at.

Reading further

CLAUDE.md covers the architecture and conventions in full. Each workflow folder has its own CLAUDE.md describing how that particular one works and why.

About

Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - RyanMKrol/LocalJobs: Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes · GitHub
Skip to content

Repository files navigation

local-jobs

A personal job runner for a Mac Mini that never sleeps.

Some work doesn't fit a serverless function or a web request. It runs for hours, drives a real browser, talks to a NAS over SMB, or needs to happen at 5am whether or not anyone is watching. This is where that work lives: one daemon that keeps about twenty jobs running on schedule, a record of every run, and a dashboard to watch it all from the sofa.

It is deliberately small. SQLite, no Docker, no queue, no cloud.

How it works

launchd ──keeps alive──▶ daemon ──runs──▶ workflow ──runs──▶ job
│ (a pipeline) (one step,
│ own process)
▼
SQLite ◀──reads── dashboard

The daemon is the only long-lived process. It wakes up on a schedule, decides what should run, and records what happened. Nothing else needs to be running for work to get done.

A workflow is a pipeline. Most real tasks are several steps that depend on each other: fetch the data, check it, transform it, publish it. A workflow describes those steps and the order they need, and independent steps run at the same time.

A job is one step, and it runs in its own process. If it hangs, it gets killed. If it crashes, it takes nothing else down with it. If it fails, it can retry without redoing the work it already finished.

The dashboard is a window, not a control panel. Jobs run the same whether it is open or closed.

What you get

Everything below applies to any job you add, so a new job is mostly just the interesting part.

It remembers what it already did. Jobs that chew through hundreds of items keep a per-item ledger, so a re-run skips finished work and picks up where it stopped. Interrupt a job halfway through 5,000 files and the next run continues from 2,501.

Steps check each other's work. Where one step hands data to the next, both sides declare what that data should look like. If a website changes its layout or an export drops a column, the pipeline stops at that boundary and tells you exactly what changed, rather than quietly writing bad data to disk.

Shared APIs get shared limits. Paid or rate-limited services are declared once, with a rate limit and a monthly spend cap. Every job that calls one goes through the same meter, so caps hold no matter how many jobs run at once, and a job that hits the ceiling stops politely instead of burning through your budget.

Failures find you. Runs that fail, time out, or leave items stuck send a push notification to your phone, with a summary of what was processed.

Nothing runs twice by accident. A workflow can only have one run in flight. Ask for a second and you get told, rather than quietly getting two.

You can run a small slice. Trigger a manual run limited to a handful of items to see what a change does before letting it loose on the whole library.

Everything is inspectable. Live logs stream as jobs run, every past run is kept with its output, and the dashboard shows what each step produced.

What it currently runs

Twenty-two workflows, all included in this repo as working examples.

Looking after a Plex library

WorkflowWhat it does
plex-renameRenames every file to Plex's canonical convention, so the library can rebuild itself from disk if the database is ever lost.
plex-library-guardWatches for silent data loss and sends one urgent alert if the library shrinks or a file disappears.
plex-language-fixWorks out each title's real original language and sets the right audio and subtitle tracks.
plex-profilesWrites a markdown profile for every film and show, with cast, ratings, and technical detail.
plex-space-saverReports where the disk space actually went, biggest first.
mount-keeperKeeps the NAS shares mounted, because macOS drops them and everything else depends on them.
missing-moviesFinds collections you own part of, and tells you which films are missing.
missing-tv-seasonsSpots complete seasons of shows you follow that you haven't got yet.
movie-recommendationsReads your library's taste and suggests films worth adding, monthly.
tv-recommendationsThe same for television.

Keeping a personal archive

WorkflowWhat it does
placesTurns your Google saved places into proper written profiles of each venue.
perfumesBuilds a profile for every fragrance you own, with notes, accords, and a written summary.
media-reviewsPulls your own book, film, TV, and album reviews out of your website's database into markdown.
listening-digestA monthly record of what you actually listened to.
workouts-syncSyncs your workout history and writes a six-month progress report per exercise.
projects-syncCatalogues your GitHub repos and writes a summary of what each project is.
vault-syncCopies all of the above into your second-brain folder, with names a human would choose.

Money and housekeeping

WorkflowWhat it does
stocks-syncTakes a daily read-only snapshot of your portfolio and alerts on a big gain.
stock-digestA weekly written summary of holdings, movers, and how diversified you actually are.
vercel-daily-redeployShips a production deploy each night, so a site can't quietly go stale.
claude-warmerKeeps a usage window warm, so the jobs that need it aren't cold when they run.
overrides-auditReminds you about settings you changed in the dashboard weeks ago and forgot to make permanent.

Your own jobs stay private by default. This repo is public, and anything you add is ignored by git unless you choose to publish it. Secrets live in .env.


Running it

Two services, both kept alive by launchd, both back after a reboot.

git clone <this repo>&&cd local-jobs
npm install
bash scripts/install-launchd.sh # the enginecd dashboard && npm install && npm run build &&cd ..
bash scripts/install-dashboard-launchd.sh # the dashboard
sudo pmset -a sleep 0 disablesleep 1 # schedules can't fire while asleep

The dashboard is at http://localhost:4788. The API stays on loopback.

To reach it from your phone, put it on a Tailscale tailnet with tailscale serve --bg 4788. Never use tailscale funnel for this, which would publish it to the internet.

Day to day:

scripts/safe-restart.sh # restart the daemon (refuses while a run is in flight)
tail -f data/daemon.out.log

Settings live in .env, documented in .env.example.

Adding a job

A job is a description of itself and a function that does the work.

constjob: JobDefinition={name: 'cleanup-temp',description: 'Deletes stale temp files',timeoutMs: 600_000,maxRetries: 3,asyncrun(ctx){ctx.log('starting');ctx.progress(50,'halfway');// throw to fail the run},};

Then say when it runs, in a workflow of one step:

constworkflow: WorkflowDefinition={name: 'cleanup-temp',description: 'Nightly temp-file cleanup',schedule: '0 4 * * *',jobs: [{job: 'cleanup-temp'}],};

Drop both in src/workflows/, restart with scripts/safe-restart.sh, and it appears in the dashboard. There is no registry to update.

Testing

npm test runs the whole suite against a scratch database, so it can never touch real data or real output. Add tests as you add behaviour.

Dashboard changes have two extra checks that drive a real browser, so they run by hand rather than in CI: dashboard/scripts/mobile-check.mjs for phone widths, and dashboard/scripts/visual-check.mjs for screenshots to look at.

Reading further

CLAUDE.md covers the architecture and conventions in full. Each workflow folder has its own CLAUDE.md describing how that particular one works and why.

About

Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - RyanMKrol/LocalJobs: Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes · GitHub
Skip to content

Repository files navigation

local-jobs

A personal job runner for a Mac Mini that never sleeps.

Some work doesn't fit a serverless function or a web request. It runs for hours, drives a real browser, talks to a NAS over SMB, or needs to happen at 5am whether or not anyone is watching. This is where that work lives: one daemon that keeps about twenty jobs running on schedule, a record of every run, and a dashboard to watch it all from the sofa.

It is deliberately small. SQLite, no Docker, no queue, no cloud.

How it works

launchd ──keeps alive──▶ daemon ──runs──▶ workflow ──runs──▶ job
│ (a pipeline) (one step,
│ own process)
▼
SQLite ◀──reads── dashboard

The daemon is the only long-lived process. It wakes up on a schedule, decides what should run, and records what happened. Nothing else needs to be running for work to get done.

A workflow is a pipeline. Most real tasks are several steps that depend on each other: fetch the data, check it, transform it, publish it. A workflow describes those steps and the order they need, and independent steps run at the same time.

A job is one step, and it runs in its own process. If it hangs, it gets killed. If it crashes, it takes nothing else down with it. If it fails, it can retry without redoing the work it already finished.

The dashboard is a window, not a control panel. Jobs run the same whether it is open or closed.

What you get

Everything below applies to any job you add, so a new job is mostly just the interesting part.

It remembers what it already did. Jobs that chew through hundreds of items keep a per-item ledger, so a re-run skips finished work and picks up where it stopped. Interrupt a job halfway through 5,000 files and the next run continues from 2,501.

Steps check each other's work. Where one step hands data to the next, both sides declare what that data should look like. If a website changes its layout or an export drops a column, the pipeline stops at that boundary and tells you exactly what changed, rather than quietly writing bad data to disk.

Shared APIs get shared limits. Paid or rate-limited services are declared once, with a rate limit and a monthly spend cap. Every job that calls one goes through the same meter, so caps hold no matter how many jobs run at once, and a job that hits the ceiling stops politely instead of burning through your budget.

Failures find you. Runs that fail, time out, or leave items stuck send a push notification to your phone, with a summary of what was processed.

Nothing runs twice by accident. A workflow can only have one run in flight. Ask for a second and you get told, rather than quietly getting two.

You can run a small slice. Trigger a manual run limited to a handful of items to see what a change does before letting it loose on the whole library.

Everything is inspectable. Live logs stream as jobs run, every past run is kept with its output, and the dashboard shows what each step produced.

What it currently runs

Twenty-two workflows, all included in this repo as working examples.

Looking after a Plex library

WorkflowWhat it does
plex-renameRenames every file to Plex's canonical convention, so the library can rebuild itself from disk if the database is ever lost.
plex-library-guardWatches for silent data loss and sends one urgent alert if the library shrinks or a file disappears.
plex-language-fixWorks out each title's real original language and sets the right audio and subtitle tracks.
plex-profilesWrites a markdown profile for every film and show, with cast, ratings, and technical detail.
plex-space-saverReports where the disk space actually went, biggest first.
mount-keeperKeeps the NAS shares mounted, because macOS drops them and everything else depends on them.
missing-moviesFinds collections you own part of, and tells you which films are missing.
missing-tv-seasonsSpots complete seasons of shows you follow that you haven't got yet.
movie-recommendationsReads your library's taste and suggests films worth adding, monthly.
tv-recommendationsThe same for television.

Keeping a personal archive

WorkflowWhat it does
placesTurns your Google saved places into proper written profiles of each venue.
perfumesBuilds a profile for every fragrance you own, with notes, accords, and a written summary.
media-reviewsPulls your own book, film, TV, and album reviews out of your website's database into markdown.
listening-digestA monthly record of what you actually listened to.
workouts-syncSyncs your workout history and writes a six-month progress report per exercise.
projects-syncCatalogues your GitHub repos and writes a summary of what each project is.
vault-syncCopies all of the above into your second-brain folder, with names a human would choose.

Money and housekeeping

WorkflowWhat it does
stocks-syncTakes a daily read-only snapshot of your portfolio and alerts on a big gain.
stock-digestA weekly written summary of holdings, movers, and how diversified you actually are.
vercel-daily-redeployShips a production deploy each night, so a site can't quietly go stale.
claude-warmerKeeps a usage window warm, so the jobs that need it aren't cold when they run.
overrides-auditReminds you about settings you changed in the dashboard weeks ago and forgot to make permanent.

Your own jobs stay private by default. This repo is public, and anything you add is ignored by git unless you choose to publish it. Secrets live in .env.


Running it

Two services, both kept alive by launchd, both back after a reboot.

git clone <this repo>&&cd local-jobs
npm install
bash scripts/install-launchd.sh # the enginecd dashboard && npm install && npm run build &&cd ..
bash scripts/install-dashboard-launchd.sh # the dashboard
sudo pmset -a sleep 0 disablesleep 1 # schedules can't fire while asleep

The dashboard is at http://localhost:4788. The API stays on loopback.

To reach it from your phone, put it on a Tailscale tailnet with tailscale serve --bg 4788. Never use tailscale funnel for this, which would publish it to the internet.

Day to day:

scripts/safe-restart.sh # restart the daemon (refuses while a run is in flight)
tail -f data/daemon.out.log

Settings live in .env, documented in .env.example.

Adding a job

A job is a description of itself and a function that does the work.

constjob: JobDefinition={name: 'cleanup-temp',description: 'Deletes stale temp files',timeoutMs: 600_000,maxRetries: 3,asyncrun(ctx){ctx.log('starting');ctx.progress(50,'halfway');// throw to fail the run},};

Then say when it runs, in a workflow of one step:

constworkflow: WorkflowDefinition={name: 'cleanup-temp',description: 'Nightly temp-file cleanup',schedule: '0 4 * * *',jobs: [{job: 'cleanup-temp'}],};

Drop both in src/workflows/, restart with scripts/safe-restart.sh, and it appears in the dashboard. There is no registry to update.

Testing

npm test runs the whole suite against a scratch database, so it can never touch real data or real output. Add tests as you add behaviour.

Dashboard changes have two extra checks that drive a real browser, so they run by hand rather than in CI: dashboard/scripts/mobile-check.mjs for phone widths, and dashboard/scripts/visual-check.mjs for screenshots to look at.

Reading further

CLAUDE.md covers the architecture and conventions in full. Each workflow folder has its own CLAUDE.md describing how that particular one works and why.

About

Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - RyanMKrol/LocalJobs: Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes · GitHub
Skip to content

Repository files navigation

local-jobs

A personal job runner for a Mac Mini that never sleeps.

Some work doesn't fit a serverless function or a web request. It runs for hours, drives a real browser, talks to a NAS over SMB, or needs to happen at 5am whether or not anyone is watching. This is where that work lives: one daemon that keeps about twenty jobs running on schedule, a record of every run, and a dashboard to watch it all from the sofa.

It is deliberately small. SQLite, no Docker, no queue, no cloud.

How it works

launchd ──keeps alive──▶ daemon ──runs──▶ workflow ──runs──▶ job
│ (a pipeline) (one step,
│ own process)
▼
SQLite ◀──reads── dashboard

The daemon is the only long-lived process. It wakes up on a schedule, decides what should run, and records what happened. Nothing else needs to be running for work to get done.

A workflow is a pipeline. Most real tasks are several steps that depend on each other: fetch the data, check it, transform it, publish it. A workflow describes those steps and the order they need, and independent steps run at the same time.

A job is one step, and it runs in its own process. If it hangs, it gets killed. If it crashes, it takes nothing else down with it. If it fails, it can retry without redoing the work it already finished.

The dashboard is a window, not a control panel. Jobs run the same whether it is open or closed.

What you get

Everything below applies to any job you add, so a new job is mostly just the interesting part.

It remembers what it already did. Jobs that chew through hundreds of items keep a per-item ledger, so a re-run skips finished work and picks up where it stopped. Interrupt a job halfway through 5,000 files and the next run continues from 2,501.

Steps check each other's work. Where one step hands data to the next, both sides declare what that data should look like. If a website changes its layout or an export drops a column, the pipeline stops at that boundary and tells you exactly what changed, rather than quietly writing bad data to disk.

Shared APIs get shared limits. Paid or rate-limited services are declared once, with a rate limit and a monthly spend cap. Every job that calls one goes through the same meter, so caps hold no matter how many jobs run at once, and a job that hits the ceiling stops politely instead of burning through your budget.

Failures find you. Runs that fail, time out, or leave items stuck send a push notification to your phone, with a summary of what was processed.

Nothing runs twice by accident. A workflow can only have one run in flight. Ask for a second and you get told, rather than quietly getting two.

You can run a small slice. Trigger a manual run limited to a handful of items to see what a change does before letting it loose on the whole library.

Everything is inspectable. Live logs stream as jobs run, every past run is kept with its output, and the dashboard shows what each step produced.

What it currently runs

Twenty-two workflows, all included in this repo as working examples.

Looking after a Plex library

WorkflowWhat it does
plex-renameRenames every file to Plex's canonical convention, so the library can rebuild itself from disk if the database is ever lost.
plex-library-guardWatches for silent data loss and sends one urgent alert if the library shrinks or a file disappears.
plex-language-fixWorks out each title's real original language and sets the right audio and subtitle tracks.
plex-profilesWrites a markdown profile for every film and show, with cast, ratings, and technical detail.
plex-space-saverReports where the disk space actually went, biggest first.
mount-keeperKeeps the NAS shares mounted, because macOS drops them and everything else depends on them.
missing-moviesFinds collections you own part of, and tells you which films are missing.
missing-tv-seasonsSpots complete seasons of shows you follow that you haven't got yet.
movie-recommendationsReads your library's taste and suggests films worth adding, monthly.
tv-recommendationsThe same for television.

Keeping a personal archive

WorkflowWhat it does
placesTurns your Google saved places into proper written profiles of each venue.
perfumesBuilds a profile for every fragrance you own, with notes, accords, and a written summary.
media-reviewsPulls your own book, film, TV, and album reviews out of your website's database into markdown.
listening-digestA monthly record of what you actually listened to.
workouts-syncSyncs your workout history and writes a six-month progress report per exercise.
projects-syncCatalogues your GitHub repos and writes a summary of what each project is.
vault-syncCopies all of the above into your second-brain folder, with names a human would choose.

Money and housekeeping

WorkflowWhat it does
stocks-syncTakes a daily read-only snapshot of your portfolio and alerts on a big gain.
stock-digestA weekly written summary of holdings, movers, and how diversified you actually are.
vercel-daily-redeployShips a production deploy each night, so a site can't quietly go stale.
claude-warmerKeeps a usage window warm, so the jobs that need it aren't cold when they run.
overrides-auditReminds you about settings you changed in the dashboard weeks ago and forgot to make permanent.

Your own jobs stay private by default. This repo is public, and anything you add is ignored by git unless you choose to publish it. Secrets live in .env.


Running it

Two services, both kept alive by launchd, both back after a reboot.

git clone <this repo>&&cd local-jobs
npm install
bash scripts/install-launchd.sh # the enginecd dashboard && npm install && npm run build &&cd ..
bash scripts/install-dashboard-launchd.sh # the dashboard
sudo pmset -a sleep 0 disablesleep 1 # schedules can't fire while asleep

The dashboard is at http://localhost:4788. The API stays on loopback.

To reach it from your phone, put it on a Tailscale tailnet with tailscale serve --bg 4788. Never use tailscale funnel for this, which would publish it to the internet.

Day to day:

scripts/safe-restart.sh # restart the daemon (refuses while a run is in flight)
tail -f data/daemon.out.log

Settings live in .env, documented in .env.example.

Adding a job

A job is a description of itself and a function that does the work.

constjob: JobDefinition={name: 'cleanup-temp',description: 'Deletes stale temp files',timeoutMs: 600_000,maxRetries: 3,asyncrun(ctx){ctx.log('starting');ctx.progress(50,'halfway');// throw to fail the run},};

Then say when it runs, in a workflow of one step:

constworkflow: WorkflowDefinition={name: 'cleanup-temp',description: 'Nightly temp-file cleanup',schedule: '0 4 * * *',jobs: [{job: 'cleanup-temp'}],};

Drop both in src/workflows/, restart with scripts/safe-restart.sh, and it appears in the dashboard. There is no registry to update.

Testing

npm test runs the whole suite against a scratch database, so it can never touch real data or real output. Add tests as you add behaviour.

Dashboard changes have two extra checks that drive a real browser, so they run by hand rather than in CI: dashboard/scripts/mobile-check.mjs for phone widths, and dashboard/scripts/visual-check.mjs for screenshots to look at.

Reading further

CLAUDE.md covers the architecture and conventions in full. Each workflow folder has its own CLAUDE.md describing how that particular one works and why.

About

Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - RyanMKrol/LocalJobs: Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes · GitHub
Skip to content

Repository files navigation

local-jobs

A personal job runner for a Mac Mini that never sleeps.

Some work doesn't fit a serverless function or a web request. It runs for hours, drives a real browser, talks to a NAS over SMB, or needs to happen at 5am whether or not anyone is watching. This is where that work lives: one daemon that keeps about twenty jobs running on schedule, a record of every run, and a dashboard to watch it all from the sofa.

It is deliberately small. SQLite, no Docker, no queue, no cloud.

How it works

launchd ──keeps alive──▶ daemon ──runs──▶ workflow ──runs──▶ job
│ (a pipeline) (one step,
│ own process)
▼
SQLite ◀──reads── dashboard

The daemon is the only long-lived process. It wakes up on a schedule, decides what should run, and records what happened. Nothing else needs to be running for work to get done.

A workflow is a pipeline. Most real tasks are several steps that depend on each other: fetch the data, check it, transform it, publish it. A workflow describes those steps and the order they need, and independent steps run at the same time.

A job is one step, and it runs in its own process. If it hangs, it gets killed. If it crashes, it takes nothing else down with it. If it fails, it can retry without redoing the work it already finished.

The dashboard is a window, not a control panel. Jobs run the same whether it is open or closed.

What you get

Everything below applies to any job you add, so a new job is mostly just the interesting part.

It remembers what it already did. Jobs that chew through hundreds of items keep a per-item ledger, so a re-run skips finished work and picks up where it stopped. Interrupt a job halfway through 5,000 files and the next run continues from 2,501.

Steps check each other's work. Where one step hands data to the next, both sides declare what that data should look like. If a website changes its layout or an export drops a column, the pipeline stops at that boundary and tells you exactly what changed, rather than quietly writing bad data to disk.

Shared APIs get shared limits. Paid or rate-limited services are declared once, with a rate limit and a monthly spend cap. Every job that calls one goes through the same meter, so caps hold no matter how many jobs run at once, and a job that hits the ceiling stops politely instead of burning through your budget.

Failures find you. Runs that fail, time out, or leave items stuck send a push notification to your phone, with a summary of what was processed.

Nothing runs twice by accident. A workflow can only have one run in flight. Ask for a second and you get told, rather than quietly getting two.

You can run a small slice. Trigger a manual run limited to a handful of items to see what a change does before letting it loose on the whole library.

Everything is inspectable. Live logs stream as jobs run, every past run is kept with its output, and the dashboard shows what each step produced.

What it currently runs

Twenty-two workflows, all included in this repo as working examples.

Looking after a Plex library

WorkflowWhat it does
plex-renameRenames every file to Plex's canonical convention, so the library can rebuild itself from disk if the database is ever lost.
plex-library-guardWatches for silent data loss and sends one urgent alert if the library shrinks or a file disappears.
plex-language-fixWorks out each title's real original language and sets the right audio and subtitle tracks.
plex-profilesWrites a markdown profile for every film and show, with cast, ratings, and technical detail.
plex-space-saverReports where the disk space actually went, biggest first.
mount-keeperKeeps the NAS shares mounted, because macOS drops them and everything else depends on them.
missing-moviesFinds collections you own part of, and tells you which films are missing.
missing-tv-seasonsSpots complete seasons of shows you follow that you haven't got yet.
movie-recommendationsReads your library's taste and suggests films worth adding, monthly.
tv-recommendationsThe same for television.

Keeping a personal archive

WorkflowWhat it does
placesTurns your Google saved places into proper written profiles of each venue.
perfumesBuilds a profile for every fragrance you own, with notes, accords, and a written summary.
media-reviewsPulls your own book, film, TV, and album reviews out of your website's database into markdown.
listening-digestA monthly record of what you actually listened to.
workouts-syncSyncs your workout history and writes a six-month progress report per exercise.
projects-syncCatalogues your GitHub repos and writes a summary of what each project is.
vault-syncCopies all of the above into your second-brain folder, with names a human would choose.

Money and housekeeping

WorkflowWhat it does
stocks-syncTakes a daily read-only snapshot of your portfolio and alerts on a big gain.
stock-digestA weekly written summary of holdings, movers, and how diversified you actually are.
vercel-daily-redeployShips a production deploy each night, so a site can't quietly go stale.
claude-warmerKeeps a usage window warm, so the jobs that need it aren't cold when they run.
overrides-auditReminds you about settings you changed in the dashboard weeks ago and forgot to make permanent.

Your own jobs stay private by default. This repo is public, and anything you add is ignored by git unless you choose to publish it. Secrets live in .env.


Running it

Two services, both kept alive by launchd, both back after a reboot.

git clone <this repo>&&cd local-jobs
npm install
bash scripts/install-launchd.sh # the enginecd dashboard && npm install && npm run build &&cd ..
bash scripts/install-dashboard-launchd.sh # the dashboard
sudo pmset -a sleep 0 disablesleep 1 # schedules can't fire while asleep

The dashboard is at http://localhost:4788. The API stays on loopback.

To reach it from your phone, put it on a Tailscale tailnet with tailscale serve --bg 4788. Never use tailscale funnel for this, which would publish it to the internet.

Day to day:

scripts/safe-restart.sh # restart the daemon (refuses while a run is in flight)
tail -f data/daemon.out.log

Settings live in .env, documented in .env.example.

Adding a job

A job is a description of itself and a function that does the work.

constjob: JobDefinition={name: 'cleanup-temp',description: 'Deletes stale temp files',timeoutMs: 600_000,maxRetries: 3,asyncrun(ctx){ctx.log('starting');ctx.progress(50,'halfway');// throw to fail the run},};

Then say when it runs, in a workflow of one step:

constworkflow: WorkflowDefinition={name: 'cleanup-temp',description: 'Nightly temp-file cleanup',schedule: '0 4 * * *',jobs: [{job: 'cleanup-temp'}],};

Drop both in src/workflows/, restart with scripts/safe-restart.sh, and it appears in the dashboard. There is no registry to update.

Testing

npm test runs the whole suite against a scratch database, so it can never touch real data or real output. Add tests as you add behaviour.

Dashboard changes have two extra checks that drive a real browser, so they run by hand rather than in CI: dashboard/scripts/mobile-check.mjs for phone widths, and dashboard/scripts/visual-check.mjs for screenshots to look at.

Reading further

CLAUDE.md covers the architecture and conventions in full. Each workflow folder has its own CLAUDE.md describing how that particular one works and why.

About

Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - RyanMKrol/LocalJobs: Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes · GitHub
Skip to content

Repository files navigation

local-jobs

A personal job runner for a Mac Mini that never sleeps.

Some work doesn't fit a serverless function or a web request. It runs for hours, drives a real browser, talks to a NAS over SMB, or needs to happen at 5am whether or not anyone is watching. This is where that work lives: one daemon that keeps about twenty jobs running on schedule, a record of every run, and a dashboard to watch it all from the sofa.

It is deliberately small. SQLite, no Docker, no queue, no cloud.

How it works

launchd ──keeps alive──▶ daemon ──runs──▶ workflow ──runs──▶ job
│ (a pipeline) (one step,
│ own process)
▼
SQLite ◀──reads── dashboard

The daemon is the only long-lived process. It wakes up on a schedule, decides what should run, and records what happened. Nothing else needs to be running for work to get done.

A workflow is a pipeline. Most real tasks are several steps that depend on each other: fetch the data, check it, transform it, publish it. A workflow describes those steps and the order they need, and independent steps run at the same time.

A job is one step, and it runs in its own process. If it hangs, it gets killed. If it crashes, it takes nothing else down with it. If it fails, it can retry without redoing the work it already finished.

The dashboard is a window, not a control panel. Jobs run the same whether it is open or closed.

What you get

Everything below applies to any job you add, so a new job is mostly just the interesting part.

It remembers what it already did. Jobs that chew through hundreds of items keep a per-item ledger, so a re-run skips finished work and picks up where it stopped. Interrupt a job halfway through 5,000 files and the next run continues from 2,501.

Steps check each other's work. Where one step hands data to the next, both sides declare what that data should look like. If a website changes its layout or an export drops a column, the pipeline stops at that boundary and tells you exactly what changed, rather than quietly writing bad data to disk.

Shared APIs get shared limits. Paid or rate-limited services are declared once, with a rate limit and a monthly spend cap. Every job that calls one goes through the same meter, so caps hold no matter how many jobs run at once, and a job that hits the ceiling stops politely instead of burning through your budget.

Failures find you. Runs that fail, time out, or leave items stuck send a push notification to your phone, with a summary of what was processed.

Nothing runs twice by accident. A workflow can only have one run in flight. Ask for a second and you get told, rather than quietly getting two.

You can run a small slice. Trigger a manual run limited to a handful of items to see what a change does before letting it loose on the whole library.

Everything is inspectable. Live logs stream as jobs run, every past run is kept with its output, and the dashboard shows what each step produced.

What it currently runs

Twenty-two workflows, all included in this repo as working examples.

Looking after a Plex library

WorkflowWhat it does
plex-renameRenames every file to Plex's canonical convention, so the library can rebuild itself from disk if the database is ever lost.
plex-library-guardWatches for silent data loss and sends one urgent alert if the library shrinks or a file disappears.
plex-language-fixWorks out each title's real original language and sets the right audio and subtitle tracks.
plex-profilesWrites a markdown profile for every film and show, with cast, ratings, and technical detail.
plex-space-saverReports where the disk space actually went, biggest first.
mount-keeperKeeps the NAS shares mounted, because macOS drops them and everything else depends on them.
missing-moviesFinds collections you own part of, and tells you which films are missing.
missing-tv-seasonsSpots complete seasons of shows you follow that you haven't got yet.
movie-recommendationsReads your library's taste and suggests films worth adding, monthly.
tv-recommendationsThe same for television.

Keeping a personal archive

WorkflowWhat it does
placesTurns your Google saved places into proper written profiles of each venue.
perfumesBuilds a profile for every fragrance you own, with notes, accords, and a written summary.
media-reviewsPulls your own book, film, TV, and album reviews out of your website's database into markdown.
listening-digestA monthly record of what you actually listened to.
workouts-syncSyncs your workout history and writes a six-month progress report per exercise.
projects-syncCatalogues your GitHub repos and writes a summary of what each project is.
vault-syncCopies all of the above into your second-brain folder, with names a human would choose.

Money and housekeeping

WorkflowWhat it does
stocks-syncTakes a daily read-only snapshot of your portfolio and alerts on a big gain.
stock-digestA weekly written summary of holdings, movers, and how diversified you actually are.
vercel-daily-redeployShips a production deploy each night, so a site can't quietly go stale.
claude-warmerKeeps a usage window warm, so the jobs that need it aren't cold when they run.
overrides-auditReminds you about settings you changed in the dashboard weeks ago and forgot to make permanent.

Your own jobs stay private by default. This repo is public, and anything you add is ignored by git unless you choose to publish it. Secrets live in .env.


Running it

Two services, both kept alive by launchd, both back after a reboot.

git clone <this repo>&&cd local-jobs
npm install
bash scripts/install-launchd.sh # the enginecd dashboard && npm install && npm run build &&cd ..
bash scripts/install-dashboard-launchd.sh # the dashboard
sudo pmset -a sleep 0 disablesleep 1 # schedules can't fire while asleep

The dashboard is at http://localhost:4788. The API stays on loopback.

To reach it from your phone, put it on a Tailscale tailnet with tailscale serve --bg 4788. Never use tailscale funnel for this, which would publish it to the internet.

Day to day:

scripts/safe-restart.sh # restart the daemon (refuses while a run is in flight)
tail -f data/daemon.out.log

Settings live in .env, documented in .env.example.

Adding a job

A job is a description of itself and a function that does the work.

constjob: JobDefinition={name: 'cleanup-temp',description: 'Deletes stale temp files',timeoutMs: 600_000,maxRetries: 3,asyncrun(ctx){ctx.log('starting');ctx.progress(50,'halfway');// throw to fail the run},};

Then say when it runs, in a workflow of one step:

constworkflow: WorkflowDefinition={name: 'cleanup-temp',description: 'Nightly temp-file cleanup',schedule: '0 4 * * *',jobs: [{job: 'cleanup-temp'}],};

Drop both in src/workflows/, restart with scripts/safe-restart.sh, and it appears in the dashboard. There is no registry to update.

Testing

npm test runs the whole suite against a scratch database, so it can never touch real data or real output. Add tests as you add behaviour.

Dashboard changes have two extra checks that drive a real browser, so they run by hand rather than in CI: dashboard/scripts/mobile-check.mjs for phone widths, and dashboard/scripts/visual-check.mjs for screenshots to look at.

Reading further

CLAUDE.md covers the architecture and conventions in full. Each workflow folder has its own CLAUDE.md describing how that particular one works and why.

About

Self-hosted job orchestrator + Next.js dashboard for an always-on Mac Mini — schedules, runs, and tracks long-running headless jobs in isolated processes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages