Upload files to "sk1"

This commit is contained in:
Mohammed Niaz Khaleel Jameel 2026-05-20 08:14:48 +00:00
parent 15c556eb7a
commit 13c1bdb99f
4 changed files with 368 additions and 0 deletions

9
sk1/Dockerfile Normal file
View 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
View 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">&#128203;</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">&#43; Add Task</button>
</div>
</form>
</div>
<!-- Kanban board -->
<div class="board">
{% for col,label,css,icon in [
('todo','To Do','col-todo','&#9711;'),
('in_progress','In Progress','col-progress','&#9654;'),
('done','Done','col-done','&#10003;')] %}
<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">&#9654; Advance</button>
</form>
{% endif %}
<form method="post" action="/delete/{{t.id}}">
<button class="btn btn-delete">&#10005;</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">&#9643;</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
View 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
View File

@ -0,0 +1,3 @@
flask==3.0.3
gunicorn==21.2.0
psycopg2-binary==2.9.9