Repository files navigation

Timerbot

A countdown timer you can share. Set a duration, send someone the link, and you are both looking at the same clock — start it on your laptop and it starts on their screen too.

Every timer is a server-side record with two independently generated slugs:

  • /t/:slug — the dashboard. Whoever has this link can start, pause, reset and edit the duration.
  • /p/:slug — the projection. Watch only, and built to be put on a screen a room is looking at. It never receives the control slug, over HTTP or over the websocket, so the dashboard link cannot be guessed from it.

State changes broadcast to every connected client over Turbo Streams, and each client ticks locally against a server-provided deadline, so the model stays authoritative without a request per second.

The entry page is an editable clock with no record behind it. Creating is folded into the first action that needs one — Iniciar or Proyectar — so there is no step whose only job is to create. /5m, /45s and /5m30s open that page preset, in either case.

Interface copy is Spanish.

Running it

bin/setup # dependencies, database, git hooks
bin/rails server # http://localhost:3000

Checks

bin/rails test# the suite, JavaScript included where node is available
bin/rubocop # Ruby style (rails-omakase)
bin/ci # everything CI runs: rubocop, three security audits, tests

bin/ci deliberately does not depend on node. The clock's JavaScript is covered two other ways: bin/rails test shells out to test/javascript/*_test.mjs wherever node exists, and .githooks/pre-commit runs those plus oxlint before a commit lands. Where node is missing, that one test skips and says so — so a green CI is not a claim that the JavaScript was checked. It is safe anyway: every rule the JavaScript enforces is enforced again server-side.

Housekeeping

SweepTimersJob deletes anything untouched for Timer::RETENTION (30 days), daily. Transitions and duration edits move updated_at; rendering a page does not, so a timer someone still uses keeps itself alive. It runs only where a Solid Queue worker does — the container starts the web server alone, so the Dockerfile sets SOLID_QUEUE_IN_PUMA=true.

Stack

Rails 8, SQLite, Hotwire (Turbo + Stimulus) over importmap with no build step, Solid Queue/Cache/Cable. Deployed as a container; see Dockerfile.

About

Minimalist timer, maximum efficiency

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Timerbot

A countdown timer you can share. Set a duration, send someone the link, and you are both looking at the same clock — start it on your laptop and it starts on their screen too.

Every timer is a server-side record with two independently generated slugs:

  • /t/:slug — the dashboard. Whoever has this link can start, pause, reset and edit the duration.
  • /p/:slug — the projection. Watch only, and built to be put on a screen a room is looking at. It never receives the control slug, over HTTP or over the websocket, so the dashboard link cannot be guessed from it.

State changes broadcast to every connected client over Turbo Streams, and each client ticks locally against a server-provided deadline, so the model stays authoritative without a request per second.

The entry page is an editable clock with no record behind it. Creating is folded into the first action that needs one — Iniciar or Proyectar — so there is no step whose only job is to create. /5m, /45s and /5m30s open that page preset, in either case.

Interface copy is Spanish.

Running it

bin/setup # dependencies, database, git hooks
bin/rails server # http://localhost:3000

Checks

bin/rails test# the suite, JavaScript included where node is available
bin/rubocop # Ruby style (rails-omakase)
bin/ci # everything CI runs: rubocop, three security audits, tests

bin/ci deliberately does not depend on node. The clock's JavaScript is covered two other ways: bin/rails test shells out to test/javascript/*_test.mjs wherever node exists, and .githooks/pre-commit runs those plus oxlint before a commit lands. Where node is missing, that one test skips and says so — so a green CI is not a claim that the JavaScript was checked. It is safe anyway: every rule the JavaScript enforces is enforced again server-side.

Housekeeping

SweepTimersJob deletes anything untouched for Timer::RETENTION (30 days), daily. Transitions and duration edits move updated_at; rendering a page does not, so a timer someone still uses keeps itself alive. It runs only where a Solid Queue worker does — the container starts the web server alone, so the Dockerfile sets SOLID_QUEUE_IN_PUMA=true.

Stack

Rails 8, SQLite, Hotwire (Turbo + Stimulus) over importmap with no build step, Solid Queue/Cache/Cable. Deployed as a container; see Dockerfile.

About

Minimalist timer, maximum efficiency

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Timerbot

A countdown timer you can share. Set a duration, send someone the link, and you are both looking at the same clock — start it on your laptop and it starts on their screen too.

Every timer is a server-side record with two independently generated slugs:

  • /t/:slug — the dashboard. Whoever has this link can start, pause, reset and edit the duration.
  • /p/:slug — the projection. Watch only, and built to be put on a screen a room is looking at. It never receives the control slug, over HTTP or over the websocket, so the dashboard link cannot be guessed from it.

State changes broadcast to every connected client over Turbo Streams, and each client ticks locally against a server-provided deadline, so the model stays authoritative without a request per second.

The entry page is an editable clock with no record behind it. Creating is folded into the first action that needs one — Iniciar or Proyectar — so there is no step whose only job is to create. /5m, /45s and /5m30s open that page preset, in either case.

Interface copy is Spanish.

Running it

bin/setup # dependencies, database, git hooks
bin/rails server # http://localhost:3000

Checks

bin/rails test# the suite, JavaScript included where node is available
bin/rubocop # Ruby style (rails-omakase)
bin/ci # everything CI runs: rubocop, three security audits, tests

bin/ci deliberately does not depend on node. The clock's JavaScript is covered two other ways: bin/rails test shells out to test/javascript/*_test.mjs wherever node exists, and .githooks/pre-commit runs those plus oxlint before a commit lands. Where node is missing, that one test skips and says so — so a green CI is not a claim that the JavaScript was checked. It is safe anyway: every rule the JavaScript enforces is enforced again server-side.

Housekeeping

SweepTimersJob deletes anything untouched for Timer::RETENTION (30 days), daily. Transitions and duration edits move updated_at; rendering a page does not, so a timer someone still uses keeps itself alive. It runs only where a Solid Queue worker does — the container starts the web server alone, so the Dockerfile sets SOLID_QUEUE_IN_PUMA=true.

Stack

Rails 8, SQLite, Hotwire (Turbo + Stimulus) over importmap with no build step, Solid Queue/Cache/Cable. Deployed as a container; see Dockerfile.

About

Minimalist timer, maximum efficiency

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

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

Repository files navigation

Timerbot

A countdown timer you can share. Set a duration, send someone the link, and you are both looking at the same clock — start it on your laptop and it starts on their screen too.

Every timer is a server-side record with two independently generated slugs:

  • /t/:slug — the dashboard. Whoever has this link can start, pause, reset and edit the duration.
  • /p/:slug — the projection. Watch only, and built to be put on a screen a room is looking at. It never receives the control slug, over HTTP or over the websocket, so the dashboard link cannot be guessed from it.

State changes broadcast to every connected client over Turbo Streams, and each client ticks locally against a server-provided deadline, so the model stays authoritative without a request per second.

The entry page is an editable clock with no record behind it. Creating is folded into the first action that needs one — Iniciar or Proyectar — so there is no step whose only job is to create. /5m, /45s and /5m30s open that page preset, in either case.

Interface copy is Spanish.

Running it

bin/setup # dependencies, database, git hooks
bin/rails server # http://localhost:3000

Checks

bin/rails test# the suite, JavaScript included where node is available
bin/rubocop # Ruby style (rails-omakase)
bin/ci # everything CI runs: rubocop, three security audits, tests

bin/ci deliberately does not depend on node. The clock's JavaScript is covered two other ways: bin/rails test shells out to test/javascript/*_test.mjs wherever node exists, and .githooks/pre-commit runs those plus oxlint before a commit lands. Where node is missing, that one test skips and says so — so a green CI is not a claim that the JavaScript was checked. It is safe anyway: every rule the JavaScript enforces is enforced again server-side.

Housekeeping

SweepTimersJob deletes anything untouched for Timer::RETENTION (30 days), daily. Transitions and duration edits move updated_at; rendering a page does not, so a timer someone still uses keeps itself alive. It runs only where a Solid Queue worker does — the container starts the web server alone, so the Dockerfile sets SOLID_QUEUE_IN_PUMA=true.

Stack

Rails 8, SQLite, Hotwire (Turbo + Stimulus) over importmap with no build step, Solid Queue/Cache/Cable. Deployed as a container; see Dockerfile.

About

Minimalist timer, maximum efficiency

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Timerbot

A countdown timer you can share. Set a duration, send someone the link, and you are both looking at the same clock — start it on your laptop and it starts on their screen too.

Every timer is a server-side record with two independently generated slugs:

  • /t/:slug — the dashboard. Whoever has this link can start, pause, reset and edit the duration.
  • /p/:slug — the projection. Watch only, and built to be put on a screen a room is looking at. It never receives the control slug, over HTTP or over the websocket, so the dashboard link cannot be guessed from it.

State changes broadcast to every connected client over Turbo Streams, and each client ticks locally against a server-provided deadline, so the model stays authoritative without a request per second.

The entry page is an editable clock with no record behind it. Creating is folded into the first action that needs one — Iniciar or Proyectar — so there is no step whose only job is to create. /5m, /45s and /5m30s open that page preset, in either case.

Interface copy is Spanish.

Running it

bin/setup # dependencies, database, git hooks
bin/rails server # http://localhost:3000

Checks

bin/rails test# the suite, JavaScript included where node is available
bin/rubocop # Ruby style (rails-omakase)
bin/ci # everything CI runs: rubocop, three security audits, tests

bin/ci deliberately does not depend on node. The clock's JavaScript is covered two other ways: bin/rails test shells out to test/javascript/*_test.mjs wherever node exists, and .githooks/pre-commit runs those plus oxlint before a commit lands. Where node is missing, that one test skips and says so — so a green CI is not a claim that the JavaScript was checked. It is safe anyway: every rule the JavaScript enforces is enforced again server-side.

Housekeeping

SweepTimersJob deletes anything untouched for Timer::RETENTION (30 days), daily. Transitions and duration edits move updated_at; rendering a page does not, so a timer someone still uses keeps itself alive. It runs only where a Solid Queue worker does — the container starts the web server alone, so the Dockerfile sets SOLID_QUEUE_IN_PUMA=true.

Stack

Rails 8, SQLite, Hotwire (Turbo + Stimulus) over importmap with no build step, Solid Queue/Cache/Cable. Deployed as a container; see Dockerfile.

About

Minimalist timer, maximum efficiency

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Timerbot

A countdown timer you can share. Set a duration, send someone the link, and you are both looking at the same clock — start it on your laptop and it starts on their screen too.

Every timer is a server-side record with two independently generated slugs:

  • /t/:slug — the dashboard. Whoever has this link can start, pause, reset and edit the duration.
  • /p/:slug — the projection. Watch only, and built to be put on a screen a room is looking at. It never receives the control slug, over HTTP or over the websocket, so the dashboard link cannot be guessed from it.

State changes broadcast to every connected client over Turbo Streams, and each client ticks locally against a server-provided deadline, so the model stays authoritative without a request per second.

The entry page is an editable clock with no record behind it. Creating is folded into the first action that needs one — Iniciar or Proyectar — so there is no step whose only job is to create. /5m, /45s and /5m30s open that page preset, in either case.

Interface copy is Spanish.

Running it

bin/setup # dependencies, database, git hooks
bin/rails server # http://localhost:3000

Checks

bin/rails test# the suite, JavaScript included where node is available
bin/rubocop # Ruby style (rails-omakase)
bin/ci # everything CI runs: rubocop, three security audits, tests

bin/ci deliberately does not depend on node. The clock's JavaScript is covered two other ways: bin/rails test shells out to test/javascript/*_test.mjs wherever node exists, and .githooks/pre-commit runs those plus oxlint before a commit lands. Where node is missing, that one test skips and says so — so a green CI is not a claim that the JavaScript was checked. It is safe anyway: every rule the JavaScript enforces is enforced again server-side.

Housekeeping

SweepTimersJob deletes anything untouched for Timer::RETENTION (30 days), daily. Transitions and duration edits move updated_at; rendering a page does not, so a timer someone still uses keeps itself alive. It runs only where a Solid Queue worker does — the container starts the web server alone, so the Dockerfile sets SOLID_QUEUE_IN_PUMA=true.

Stack

Rails 8, SQLite, Hotwire (Turbo + Stimulus) over importmap with no build step, Solid Queue/Cache/Cable. Deployed as a container; see Dockerfile.

About

Minimalist timer, maximum efficiency

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Timerbot

A countdown timer you can share. Set a duration, send someone the link, and you are both looking at the same clock — start it on your laptop and it starts on their screen too.

Every timer is a server-side record with two independently generated slugs:

  • /t/:slug — the dashboard. Whoever has this link can start, pause, reset and edit the duration.
  • /p/:slug — the projection. Watch only, and built to be put on a screen a room is looking at. It never receives the control slug, over HTTP or over the websocket, so the dashboard link cannot be guessed from it.

State changes broadcast to every connected client over Turbo Streams, and each client ticks locally against a server-provided deadline, so the model stays authoritative without a request per second.

The entry page is an editable clock with no record behind it. Creating is folded into the first action that needs one — Iniciar or Proyectar — so there is no step whose only job is to create. /5m, /45s and /5m30s open that page preset, in either case.

Interface copy is Spanish.

Running it

bin/setup # dependencies, database, git hooks
bin/rails server # http://localhost:3000

Checks

bin/rails test# the suite, JavaScript included where node is available
bin/rubocop # Ruby style (rails-omakase)
bin/ci # everything CI runs: rubocop, three security audits, tests

bin/ci deliberately does not depend on node. The clock's JavaScript is covered two other ways: bin/rails test shells out to test/javascript/*_test.mjs wherever node exists, and .githooks/pre-commit runs those plus oxlint before a commit lands. Where node is missing, that one test skips and says so — so a green CI is not a claim that the JavaScript was checked. It is safe anyway: every rule the JavaScript enforces is enforced again server-side.

Housekeeping

SweepTimersJob deletes anything untouched for Timer::RETENTION (30 days), daily. Transitions and duration edits move updated_at; rendering a page does not, so a timer someone still uses keeps itself alive. It runs only where a Solid Queue worker does — the container starts the web server alone, so the Dockerfile sets SOLID_QUEUE_IN_PUMA=true.

Stack

Rails 8, SQLite, Hotwire (Turbo + Stimulus) over importmap with no build step, Solid Queue/Cache/Cable. Deployed as a container; see Dockerfile.

About

Minimalist timer, maximum efficiency

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

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

Repository files navigation

Timerbot

A countdown timer you can share. Set a duration, send someone the link, and you are both looking at the same clock — start it on your laptop and it starts on their screen too.

Every timer is a server-side record with two independently generated slugs:

  • /t/:slug — the dashboard. Whoever has this link can start, pause, reset and edit the duration.
  • /p/:slug — the projection. Watch only, and built to be put on a screen a room is looking at. It never receives the control slug, over HTTP or over the websocket, so the dashboard link cannot be guessed from it.

State changes broadcast to every connected client over Turbo Streams, and each client ticks locally against a server-provided deadline, so the model stays authoritative without a request per second.

The entry page is an editable clock with no record behind it. Creating is folded into the first action that needs one — Iniciar or Proyectar — so there is no step whose only job is to create. /5m, /45s and /5m30s open that page preset, in either case.

Interface copy is Spanish.

Running it

bin/setup # dependencies, database, git hooks
bin/rails server # http://localhost:3000

Checks

bin/rails test# the suite, JavaScript included where node is available
bin/rubocop # Ruby style (rails-omakase)
bin/ci # everything CI runs: rubocop, three security audits, tests

bin/ci deliberately does not depend on node. The clock's JavaScript is covered two other ways: bin/rails test shells out to test/javascript/*_test.mjs wherever node exists, and .githooks/pre-commit runs those plus oxlint before a commit lands. Where node is missing, that one test skips and says so — so a green CI is not a claim that the JavaScript was checked. It is safe anyway: every rule the JavaScript enforces is enforced again server-side.

Housekeeping

SweepTimersJob deletes anything untouched for Timer::RETENTION (30 days), daily. Transitions and duration edits move updated_at; rendering a page does not, so a timer someone still uses keeps itself alive. It runs only where a Solid Queue worker does — the container starts the web server alone, so the Dockerfile sets SOLID_QUEUE_IN_PUMA=true.

Stack

Rails 8, SQLite, Hotwire (Turbo + Stimulus) over importmap with no build step, Solid Queue/Cache/Cable. Deployed as a container; see Dockerfile.

About

Minimalist timer, maximum efficiency

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages