Compare commits
23 Commits
d2b8d60598
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ca4ac4c3ac | |||
| 6471d72b74 | |||
| 4e2d489632 | |||
| f964b6e344 | |||
| 3fbf19a271 | |||
| 35bce6e53d | |||
| 4c7d05abd0 | |||
| d77ed7ed31 | |||
| bcf994ab7b | |||
| 936ce2fc96 | |||
| 12c4553c81 | |||
| 3c2b3d1a88 | |||
| e48221b1cd | |||
| a3d5d2cbfa | |||
| 2c6491137f | |||
| f16f202d3e | |||
| 83b9e9fead | |||
| 17feb6ad2f | |||
| b167041ce2 | |||
| 0de45ef1e5 | |||
| 61d62ca2f4 | |||
| 30431709d3 | |||
| e7d32152d5 |
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY app.py .
|
||||||
|
COPY templates ./templates
|
||||||
|
COPY static ./static
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]
|
||||||
54
Jenkinsfile
vendored
Normal file
54
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
REGISTRY = "gitea.thewrightserver.net"
|
||||||
|
IMAGE = "gitea.thewrightserver.net/josh/workweekprogress"
|
||||||
|
TAG = "${env.BUILD_NUMBER}"
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage("Checkout") {
|
||||||
|
steps {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage("Build Image") {
|
||||||
|
steps {
|
||||||
|
sh """
|
||||||
|
docker build \
|
||||||
|
-t ${IMAGE}:${TAG} \
|
||||||
|
-t ${IMAGE}:latest \
|
||||||
|
.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage("Push Image") {
|
||||||
|
steps {
|
||||||
|
withCredentials([
|
||||||
|
usernamePassword(
|
||||||
|
credentialsId: 'gitea-registry-creds',
|
||||||
|
usernameVariable: 'GITEA_USER',
|
||||||
|
passwordVariable: 'GITEA_TOKEN'
|
||||||
|
)
|
||||||
|
]) {
|
||||||
|
sh """
|
||||||
|
echo "$GITEA_TOKEN" | docker login ${REGISTRY} \
|
||||||
|
-u "$GITEA_USER" --password-stdin
|
||||||
|
|
||||||
|
docker push ${IMAGE}:${TAG}
|
||||||
|
docker push ${IMAGE}:latest
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
cleanup {
|
||||||
|
sh "docker image prune -f"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
README.md
34
README.md
@@ -1,4 +1,4 @@
|
|||||||
# ⏳ Work Week Progress Bar
|
# Day Drain
|
||||||
|
|
||||||
Because staring at the clock is bad for morale, but staring at a **progress bar** is somehow motivating.
|
Because staring at the clock is bad for morale, but staring at a **progress bar** is somehow motivating.
|
||||||
|
|
||||||
@@ -8,29 +8,25 @@ This is a small Flask-powered web app that visualizes:
|
|||||||
|
|
||||||
It progresses **only during your scheduled work hours**
|
It progresses **only during your scheduled work hours**
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧠 What It Does
|
|
||||||
|
|
||||||
### Daily Progress Bar
|
### Daily Progress Bar
|
||||||
- Advances **every minute**
|
- Advances **every 10ms** (configurable)
|
||||||
- Only runs **Sunday–Wednesday**
|
- Only runs **on workdays**
|
||||||
- Only between **7:00 AM – 5:30 PM**
|
- Only during **work hours**
|
||||||
- Before work? Frozen.
|
|
||||||
- After work? Done.
|
|
||||||
- Not a workday? Blissfully idle.
|
|
||||||
|
|
||||||
### Weekly Progress Bar
|
### Weekly Progress Bar
|
||||||
- Your work week is **4 days** (Sun–Wed)
|
- Assumes your work week is **4 days**
|
||||||
- Each day = **25%**
|
- Each day = **25%**
|
||||||
- During the day, the bar fills smoothly
|
- During the day, the bar fills smoothly
|
||||||
- End of Wednesday = **100% freedom**
|
- End of Workweek = **100% freedom**
|
||||||
|
|
||||||
---
|
### Demo Site
|
||||||
|
https://daydrain.com
|
||||||
|
|
||||||
## 🛠 Tech Stack
|
### Configuration
|
||||||
|
|
||||||
- **Flask** – serves the page and minds its business
|
| Variable | Default | Description |
|
||||||
- **Vanilla JavaScript** – handles all time logic client-side
|
|--------|---------|-------------|
|
||||||
- **HTML + CSS** – gradients, glow, and just enough polish
|
| WORK_START_TIME | 07:00 | Workday start (HH:MM) |
|
||||||
- **Zero databases** – this app remembers nothing, like a healthy coping mechanism
|
| WORK_END_TIME | 17:30 | Workday end (HH:MM) |
|
||||||
|
| WORK_DAYS | 0,1,2,3 | JS day numbers (Sun=0) |
|
||||||
|
| UPDATE_INTERVAL_MS | 10 | Update frequency |
|
||||||
|
|||||||
35
app.py
35
app.py
@@ -1,10 +1,41 @@
|
|||||||
|
import os
|
||||||
from flask import Flask, render_template
|
from flask import Flask, render_template
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
def parse_time(value, default):
|
||||||
|
try:
|
||||||
|
hour, minute = map(int, value.split(":"))
|
||||||
|
return {"hour": hour, "minute": minute}
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
start_time = parse_time(
|
||||||
|
os.getenv("WORK_START_TIME", "07:00"),
|
||||||
|
{"hour": 7, "minute": 0}
|
||||||
|
)
|
||||||
|
end_time = parse_time(
|
||||||
|
os.getenv("WORK_END_TIME", "17:30"),
|
||||||
|
{"hour": 17, "minute": 30}
|
||||||
|
)
|
||||||
|
|
||||||
|
work_days = [
|
||||||
|
int(d.strip())
|
||||||
|
for d in os.getenv("WORK_DAYS", "0,1,2,3").split(",")
|
||||||
|
if d.strip().isdigit()
|
||||||
|
]
|
||||||
|
|
||||||
|
update_interval = int(os.getenv("UPDATE_INTERVAL_MS", "10"))
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"index.html",
|
||||||
|
work_start=start_time,
|
||||||
|
work_end=end_time,
|
||||||
|
work_days=work_days,
|
||||||
|
update_interval=update_interval
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
app.run(host="0.0.0.0", port=5000)
|
||||||
|
|||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask
|
||||||
|
gunicorn
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Work Week Progress</title>
|
<title>Day Drain</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Workday Progress</h1>
|
<h1>Day Drain</h1>
|
||||||
<div class="status" id="statusText"></div>
|
<div class="status" id="statusText"></div>
|
||||||
|
|
||||||
<!-- Daily Progress -->
|
<!-- Daily Progress -->
|
||||||
@@ -25,14 +25,34 @@
|
|||||||
<div class="percent weekly-percent" id="weeklyPercent">0%</div>
|
<div class="percent weekly-percent" id="weeklyPercent">0%</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- App configuration -->
|
||||||
const WORK_START_HOUR = 7;
|
<script id="app-config" type="application/json">
|
||||||
const WORK_START_MINUTE = 0;
|
{{ {
|
||||||
const WORK_END_HOUR = 17;
|
"workStart": work_start,
|
||||||
const WORK_END_MINUTE = 30;
|
"workEnd": work_end,
|
||||||
|
"workDays": work_days,
|
||||||
|
"updateInterval": update_interval
|
||||||
|
} | tojson }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Application logic -->
|
||||||
|
<script>
|
||||||
|
const config = JSON.parse(
|
||||||
|
document.getElementById("app-config").textContent
|
||||||
|
);
|
||||||
|
|
||||||
|
const WORK_START_HOUR = config.workStart.hour;
|
||||||
|
const WORK_START_MINUTE = config.workStart.minute;
|
||||||
|
const WORK_END_HOUR = config.workEnd.hour;
|
||||||
|
const WORK_END_MINUTE = config.workEnd.minute;
|
||||||
|
|
||||||
|
const WORK_DAYS = config.workDays;
|
||||||
|
const UPDATE_INTERVAL = config.updateInterval;
|
||||||
|
|
||||||
|
const TOTAL_WORK_SECONDS =
|
||||||
|
(WORK_END_HOUR * 3600 + WORK_END_MINUTE * 60) -
|
||||||
|
(WORK_START_HOUR * 3600 + WORK_START_MINUTE * 60);
|
||||||
|
|
||||||
const TOTAL_WORK_MINUTES = 630; // 10.5 hours
|
|
||||||
const WORK_DAYS = [0, 1, 2, 3]; // Sunday → Wednesday
|
|
||||||
const WEEKLY_DAY_WEIGHT = 100 / WORK_DAYS.length;
|
const WEEKLY_DAY_WEIGHT = 100 / WORK_DAYS.length;
|
||||||
|
|
||||||
function updateProgress() {
|
function updateProgress() {
|
||||||
@@ -63,17 +83,21 @@
|
|||||||
dailyPercent = 100;
|
dailyPercent = 100;
|
||||||
statusText.textContent = "Workday complete 🎉";
|
statusText.textContent = "Workday complete 🎉";
|
||||||
} else {
|
} else {
|
||||||
const elapsedMinutes = Math.floor((now - start) / 60000);
|
const elapsedSeconds = (now - start) / 1000;
|
||||||
dailyPercent = Math.min((elapsedMinutes / TOTAL_WORK_MINUTES) * 100, 100);
|
|
||||||
|
dailyPercent = Math.min(
|
||||||
|
(elapsedSeconds / TOTAL_WORK_SECONDS) * 100,
|
||||||
|
100
|
||||||
|
);
|
||||||
statusText.textContent = "Grinding…";
|
statusText.textContent = "Grinding…";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply daily progress
|
// Daily progress
|
||||||
dailyFill.style.width = dailyPercent + "%";
|
dailyFill.style.width = dailyPercent + "%";
|
||||||
dailyPercentText.textContent = dailyPercent.toFixed(1) + "%";
|
dailyPercentText.textContent = dailyPercent.toFixed(4) + "%";
|
||||||
|
|
||||||
// Weekly progress
|
// Weekly progress
|
||||||
let completedDays = WORK_DAYS.filter(d => d < day).length;
|
const completedDays = WORK_DAYS.filter(d => d < day).length;
|
||||||
let weeklyPercent =
|
let weeklyPercent =
|
||||||
(completedDays * WEEKLY_DAY_WEIGHT) +
|
(completedDays * WEEKLY_DAY_WEIGHT) +
|
||||||
(isWorkday ? (dailyPercent / 100) * WEEKLY_DAY_WEIGHT : 0);
|
(isWorkday ? (dailyPercent / 100) * WEEKLY_DAY_WEIGHT : 0);
|
||||||
@@ -81,11 +105,11 @@
|
|||||||
weeklyPercent = Math.min(weeklyPercent, 100);
|
weeklyPercent = Math.min(weeklyPercent, 100);
|
||||||
|
|
||||||
weeklyFill.style.width = weeklyPercent + "%";
|
weeklyFill.style.width = weeklyPercent + "%";
|
||||||
weeklyPercentText.textContent = weeklyPercent.toFixed(1) + "%";
|
weeklyPercentText.textContent = weeklyPercent.toFixed(4) + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProgress();
|
updateProgress();
|
||||||
setInterval(updateProgress, 60 * 1000);
|
setInterval(updateProgress, UPDATE_INTERVAL);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user