') + ')', '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('^' + ".*" + ', '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" + ', '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('^' + ".*" + ', '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); } })(); })(); GitHub - mpoc/sleep-tracker: Minimalist sleep diary app using Google Sheets · GitHub
Skip to content

Repository files navigation

Sleep Tracker

A simple progressive web app to track sleep.

An example of a sleep entry logged on the 15th of October

Specifying secrets

Secrets have to be set up before running the project.

Environment variables for .env file:

  1. API_KEY - API key that client will have to provide in order to be able to access the sleep tracker webpage
  2. SPREADSHEET_ID - Google Sheets spreadsheet ID, e.g. the ID for a spreadsheet with the link https://docs.google.com/spreadsheets/d/13Nig7emkfeSDraasAERmsc82k2i3y4Csdfcazj0kUPcUY/ would be 13Nig7emkfeSDraasAERmsc82k2i3y4Csdfcazj0kUPcUY
  3. SPREADSHEET_RANGE - The name of the Google Sheets spreadsheet, e.g. Sheet1
  4. PUSHBULLET_API_KEY - PushBullet API key to send a notification after creating/deleting a sleep entry
  5. PUSHBULLET_ENABLED - Enable PushBullet notifications (default: true)
  6. WEB_PUSH_ENABLED - Enable Web Push notifications (default: false, requires VAPID keys)

Google Sheets API OAuth client credentials for secret/credentials.json file also have to be generated at https://console.cloud.google.com/apis/api/sheets.googleapis.com/credentials. The client ID has to be of type Desktop app.

During first login there will be a command line prompt to log in with Google. After logging in, a secret/token.json file will be generated and the app will have to be restarted in order for it to work.

This command can be used to generate the initial secret/token.json file:

docker run -it --rm -v `pwd`/secret:/usr/src/sleep-tracker/secret -p 8002:8000 mpoc/sleep-tracker

After running it, navigate to http://localhost:8000/?apiKey=<API_KEY> to trigger the Google login.

Web Push notifications (optional)

To enable native push notifications, generate VAPID keys:

bun run scripts/generate-vapid-keys.ts

This creates secret/vapid-keys.json. Then set WEB_PUSH_ENABLED=true in your .env file.

How to run

Running locally

bun install
bun start

Running using Docker

docker-compose up -d

How to log sleep

Go to http://localhost:8000/?apiKey=<API_KEY> to see the app.

Note that in order to log the sleep entry location, it will have run over HTTPS. Without HTTPS, this error will happen when trying to access the location:

ERROR(1): Only secure origins are allowed (see: https://goo.gl/Y0ZkNV).

How to build

docker build -t mpoc/sleep-tracker:latest .
docker push mpoc/sleep-tracker:latest

About

Minimalist sleep diary app using Google Sheets

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages