Latest commit

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

⏱️ Clockwork

A When2meet-style scheduling tool. A host creates an event with a date range, shares the link, and attendees mark the time slots they're available for. Everyone can see the combined availability at a glance.

Philosophy

Clockwork is meant to be a simple, modern way to figure out a time that works. Nothing more. It stays away from piling on extra features that get in the way when all you want to do is pick a time.

It also doesn't store any private information. There's no user accounts and nothing to log into. This keeps the server nice and slim, since it never has to deal with credentials or identities.

Why Rust Backend?

No particular reason. Just want to get my hands on the language 😆

How it works

  1. A host creates an event (name, description, and an allowed date range).
  2. The host shares the event link with attendees.
  3. Each attendee submits a name, an emoji, an optional comment, and the time slots they're free. After submission there is an option to edit or delete their own response, without needing an account.
  4. Everyone viewing the event sees every attendee's availability overlaid on one grid.
  5. Events past their end date are periodically swept and removed by a background cleanup job.

Screenshots

Everyone's availability overlaid on one grid

Running with Docker Compose

This is the easiest way to run the full stack:

cp .env.example .env
# edit .env if you're not running on localhost
docker compose up --build

API_BASE_URL and WEB_BASE_URL in .env must be URLs reachable from the browser, since API_BASE_URL is baked into the frontend bundle at build time and WEB_BASE_URL is used as the API's CORS ALLOW_ORIGIN. See .env.example for details.

Running locally

API (api/)

Requires Rust and sqlite3.

cd api
cp .example.env .env # adjust DATABASE_URL / ALLOW_ORIGIN as needed
cargo run

Migrations in api/migrations run automatically on startup. The API listens on port 3000.

Key environment variables:

VariableDescriptionDefault
DATABASE_URLSQLite connection string— (required)
ALLOW_ORIGINOrigin allowed via CORSnone (CORS disabled)
EVENT_CLEANUP_INTERVAL_SECSHow often expired events are swept3600

Web (web/)

Requires Node.js and pnpm.

cd web
cp example.env .env # set VITE_API_BASE_URL if the API isn't on localhost:3000
pnpm install
pnpm dev

Other useful scripts: pnpm build, pnpm lint, pnpm format, pnpm typecheck.

API

The HTTP API is documented in api/openapi.yaml. Endpoints:

MethodPathDescription
POST/eventsCreate an event
GET/events/{event_id}Get an event with all attendees and time slots
POST/events/{event_id}/time-slotsSubmit an attendee's availability
PUT/events/{event_id}/attendees/{attendee_id}Edit an attendee's submission
DELETE/events/{event_id}/attendees/{attendee_id}Delete an attendee's submission

Editing or deleting a submission requires the submission token issued when it was created.

Testing

Browser end-to-end tests (Playwright) live in e2e/:

cd e2e
pnpm install
pnpm exec playwright install chromium # first run only
pnpm test

This boots a real API (against a scratch SQLite DB) and the web dev server itself, so no separate setup is needed.

Tech stack

  • API: Rust, Axum, SQLx (SQLite), Tokio
  • Web: React 19, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router
  • Deployment: Docker Compose, Caddy (serving the built web app)

AI usage disclosure

This project is created by a human, who is also responsible for its high-level design. AI is used as a coding agent to help implement that design as well as writing documents, and all AI-generated code is reviewed by a human before being accepted.

License

AGPLv3

About

A simple web app that helps your friend group pick an event time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

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

Latest commit

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

⏱️ Clockwork

A When2meet-style scheduling tool. A host creates an event with a date range, shares the link, and attendees mark the time slots they're available for. Everyone can see the combined availability at a glance.

Philosophy

Clockwork is meant to be a simple, modern way to figure out a time that works. Nothing more. It stays away from piling on extra features that get in the way when all you want to do is pick a time.

It also doesn't store any private information. There's no user accounts and nothing to log into. This keeps the server nice and slim, since it never has to deal with credentials or identities.

Why Rust Backend?

No particular reason. Just want to get my hands on the language 😆

How it works

  1. A host creates an event (name, description, and an allowed date range).
  2. The host shares the event link with attendees.
  3. Each attendee submits a name, an emoji, an optional comment, and the time slots they're free. After submission there is an option to edit or delete their own response, without needing an account.
  4. Everyone viewing the event sees every attendee's availability overlaid on one grid.
  5. Events past their end date are periodically swept and removed by a background cleanup job.

Screenshots

Everyone's availability overlaid on one grid

Running with Docker Compose

This is the easiest way to run the full stack:

cp .env.example .env
# edit .env if you're not running on localhost
docker compose up --build

API_BASE_URL and WEB_BASE_URL in .env must be URLs reachable from the browser, since API_BASE_URL is baked into the frontend bundle at build time and WEB_BASE_URL is used as the API's CORS ALLOW_ORIGIN. See .env.example for details.

Running locally

API (api/)

Requires Rust and sqlite3.

cd api
cp .example.env .env # adjust DATABASE_URL / ALLOW_ORIGIN as needed
cargo run

Migrations in api/migrations run automatically on startup. The API listens on port 3000.

Key environment variables:

VariableDescriptionDefault
DATABASE_URLSQLite connection string— (required)
ALLOW_ORIGINOrigin allowed via CORSnone (CORS disabled)
EVENT_CLEANUP_INTERVAL_SECSHow often expired events are swept3600

Web (web/)

Requires Node.js and pnpm.

cd web
cp example.env .env # set VITE_API_BASE_URL if the API isn't on localhost:3000
pnpm install
pnpm dev

Other useful scripts: pnpm build, pnpm lint, pnpm format, pnpm typecheck.

API

The HTTP API is documented in api/openapi.yaml. Endpoints:

MethodPathDescription
POST/eventsCreate an event
GET/events/{event_id}Get an event with all attendees and time slots
POST/events/{event_id}/time-slotsSubmit an attendee's availability
PUT/events/{event_id}/attendees/{attendee_id}Edit an attendee's submission
DELETE/events/{event_id}/attendees/{attendee_id}Delete an attendee's submission

Editing or deleting a submission requires the submission token issued when it was created.

Testing

Browser end-to-end tests (Playwright) live in e2e/:

cd e2e
pnpm install
pnpm exec playwright install chromium # first run only
pnpm test

This boots a real API (against a scratch SQLite DB) and the web dev server itself, so no separate setup is needed.

Tech stack

  • API: Rust, Axum, SQLx (SQLite), Tokio
  • Web: React 19, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router
  • Deployment: Docker Compose, Caddy (serving the built web app)

AI usage disclosure

This project is created by a human, who is also responsible for its high-level design. AI is used as a coding agent to help implement that design as well as writing documents, and all AI-generated code is reviewed by a human before being accepted.

License

AGPLv3

About

A simple web app that helps your friend group pick an event time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

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

Latest commit

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

⏱️ Clockwork

A When2meet-style scheduling tool. A host creates an event with a date range, shares the link, and attendees mark the time slots they're available for. Everyone can see the combined availability at a glance.

Philosophy

Clockwork is meant to be a simple, modern way to figure out a time that works. Nothing more. It stays away from piling on extra features that get in the way when all you want to do is pick a time.

It also doesn't store any private information. There's no user accounts and nothing to log into. This keeps the server nice and slim, since it never has to deal with credentials or identities.

Why Rust Backend?

No particular reason. Just want to get my hands on the language 😆

How it works

  1. A host creates an event (name, description, and an allowed date range).
  2. The host shares the event link with attendees.
  3. Each attendee submits a name, an emoji, an optional comment, and the time slots they're free. After submission there is an option to edit or delete their own response, without needing an account.
  4. Everyone viewing the event sees every attendee's availability overlaid on one grid.
  5. Events past their end date are periodically swept and removed by a background cleanup job.

Screenshots

Everyone's availability overlaid on one grid

Running with Docker Compose

This is the easiest way to run the full stack:

cp .env.example .env
# edit .env if you're not running on localhost
docker compose up --build

API_BASE_URL and WEB_BASE_URL in .env must be URLs reachable from the browser, since API_BASE_URL is baked into the frontend bundle at build time and WEB_BASE_URL is used as the API's CORS ALLOW_ORIGIN. See .env.example for details.

Running locally

API (api/)

Requires Rust and sqlite3.

cd api
cp .example.env .env # adjust DATABASE_URL / ALLOW_ORIGIN as needed
cargo run

Migrations in api/migrations run automatically on startup. The API listens on port 3000.

Key environment variables:

VariableDescriptionDefault
DATABASE_URLSQLite connection string— (required)
ALLOW_ORIGINOrigin allowed via CORSnone (CORS disabled)
EVENT_CLEANUP_INTERVAL_SECSHow often expired events are swept3600

Web (web/)

Requires Node.js and pnpm.

cd web
cp example.env .env # set VITE_API_BASE_URL if the API isn't on localhost:3000
pnpm install
pnpm dev

Other useful scripts: pnpm build, pnpm lint, pnpm format, pnpm typecheck.

API

The HTTP API is documented in api/openapi.yaml. Endpoints:

MethodPathDescription
POST/eventsCreate an event
GET/events/{event_id}Get an event with all attendees and time slots
POST/events/{event_id}/time-slotsSubmit an attendee's availability
PUT/events/{event_id}/attendees/{attendee_id}Edit an attendee's submission
DELETE/events/{event_id}/attendees/{attendee_id}Delete an attendee's submission

Editing or deleting a submission requires the submission token issued when it was created.

Testing

Browser end-to-end tests (Playwright) live in e2e/:

cd e2e
pnpm install
pnpm exec playwright install chromium # first run only
pnpm test

This boots a real API (against a scratch SQLite DB) and the web dev server itself, so no separate setup is needed.

Tech stack

  • API: Rust, Axum, SQLx (SQLite), Tokio
  • Web: React 19, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router
  • Deployment: Docker Compose, Caddy (serving the built web app)

AI usage disclosure

This project is created by a human, who is also responsible for its high-level design. AI is used as a coding agent to help implement that design as well as writing documents, and all AI-generated code is reviewed by a human before being accepted.

License

AGPLv3

About

A simple web app that helps your friend group pick an event time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

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

Latest commit

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

⏱️ Clockwork

A When2meet-style scheduling tool. A host creates an event with a date range, shares the link, and attendees mark the time slots they're available for. Everyone can see the combined availability at a glance.

Philosophy

Clockwork is meant to be a simple, modern way to figure out a time that works. Nothing more. It stays away from piling on extra features that get in the way when all you want to do is pick a time.

It also doesn't store any private information. There's no user accounts and nothing to log into. This keeps the server nice and slim, since it never has to deal with credentials or identities.

Why Rust Backend?

No particular reason. Just want to get my hands on the language 😆

How it works

  1. A host creates an event (name, description, and an allowed date range).
  2. The host shares the event link with attendees.
  3. Each attendee submits a name, an emoji, an optional comment, and the time slots they're free. After submission there is an option to edit or delete their own response, without needing an account.
  4. Everyone viewing the event sees every attendee's availability overlaid on one grid.
  5. Events past their end date are periodically swept and removed by a background cleanup job.

Screenshots

Everyone's availability overlaid on one grid

Running with Docker Compose

This is the easiest way to run the full stack:

cp .env.example .env
# edit .env if you're not running on localhost
docker compose up --build

API_BASE_URL and WEB_BASE_URL in .env must be URLs reachable from the browser, since API_BASE_URL is baked into the frontend bundle at build time and WEB_BASE_URL is used as the API's CORS ALLOW_ORIGIN. See .env.example for details.

Running locally

API (api/)

Requires Rust and sqlite3.

cd api
cp .example.env .env # adjust DATABASE_URL / ALLOW_ORIGIN as needed
cargo run

Migrations in api/migrations run automatically on startup. The API listens on port 3000.

Key environment variables:

VariableDescriptionDefault
DATABASE_URLSQLite connection string— (required)
ALLOW_ORIGINOrigin allowed via CORSnone (CORS disabled)
EVENT_CLEANUP_INTERVAL_SECSHow often expired events are swept3600

Web (web/)

Requires Node.js and pnpm.

cd web
cp example.env .env # set VITE_API_BASE_URL if the API isn't on localhost:3000
pnpm install
pnpm dev

Other useful scripts: pnpm build, pnpm lint, pnpm format, pnpm typecheck.

API

The HTTP API is documented in api/openapi.yaml. Endpoints:

MethodPathDescription
POST/eventsCreate an event
GET/events/{event_id}Get an event with all attendees and time slots
POST/events/{event_id}/time-slotsSubmit an attendee's availability
PUT/events/{event_id}/attendees/{attendee_id}Edit an attendee's submission
DELETE/events/{event_id}/attendees/{attendee_id}Delete an attendee's submission

Editing or deleting a submission requires the submission token issued when it was created.

Testing

Browser end-to-end tests (Playwright) live in e2e/:

cd e2e
pnpm install
pnpm exec playwright install chromium # first run only
pnpm test

This boots a real API (against a scratch SQLite DB) and the web dev server itself, so no separate setup is needed.

Tech stack

  • API: Rust, Axum, SQLx (SQLite), Tokio
  • Web: React 19, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router
  • Deployment: Docker Compose, Caddy (serving the built web app)

AI usage disclosure

This project is created by a human, who is also responsible for its high-level design. AI is used as a coding agent to help implement that design as well as writing documents, and all AI-generated code is reviewed by a human before being accepted.

License

AGPLv3

About

A simple web app that helps your friend group pick an event time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

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

Latest commit

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

⏱️ Clockwork

A When2meet-style scheduling tool. A host creates an event with a date range, shares the link, and attendees mark the time slots they're available for. Everyone can see the combined availability at a glance.

Philosophy

Clockwork is meant to be a simple, modern way to figure out a time that works. Nothing more. It stays away from piling on extra features that get in the way when all you want to do is pick a time.

It also doesn't store any private information. There's no user accounts and nothing to log into. This keeps the server nice and slim, since it never has to deal with credentials or identities.

Why Rust Backend?

No particular reason. Just want to get my hands on the language 😆

How it works

  1. A host creates an event (name, description, and an allowed date range).
  2. The host shares the event link with attendees.
  3. Each attendee submits a name, an emoji, an optional comment, and the time slots they're free. After submission there is an option to edit or delete their own response, without needing an account.
  4. Everyone viewing the event sees every attendee's availability overlaid on one grid.
  5. Events past their end date are periodically swept and removed by a background cleanup job.

Screenshots

Everyone's availability overlaid on one grid

Running with Docker Compose

This is the easiest way to run the full stack:

cp .env.example .env
# edit .env if you're not running on localhost
docker compose up --build

API_BASE_URL and WEB_BASE_URL in .env must be URLs reachable from the browser, since API_BASE_URL is baked into the frontend bundle at build time and WEB_BASE_URL is used as the API's CORS ALLOW_ORIGIN. See .env.example for details.

Running locally

API (api/)

Requires Rust and sqlite3.

cd api
cp .example.env .env # adjust DATABASE_URL / ALLOW_ORIGIN as needed
cargo run

Migrations in api/migrations run automatically on startup. The API listens on port 3000.

Key environment variables:

VariableDescriptionDefault
DATABASE_URLSQLite connection string— (required)
ALLOW_ORIGINOrigin allowed via CORSnone (CORS disabled)
EVENT_CLEANUP_INTERVAL_SECSHow often expired events are swept3600

Web (web/)

Requires Node.js and pnpm.

cd web
cp example.env .env # set VITE_API_BASE_URL if the API isn't on localhost:3000
pnpm install
pnpm dev

Other useful scripts: pnpm build, pnpm lint, pnpm format, pnpm typecheck.

API

The HTTP API is documented in api/openapi.yaml. Endpoints:

MethodPathDescription
POST/eventsCreate an event
GET/events/{event_id}Get an event with all attendees and time slots
POST/events/{event_id}/time-slotsSubmit an attendee's availability
PUT/events/{event_id}/attendees/{attendee_id}Edit an attendee's submission
DELETE/events/{event_id}/attendees/{attendee_id}Delete an attendee's submission

Editing or deleting a submission requires the submission token issued when it was created.

Testing

Browser end-to-end tests (Playwright) live in e2e/:

cd e2e
pnpm install
pnpm exec playwright install chromium # first run only
pnpm test

This boots a real API (against a scratch SQLite DB) and the web dev server itself, so no separate setup is needed.

Tech stack

  • API: Rust, Axum, SQLx (SQLite), Tokio
  • Web: React 19, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router
  • Deployment: Docker Compose, Caddy (serving the built web app)

AI usage disclosure

This project is created by a human, who is also responsible for its high-level design. AI is used as a coding agent to help implement that design as well as writing documents, and all AI-generated code is reviewed by a human before being accepted.

License

AGPLv3

About

A simple web app that helps your friend group pick an event time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

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

Latest commit

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

⏱️ Clockwork

A When2meet-style scheduling tool. A host creates an event with a date range, shares the link, and attendees mark the time slots they're available for. Everyone can see the combined availability at a glance.

Philosophy

Clockwork is meant to be a simple, modern way to figure out a time that works. Nothing more. It stays away from piling on extra features that get in the way when all you want to do is pick a time.

It also doesn't store any private information. There's no user accounts and nothing to log into. This keeps the server nice and slim, since it never has to deal with credentials or identities.

Why Rust Backend?

No particular reason. Just want to get my hands on the language 😆

How it works

  1. A host creates an event (name, description, and an allowed date range).
  2. The host shares the event link with attendees.
  3. Each attendee submits a name, an emoji, an optional comment, and the time slots they're free. After submission there is an option to edit or delete their own response, without needing an account.
  4. Everyone viewing the event sees every attendee's availability overlaid on one grid.
  5. Events past their end date are periodically swept and removed by a background cleanup job.

Screenshots

Everyone's availability overlaid on one grid

Running with Docker Compose

This is the easiest way to run the full stack:

cp .env.example .env
# edit .env if you're not running on localhost
docker compose up --build

API_BASE_URL and WEB_BASE_URL in .env must be URLs reachable from the browser, since API_BASE_URL is baked into the frontend bundle at build time and WEB_BASE_URL is used as the API's CORS ALLOW_ORIGIN. See .env.example for details.

Running locally

API (api/)

Requires Rust and sqlite3.

cd api
cp .example.env .env # adjust DATABASE_URL / ALLOW_ORIGIN as needed
cargo run

Migrations in api/migrations run automatically on startup. The API listens on port 3000.

Key environment variables:

VariableDescriptionDefault
DATABASE_URLSQLite connection string— (required)
ALLOW_ORIGINOrigin allowed via CORSnone (CORS disabled)
EVENT_CLEANUP_INTERVAL_SECSHow often expired events are swept3600

Web (web/)

Requires Node.js and pnpm.

cd web
cp example.env .env # set VITE_API_BASE_URL if the API isn't on localhost:3000
pnpm install
pnpm dev

Other useful scripts: pnpm build, pnpm lint, pnpm format, pnpm typecheck.

API

The HTTP API is documented in api/openapi.yaml. Endpoints:

MethodPathDescription
POST/eventsCreate an event
GET/events/{event_id}Get an event with all attendees and time slots
POST/events/{event_id}/time-slotsSubmit an attendee's availability
PUT/events/{event_id}/attendees/{attendee_id}Edit an attendee's submission
DELETE/events/{event_id}/attendees/{attendee_id}Delete an attendee's submission

Editing or deleting a submission requires the submission token issued when it was created.

Testing

Browser end-to-end tests (Playwright) live in e2e/:

cd e2e
pnpm install
pnpm exec playwright install chromium # first run only
pnpm test

This boots a real API (against a scratch SQLite DB) and the web dev server itself, so no separate setup is needed.

Tech stack

  • API: Rust, Axum, SQLx (SQLite), Tokio
  • Web: React 19, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router
  • Deployment: Docker Compose, Caddy (serving the built web app)

AI usage disclosure

This project is created by a human, who is also responsible for its high-level design. AI is used as a coding agent to help implement that design as well as writing documents, and all AI-generated code is reviewed by a human before being accepted.

License

AGPLv3

About

A simple web app that helps your friend group pick an event time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

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

Latest commit

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

⏱️ Clockwork

A When2meet-style scheduling tool. A host creates an event with a date range, shares the link, and attendees mark the time slots they're available for. Everyone can see the combined availability at a glance.

Philosophy

Clockwork is meant to be a simple, modern way to figure out a time that works. Nothing more. It stays away from piling on extra features that get in the way when all you want to do is pick a time.

It also doesn't store any private information. There's no user accounts and nothing to log into. This keeps the server nice and slim, since it never has to deal with credentials or identities.

Why Rust Backend?

No particular reason. Just want to get my hands on the language 😆

How it works

  1. A host creates an event (name, description, and an allowed date range).
  2. The host shares the event link with attendees.
  3. Each attendee submits a name, an emoji, an optional comment, and the time slots they're free. After submission there is an option to edit or delete their own response, without needing an account.
  4. Everyone viewing the event sees every attendee's availability overlaid on one grid.
  5. Events past their end date are periodically swept and removed by a background cleanup job.

Screenshots

Everyone's availability overlaid on one grid

Running with Docker Compose

This is the easiest way to run the full stack:

cp .env.example .env
# edit .env if you're not running on localhost
docker compose up --build

API_BASE_URL and WEB_BASE_URL in .env must be URLs reachable from the browser, since API_BASE_URL is baked into the frontend bundle at build time and WEB_BASE_URL is used as the API's CORS ALLOW_ORIGIN. See .env.example for details.

Running locally

API (api/)

Requires Rust and sqlite3.

cd api
cp .example.env .env # adjust DATABASE_URL / ALLOW_ORIGIN as needed
cargo run

Migrations in api/migrations run automatically on startup. The API listens on port 3000.

Key environment variables:

VariableDescriptionDefault
DATABASE_URLSQLite connection string— (required)
ALLOW_ORIGINOrigin allowed via CORSnone (CORS disabled)
EVENT_CLEANUP_INTERVAL_SECSHow often expired events are swept3600

Web (web/)

Requires Node.js and pnpm.

cd web
cp example.env .env # set VITE_API_BASE_URL if the API isn't on localhost:3000
pnpm install
pnpm dev

Other useful scripts: pnpm build, pnpm lint, pnpm format, pnpm typecheck.

API

The HTTP API is documented in api/openapi.yaml. Endpoints:

MethodPathDescription
POST/eventsCreate an event
GET/events/{event_id}Get an event with all attendees and time slots
POST/events/{event_id}/time-slotsSubmit an attendee's availability
PUT/events/{event_id}/attendees/{attendee_id}Edit an attendee's submission
DELETE/events/{event_id}/attendees/{attendee_id}Delete an attendee's submission

Editing or deleting a submission requires the submission token issued when it was created.

Testing

Browser end-to-end tests (Playwright) live in e2e/:

cd e2e
pnpm install
pnpm exec playwright install chromium # first run only
pnpm test

This boots a real API (against a scratch SQLite DB) and the web dev server itself, so no separate setup is needed.

Tech stack

  • API: Rust, Axum, SQLx (SQLite), Tokio
  • Web: React 19, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router
  • Deployment: Docker Compose, Caddy (serving the built web app)

AI usage disclosure

This project is created by a human, who is also responsible for its high-level design. AI is used as a coding agent to help implement that design as well as writing documents, and all AI-generated code is reviewed by a human before being accepted.

License

AGPLv3

About

A simple web app that helps your friend group pick an event time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

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

Latest commit

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

⏱️ Clockwork

A When2meet-style scheduling tool. A host creates an event with a date range, shares the link, and attendees mark the time slots they're available for. Everyone can see the combined availability at a glance.

Philosophy

Clockwork is meant to be a simple, modern way to figure out a time that works. Nothing more. It stays away from piling on extra features that get in the way when all you want to do is pick a time.

It also doesn't store any private information. There's no user accounts and nothing to log into. This keeps the server nice and slim, since it never has to deal with credentials or identities.

Why Rust Backend?

No particular reason. Just want to get my hands on the language 😆

How it works

  1. A host creates an event (name, description, and an allowed date range).
  2. The host shares the event link with attendees.
  3. Each attendee submits a name, an emoji, an optional comment, and the time slots they're free. After submission there is an option to edit or delete their own response, without needing an account.
  4. Everyone viewing the event sees every attendee's availability overlaid on one grid.
  5. Events past their end date are periodically swept and removed by a background cleanup job.

Screenshots

Everyone's availability overlaid on one grid

Running with Docker Compose

This is the easiest way to run the full stack:

cp .env.example .env
# edit .env if you're not running on localhost
docker compose up --build

API_BASE_URL and WEB_BASE_URL in .env must be URLs reachable from the browser, since API_BASE_URL is baked into the frontend bundle at build time and WEB_BASE_URL is used as the API's CORS ALLOW_ORIGIN. See .env.example for details.

Running locally

API (api/)

Requires Rust and sqlite3.

cd api
cp .example.env .env # adjust DATABASE_URL / ALLOW_ORIGIN as needed
cargo run

Migrations in api/migrations run automatically on startup. The API listens on port 3000.

Key environment variables:

VariableDescriptionDefault
DATABASE_URLSQLite connection string— (required)
ALLOW_ORIGINOrigin allowed via CORSnone (CORS disabled)
EVENT_CLEANUP_INTERVAL_SECSHow often expired events are swept3600

Web (web/)

Requires Node.js and pnpm.

cd web
cp example.env .env # set VITE_API_BASE_URL if the API isn't on localhost:3000
pnpm install
pnpm dev

Other useful scripts: pnpm build, pnpm lint, pnpm format, pnpm typecheck.

API

The HTTP API is documented in api/openapi.yaml. Endpoints:

MethodPathDescription
POST/eventsCreate an event
GET/events/{event_id}Get an event with all attendees and time slots
POST/events/{event_id}/time-slotsSubmit an attendee's availability
PUT/events/{event_id}/attendees/{attendee_id}Edit an attendee's submission
DELETE/events/{event_id}/attendees/{attendee_id}Delete an attendee's submission

Editing or deleting a submission requires the submission token issued when it was created.

Testing

Browser end-to-end tests (Playwright) live in e2e/:

cd e2e
pnpm install
pnpm exec playwright install chromium # first run only
pnpm test

This boots a real API (against a scratch SQLite DB) and the web dev server itself, so no separate setup is needed.

Tech stack

  • API: Rust, Axum, SQLx (SQLite), Tokio
  • Web: React 19, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router
  • Deployment: Docker Compose, Caddy (serving the built web app)

AI usage disclosure

This project is created by a human, who is also responsible for its high-level design. AI is used as a coding agent to help implement that design as well as writing documents, and all AI-generated code is reviewed by a human before being accepted.

License

AGPLv3

About

A simple web app that helps your friend group pick an event time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages