Upload files to "sk1"
This commit is contained in:
parent
15c556eb7a
commit
13c1bdb99f
9
sk1/Dockerfile
Normal file
9
sk1/Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
RUN useradd -m appuser && chown -R appuser /app
|
||||
USER appuser
|
||||
EXPOSE 5000
|
||||
CMD ["gunicorn", "--workers", "2", "--bind", "0.0.0.0:5000", "--timeout", "60", "--access-logfile", "-", "app:app"]
|
||||
329
sk1/app.py
Normal file
329
sk1/app.py
Normal file
@ -0,0 +1,329 @@
|
||||
"""TaskFlow — Flask task management application."""
|
||||
import os
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
from flask import Flask, request, redirect, url_for, render_template_string, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def get_db():
|
||||
return psycopg2.connect(
|
||||
host=os.environ.get("POSTGRES_HOST"),
|
||||
port=int(os.environ.get("POSTGRES_PORT", 5432)),
|
||||
user=os.environ["POSTGRES_USER"],
|
||||
password=os.environ["POSTGRES_PASSWORD"],
|
||||
dbname=os.environ["POSTGRES_DB"],
|
||||
sslmode="require"
|
||||
)
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
try:
|
||||
conn = get_db(); conn.close()
|
||||
return jsonify({"status": "ok"}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"status": "error", "detail": str(e)}), 500
|
||||
|
||||
TEMPLATE = """
|
||||
<!doctype html><html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TaskFlow</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#0f1117;
|
||||
--surface:#1a1d27;
|
||||
--surface2:#21253a;
|
||||
--surface3:#272b3e;
|
||||
--border:#2e3350;
|
||||
--border2:#3a3f5c;
|
||||
--text:#e8eaf6;
|
||||
--muted:#7b82a8;
|
||||
--accent:#5b6af0;
|
||||
--accent2:#7b8af8;
|
||||
--todo:#7b82a8;
|
||||
--prog:#5b6af0;
|
||||
--done:#34d399;
|
||||
--danger:#f87171;
|
||||
--yellow:#fbbf24;
|
||||
}
|
||||
html{scroll-behavior:smooth}
|
||||
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||
|
||||
/* Header */
|
||||
header{
|
||||
background:var(--surface);
|
||||
border-bottom:1px solid var(--border);
|
||||
padding:1rem 2rem;
|
||||
display:flex;align-items:center;gap:1rem;
|
||||
position:sticky;top:0;z-index:100;
|
||||
}
|
||||
.logo{display:flex;align-items:center;gap:.6rem}
|
||||
.logo-icon{
|
||||
width:32px;height:32px;border-radius:8px;
|
||||
background:linear-gradient(135deg,var(--accent),var(--accent2));
|
||||
display:flex;align-items:center;justify-content:center;font-size:1rem;
|
||||
}
|
||||
.logo h1{font-size:1.1rem;font-weight:600;letter-spacing:-.01em}
|
||||
.header-right{margin-left:auto;display:flex;align-items:center;gap:1rem}
|
||||
.stat-pill{
|
||||
font-size:.72rem;font-weight:500;
|
||||
padding:.3rem .75rem;border-radius:999px;
|
||||
background:var(--surface2);border:1px solid var(--border);
|
||||
color:var(--muted);
|
||||
}
|
||||
.stat-pill span{color:var(--text);font-weight:600}
|
||||
|
||||
/* Main */
|
||||
main{padding:2rem;max-width:1280px;margin:0 auto}
|
||||
|
||||
/* Add task section */
|
||||
.add-section{
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:16px;
|
||||
padding:1.5rem;
|
||||
margin-bottom:2rem;
|
||||
}
|
||||
.add-section-title{
|
||||
font-size:.8rem;font-weight:600;text-transform:uppercase;
|
||||
letter-spacing:.08em;color:var(--muted);
|
||||
margin-bottom:1.25rem;display:flex;align-items:center;gap:.5rem;
|
||||
}
|
||||
.add-section-title::before{
|
||||
content:'';display:block;width:3px;height:.85rem;
|
||||
background:var(--accent);border-radius:2px;
|
||||
}
|
||||
.add-form{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-start}
|
||||
.add-form .field{flex:1;min-width:180px}
|
||||
.add-form .field.desc{flex:2;min-width:240px}
|
||||
.add-form label{
|
||||
display:block;font-size:.7rem;font-weight:500;
|
||||
color:var(--muted);margin-bottom:.35rem;
|
||||
}
|
||||
.add-form input,.add-form textarea{
|
||||
width:100%;padding:.6rem .875rem;
|
||||
background:var(--surface2);
|
||||
border:1px solid var(--border);
|
||||
border-radius:10px;
|
||||
font-size:.85rem;font-family:inherit;
|
||||
color:var(--text);outline:none;
|
||||
transition:border-color .15s,box-shadow .15s;
|
||||
}
|
||||
.add-form input:focus,.add-form textarea:focus{
|
||||
border-color:var(--accent);
|
||||
box-shadow:0 0 0 3px rgba(91,106,240,.18);
|
||||
}
|
||||
.add-form input::placeholder,.add-form textarea::placeholder{color:var(--muted);opacity:.6}
|
||||
.add-form textarea{resize:none;height:42px}
|
||||
.btn-add{
|
||||
align-self:flex-end;
|
||||
padding:.6rem 1.4rem;
|
||||
background:var(--accent);
|
||||
color:#fff;border:none;border-radius:10px;
|
||||
font-size:.85rem;font-weight:600;font-family:inherit;
|
||||
cursor:pointer;white-space:nowrap;
|
||||
transition:background .15s,transform .1s,box-shadow .15s;
|
||||
box-shadow:0 2px 12px rgba(91,106,240,.35);
|
||||
}
|
||||
.btn-add:hover{background:var(--accent2);box-shadow:0 4px 20px rgba(91,106,240,.45)}
|
||||
.btn-add:active{transform:scale(.97)}
|
||||
|
||||
/* Board */
|
||||
.board{display:grid;grid-template-columns:repeat(3,1fr);gap:1.25rem}
|
||||
@media(max-width:720px){.board{grid-template-columns:1fr}}
|
||||
|
||||
.column{
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:16px;
|
||||
overflow:hidden;
|
||||
display:flex;flex-direction:column;
|
||||
}
|
||||
.col-header{
|
||||
padding:.875rem 1rem;
|
||||
border-bottom:1px solid var(--border);
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
}
|
||||
.col-title{display:flex;align-items:center;gap:.5rem;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.07em}
|
||||
.col-dot{width:8px;height:8px;border-radius:50%}
|
||||
.col-todo .col-dot{background:var(--todo)}
|
||||
.col-todo .col-title{color:var(--todo)}
|
||||
.col-progress .col-dot{background:var(--prog)}
|
||||
.col-progress .col-title{color:var(--accent2)}
|
||||
.col-done .col-dot{background:var(--done)}
|
||||
.col-done .col-title{color:var(--done)}
|
||||
.col-count{
|
||||
font-size:.68rem;font-weight:600;
|
||||
padding:.15rem .5rem;border-radius:999px;
|
||||
background:var(--surface2);color:var(--muted);
|
||||
border:1px solid var(--border);
|
||||
}
|
||||
.tasks{padding:.75rem;display:flex;flex-direction:column;gap:.5rem;flex:1}
|
||||
|
||||
/* Task card */
|
||||
.task{
|
||||
background:var(--surface2);
|
||||
border:1px solid var(--border);
|
||||
border-radius:10px;
|
||||
padding:.875rem;
|
||||
transition:border-color .15s,transform .15s;
|
||||
}
|
||||
.task:hover{border-color:var(--border2);transform:translateY(-1px)}
|
||||
.col-done .task{border-left:2px solid var(--done);opacity:.75}
|
||||
.task-title{font-size:.83rem;font-weight:500;line-height:1.4;margin-bottom:.3rem;color:var(--text)}
|
||||
.task-desc{font-size:.75rem;color:var(--muted);line-height:1.5;margin-bottom:.625rem}
|
||||
.task-footer{display:flex;gap:.375rem;align-items:center}
|
||||
.btn{
|
||||
display:inline-flex;align-items:center;gap:.25rem;
|
||||
padding:.25rem .625rem;border-radius:7px;
|
||||
font-size:.72rem;font-weight:500;font-family:inherit;
|
||||
border:1px solid;cursor:pointer;
|
||||
transition:opacity .15s,background .15s;
|
||||
}
|
||||
.btn-advance{
|
||||
background:rgba(91,106,240,.15);
|
||||
border-color:rgba(91,106,240,.4);
|
||||
color:var(--accent2);
|
||||
}
|
||||
.btn-advance:hover{background:rgba(91,106,240,.25)}
|
||||
.btn-delete{
|
||||
background:transparent;
|
||||
border-color:rgba(248,113,113,.25);
|
||||
color:var(--danger);
|
||||
}
|
||||
.btn-delete:hover{background:rgba(248,113,113,.1)}
|
||||
.task-date{font-size:.68rem;color:var(--muted);margin-left:auto;opacity:.6}
|
||||
|
||||
/* Empty */
|
||||
.empty{text-align:center;padding:2rem .5rem;color:var(--muted);font-size:.78rem}
|
||||
.empty-icon{font-size:1.75rem;margin-bottom:.5rem;opacity:.25}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<div class="logo-icon">📋</div>
|
||||
<h1>TaskFlow</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="stat-pill">To Do <span>{{tasks['todo']|length}}</span></span>
|
||||
<span class="stat-pill">In Progress <span>{{tasks['in_progress']|length}}</span></span>
|
||||
<span class="stat-pill">Done <span>{{tasks['done']|length}}</span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Add task form at top -->
|
||||
<div class="add-section">
|
||||
<div class="add-section-title">New Task</div>
|
||||
<form method="post" action="/create">
|
||||
<div class="add-form">
|
||||
<div class="field">
|
||||
<label>Title</label>
|
||||
<input type="text" name="title" required placeholder="What needs to be done?">
|
||||
</div>
|
||||
<div class="field desc">
|
||||
<label>Description <span style="opacity:.5;font-weight:400">(optional)</span></label>
|
||||
<textarea name="description" placeholder="Add more details..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-add">+ Add Task</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Kanban board -->
|
||||
<div class="board">
|
||||
{% for col,label,css,icon in [
|
||||
('todo','To Do','col-todo','◯'),
|
||||
('in_progress','In Progress','col-progress','▶'),
|
||||
('done','Done','col-done','✓')] %}
|
||||
<div class="column {{css}}">
|
||||
<div class="col-header">
|
||||
<div class="col-title">
|
||||
<span class="col-dot"></span>{{icon}} {{label}}
|
||||
</div>
|
||||
<span class="col-count">{{tasks[col]|length}}</span>
|
||||
</div>
|
||||
<div class="tasks">
|
||||
{% if tasks[col] %}
|
||||
{% for t in tasks[col] %}
|
||||
<div class="task">
|
||||
<div class="task-title">{{t.title}}</div>
|
||||
{% if t.description %}<div class="task-desc">{{t.description}}</div>{% endif %}
|
||||
<div class="task-footer">
|
||||
{% if col != 'done' %}
|
||||
<form method="post" action="/advance/{{t.id}}">
|
||||
<button class="btn btn-advance">▶ Advance</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/delete/{{t.id}}">
|
||||
<button class="btn btn-delete">✕</button>
|
||||
</form>
|
||||
<span class="task-date">{{t.created_at.strftime('%b %d') if t.created_at else ''}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty">
|
||||
<div class="empty-icon">▫</div>
|
||||
Nothing here yet
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
</body></html>
|
||||
"""
|
||||
NEXT_STATUS = {"todo": "in_progress", "in_progress": "done"}
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
conn = get_db()
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("SELECT * FROM tasks ORDER BY created_at")
|
||||
rows = cur.fetchall()
|
||||
cur.close(); conn.close()
|
||||
tasks = {"todo": [], "in_progress": [], "done": []}
|
||||
for row in rows:
|
||||
tasks[row["status"]].append(row)
|
||||
return render_template_string(TEMPLATE, tasks=tasks)
|
||||
|
||||
@app.route("/create", methods=["POST"])
|
||||
def create():
|
||||
title = request.form.get("title", "").strip()
|
||||
desc = request.form.get("description", "").strip()
|
||||
if title:
|
||||
conn = get_db(); cur = conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO tasks (title, description, status) VALUES (%s, %s, 'todo')",
|
||||
(title, desc or None))
|
||||
conn.commit(); cur.close(); conn.close()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@app.route("/advance/<int:task_id>", methods=["POST"])
|
||||
def advance(task_id):
|
||||
conn = get_db(); cur = conn.cursor()
|
||||
cur.execute("SELECT status FROM tasks WHERE id = %s", (task_id,))
|
||||
row = cur.fetchone()
|
||||
if row and row[0] in NEXT_STATUS:
|
||||
cur.execute("UPDATE tasks SET status = %s WHERE id = %s",
|
||||
(NEXT_STATUS[row[0]], task_id))
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@app.route("/delete/<int:task_id>", methods=["POST"])
|
||||
def delete(task_id):
|
||||
conn = get_db(); cur = conn.cursor()
|
||||
cur.execute("DELETE FROM tasks WHERE id = %s", (task_id,))
|
||||
conn.commit(); cur.close(); conn.close()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||
27
sk1/init.sql
Normal file
27
sk1/init.sql
Normal file
@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'todo'
|
||||
CHECK (status IN ('todo', 'in_progress', 'done')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN NEW.updated_at = NOW(); RETURN NEW; END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS set_updated_at ON tasks;
|
||||
CREATE TRIGGER set_updated_at
|
||||
BEFORE UPDATE ON tasks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
INSERT INTO tasks (title, description, status) VALUES
|
||||
('Set up Azure resources', 'Create App Service and PostgreSQL Flexible Server', 'done'),
|
||||
('Build Docker image', 'Push image to Azure Container Registry', 'done'),
|
||||
('Configure App Service', 'Set environment variables and container settings', 'in_progress'),
|
||||
('Enable HTTPS', 'Built-in on azurewebsites.net — automatic', 'todo'),
|
||||
('Write README', 'Document deployment and cost analysis', 'todo')
|
||||
ON CONFLICT DO NOTHING;
|
||||
3
sk1/requirements.txt
Normal file
3
sk1/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
flask==3.0.3
|
||||
gunicorn==21.2.0
|
||||
psycopg2-binary==2.9.9
|
||||
Loading…
Reference in New Issue
Block a user