#!/usr/bin/env bash
# =============================================================================
# setup.sh — TaskFlow — Azure App Service + PostgreSQL Flexible Server
#
# Architecture:
# - Azure App Service (Free F1) — runs the Flask container
# - Azure Database for PostgreSQL Flexible Server (Burstable B1ms)
# - Azure Container Registry — stores the Docker image
# - HTTPS via built-in azurewebsites.net certificate (free, automatic)
#
# Usage:
# export CERT_EMAIL="you@student.tuke.sk"
# export DB_PASSWORD="YourStrongPassword123!"
# chmod +x setup.sh && ./setup.sh
# =============================================================================
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
RG="taskflow-rg"
LOCATION="francecentral"
ACR_NAME="taskflowregistry${RANDOM}"
APP_PLAN="taskflow-plan"
APP_NAME="taskflow-${RANDOM}"
DB_SERVER="taskflow-db-${RANDOM}"
DB_USER="taskuser"
DB_NAME="taskdb"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'
BOLD='\033[1m'; RESET='\033[0m'; RED='\033[0;31m'
step() { echo -e "\n${CYAN}${BOLD}[SETUP]${RESET} $*"; }
ok() { echo -e " ${GREEN}✓${RESET} $*"; }
warn() { echo -e " ${YELLOW}!${RESET} $*"; }
die() { echo -e "\n${RED}${BOLD}[ERROR]${RESET} $*" >&2; exit 1; }
echo -e "\n${BOLD}========================================"
echo -e " TaskFlow — Azure App Service"
echo -e "========================================${RESET}"
echo -e " Resource Group : ${RG}"
echo -e " Location : ${LOCATION}"
echo -e " App Service : ${APP_NAME}.azurewebsites.net"
echo -e "${BOLD}========================================${RESET}\n"
# ---------------------------------------------------------------------------
# 0. Validate
# ---------------------------------------------------------------------------
step "Checking prerequisites ..."
command -v az &>/dev/null || die "Azure CLI not found."
command -v docker &>/dev/null || die "Docker not found."
[[ -z "${DB_PASSWORD:-}" ]] && die "DB_PASSWORD not set.\n export DB_PASSWORD=YourPassword123!"
az account show &>/dev/null || die "Not logged in. Run: az login"
ok "All prerequisites met"
# ---------------------------------------------------------------------------
# 1. Write all application files
# ---------------------------------------------------------------------------
step "Writing application files ..."
mkdir -p "${SCRIPT_DIR}/app" "${SCRIPT_DIR}/db"
cat > "${SCRIPT_DIR}/app/app.py" << 'EOF'
"""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 = """
TaskFlow
📋 TaskFlow
{% for col,label,css in [('todo','To Do','col-todo'),
('in_progress','In Progress','col-progress'),
('done','Done','col-done')] %}
{{label}} ({{tasks[col]|length}})
{% for t in tasks[col] %}
{{t.title}}
{% if t.description %}
{{t.description}}
{% endif %}
{% if col != 'done' %}
{% endif %}
{% endfor %}
{% endfor %}
"""
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/", 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/", 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)
EOF
ok "app/app.py"
cat > "${SCRIPT_DIR}/app/requirements.txt" << 'EOF'
flask==3.0.3
gunicorn==21.2.0
psycopg2-binary==2.9.9
EOF
ok "app/requirements.txt"
cat > "${SCRIPT_DIR}/app/Dockerfile" << 'EOF'
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"]
EOF
ok "app/Dockerfile"
cat > "${SCRIPT_DIR}/db/init.sql" << 'EOF'
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;
EOF
ok "db/init.sql"
# ---------------------------------------------------------------------------
# backup.sh
# ---------------------------------------------------------------------------
cat > "${SCRIPT_DIR}/backup.sh" << 'EOF'
#!/usr/bin/env bash
# backup.sh — TaskFlow — PostgreSQL backup and restore via Azure CLI
set -euo pipefail
BACKUP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/backups"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_FILE="${BACKUP_DIR}/taskflow-${TIMESTAMP}.sql"
# Load config from .env if present
[ -f "$(dirname "${BASH_SOURCE[0]}")/.env" ] && source "$(dirname "${BASH_SOURCE[0]}")/.env"
[[ -z "${POSTGRES_HOST:-}" ]] && echo "Set POSTGRES_HOST" && exit 1
[[ -z "${POSTGRES_USER:-}" ]] && echo "Set POSTGRES_USER" && exit 1
[[ -z "${POSTGRES_PASSWORD:-}" ]] && echo "Set POSTGRES_PASSWORD" && exit 1
[[ -z "${POSTGRES_DB:-}" ]] && echo "Set POSTGRES_DB" && exit 1
if [[ "${1:-}" == "--restore" ]]; then
FILE="${2:-}"; [[ -z "$FILE" ]] && echo "Usage: ./backup.sh --restore " && exit 1
PGPASSWORD="${POSTGRES_PASSWORD}" psql \
-h "${POSTGRES_HOST}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
--set=sslmode=require < "$FILE"
echo "Restored from: $FILE"
exit 0
fi
mkdir -p "$BACKUP_DIR"
PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump \
-h "${POSTGRES_HOST}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
--clean --if-exists --no-password \
> "$BACKUP_FILE"
echo "Backup saved: $BACKUP_FILE"
ls -1t "$BACKUP_DIR"/taskflow-*.sql 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null || true
EOF
chmod +x "${SCRIPT_DIR}/backup.sh"
ok "backup.sh"
# ---------------------------------------------------------------------------
# .gitignore
# ---------------------------------------------------------------------------
cat > "${SCRIPT_DIR}/.gitignore" << 'EOF'
.env
backups/
__pycache__/
*.py[cod]
*.swp
.DS_Store
EOF
ok ".gitignore"
# ---------------------------------------------------------------------------
# .env.example
# ---------------------------------------------------------------------------
cat > "${SCRIPT_DIR}/.env.example" << 'EOF'
# Copy to .env and fill in real values — never commit .env to GIT
POSTGRES_HOST=taskflow-db-XXXXX.postgres.database.azure.com
POSTGRES_USER=taskuser
POSTGRES_PASSWORD=changeme
POSTGRES_DB=taskdb
EOF
ok ".env.example"
ok "All files written"
# ---------------------------------------------------------------------------
# 2. Azure infrastructure
# ---------------------------------------------------------------------------
step "Creating Resource Group '${RG}' in ${LOCATION} ..."
az group create --name "${RG}" --location "${LOCATION}" --output none
ok "Resource group ready"
# ---------------------------------------------------------------------------
# 3. Azure Container Registry
# ---------------------------------------------------------------------------
step "Creating Azure Container Registry '${ACR_NAME}' ..."
az acr create \
--resource-group "${RG}" \
--name "${ACR_NAME}" \
--sku Basic \
--admin-enabled true \
--output none
ok "ACR created: ${ACR_NAME}.azurecr.io"
step "Building and pushing Docker image ..."
az acr login --name "${ACR_NAME}"
docker build -t "${ACR_NAME}.azurecr.io/taskflow-web:latest" "${SCRIPT_DIR}/app"
docker push "${ACR_NAME}.azurecr.io/taskflow-web:latest"
ok "Image pushed to ACR"
step "Fetching ACR credentials ..."
for i in {1..12}; do
ACR_PASSWORD=$(az acr credential show --name "${ACR_NAME}" --resource-group "${RG}" --query "passwords[0].value" -o tsv 2>/dev/null || true)
[[ -n "${ACR_PASSWORD}" ]] && break
echo " waiting for ACR to be ready (${i}/12)..."; sleep 10
done
[[ -z "${ACR_PASSWORD}" ]] && die "Could not fetch ACR credentials after 2 minutes."
# ---------------------------------------------------------------------------
# 4. PostgreSQL Flexible Server
# ---------------------------------------------------------------------------
step "Creating PostgreSQL Flexible Server '${DB_SERVER}' (takes ~3 min) ..."
az postgres flexible-server create \
--resource-group "${RG}" \
--name "${DB_SERVER}" \
--location "${LOCATION}" \
--admin-user "${DB_USER}" \
--admin-password "${DB_PASSWORD}" \
--sku-name "Standard_B1ms" \
--tier "Burstable" \
--storage-size 32 \
--version 15 \
--public-access "All" \
--output none
ok "PostgreSQL server created: ${DB_SERVER}.postgres.database.azure.com"
step "Creating database '${DB_NAME}' ..."
az postgres flexible-server db create \
--resource-group "${RG}" \
--server-name "${DB_SERVER}" \
--database-name "${DB_NAME}" \
--output none
ok "Database '${DB_NAME}' created"
step "Initialising database schema ..."
DB_HOST="${DB_SERVER}.postgres.database.azure.com"
PGPASSWORD="${DB_PASSWORD}" psql \
"host=${DB_HOST} user=${DB_USER} dbname=${DB_NAME} sslmode=require" \
-f "${SCRIPT_DIR}/db/init.sql"
ok "Schema and sample data loaded"
# Write .env file with real values
cat > "${SCRIPT_DIR}/.env" << EOF
POSTGRES_HOST=${DB_HOST}
POSTGRES_USER=${DB_USER}
POSTGRES_PASSWORD=${DB_PASSWORD}
POSTGRES_DB=${DB_NAME}
EOF
ok ".env written with database credentials"
# ---------------------------------------------------------------------------
# 5. App Service Plan + Web App
# ---------------------------------------------------------------------------
step "Creating App Service Plan (Linux, Free F1) ..."
az appservice plan create \
--name "${APP_PLAN}" \
--resource-group "${RG}" \
--location "${LOCATION}" \
--sku B1 \
--is-linux \
--output none
ok "App Service Plan ready"
step "Creating Web App '${APP_NAME}' with container ..."
az webapp create \
--resource-group "${RG}" \
--plan "${APP_PLAN}" \
--name "${APP_NAME}" \
--container-image-name "${ACR_NAME}.azurecr.io/taskflow-web:latest" \
--container-registry-url "https://${ACR_NAME}.azurecr.io" \
--container-registry-user "${ACR_NAME}" \
--container-registry-password "${ACR_PASSWORD}" \
--output none
ok "Web App created"
step "Configuring environment variables (secrets) ..."
az webapp config appsettings set \
--resource-group "${RG}" \
--name "${APP_NAME}" \
--settings \
POSTGRES_HOST="${DB_HOST}" \
POSTGRES_USER="${DB_USER}" \
POSTGRES_PASSWORD="${DB_PASSWORD}" \
POSTGRES_DB="${DB_NAME}" \
POSTGRES_PORT="5432" \
FLASK_ENV="production" \
WEBSITES_PORT="5000" \
--output none
ok "Environment variables configured"
step "Enabling HTTPS only ..."
az webapp update \
--resource-group "${RG}" \
--name "${APP_NAME}" \
--https-only true \
--output none
ok "HTTPS enforced"
step "Restarting web app ..."
az webapp restart --resource-group "${RG}" --name "${APP_NAME}" --output none
ok "Web app restarted"
APP_URL="https://${APP_NAME}.azurewebsites.net"
# ---------------------------------------------------------------------------
# Write prepare-app.sh and remove-app.sh
# ---------------------------------------------------------------------------
cat > "${SCRIPT_DIR}/prepare-app.sh" << PREPEOF
#!/usr/bin/env bash
# prepare-app.sh — Re-deploy TaskFlow to Azure App Service
# Run setup.sh for first-time deployment.
# Use this to rebuild and redeploy the container after code changes.
set -euo pipefail
source "\$(dirname "\${BASH_SOURCE[0]}")/.env"
RG="taskflow-rg"
ACR_NAME="${ACR_NAME}"
APP_NAME="${APP_NAME}"
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
echo "Rebuilding and redeploying TaskFlow..."
az acr login --name "\${ACR_NAME}"
docker build -t "\${ACR_NAME}.azurecr.io/taskflow-web:latest" "\${SCRIPT_DIR}/app"
docker push "\${ACR_NAME}.azurecr.io/taskflow-web:latest"
az webapp restart --resource-group "\${RG}" --name "\${APP_NAME}" --output none
echo "Done. App is live at: https://\${APP_NAME}.azurewebsites.net"
PREPEOF
chmod +x "${SCRIPT_DIR}/prepare-app.sh"
cat > "${SCRIPT_DIR}/remove-app.sh" << 'REMOVEEOF'
#!/usr/bin/env bash
# remove-app.sh — Delete all TaskFlow Azure resources
set -euo pipefail
RG="taskflow-rg"
echo "WARNING: This permanently deletes resource group '${RG}' and everything inside."
read -r -p "Type 'yes' to confirm: " CONFIRM
[[ "${CONFIRM}" != "yes" ]] && echo "Aborted." && exit 0
az group delete --name "${RG}" --yes --no-wait
echo "Deletion started. All TaskFlow Azure resources are being removed."
REMOVEEOF
chmod +x "${SCRIPT_DIR}/remove-app.sh"
# ---------------------------------------------------------------------------
# Write README.md
# ---------------------------------------------------------------------------
cat > "${SCRIPT_DIR}/README.md" << READMEEOF
# TaskFlow — Cloud Deployment (sk1)
> A task management web application deployed on Microsoft Azure.
> Live at: **https://${APP_NAME}.azurewebsites.net**
---
## 1. What the Application Does
TaskFlow is a simple web-based task manager for individuals or small teams.
Users can:
- **Create** tasks with a title and optional description
- **Advance** tasks through three stages: \`To Do → In Progress → Done\`
- **Delete** tasks they no longer need
All data is stored in a managed PostgreSQL database and persists across restarts and redeployments.
---
## 2. Public Cloud and Infrastructure Description
### Cloud Provider
**Microsoft Azure** — Azure for Students subscription.
### Azure Services Used
| Service | Purpose |
|---|---|
| **Azure Container Registry (ACR)** — Basic | Stores the Docker image for the Flask app |
| **Azure App Service Plan** — Linux B1 | Managed hosting platform for the container |
| **Azure Web App** (container) | Runs the Flask app with built-in HTTPS and auto-restart |
| **Azure Database for PostgreSQL Flexible Server** — Burstable B1ms | Managed PostgreSQL 15 database with persistent storage |
### Components
| Component | Type | Role |
|---|---|---|
| \`taskflow-web\` | Docker container on App Service | Python 3.11 / Flask app served by Gunicorn |
| \`postgres\` | Azure managed database | PostgreSQL 15 — stores all tasks |
| \`taskflow-registry\` | Azure Container Registry | Private Docker image registry |
### How Traffic Flows
\`\`\`
Browser (HTTPS)
→ Azure App Service (built-in TLS, azurewebsites.net cert)
→ Flask container (Gunicorn :5000)
→ Azure PostgreSQL Flexible Server (:5432, SSL required)
→ Managed persistent storage (Azure-managed disk)
\`\`\`
### HTTPS / TLS
Azure App Service provides a **free, automatic, browser-trusted TLS certificate** for all \`*.azurewebsites.net\` domains. HTTPS-only mode is enforced — HTTP requests are automatically redirected to HTTPS.
### Persistent Storage
Data is stored in **Azure Database for PostgreSQL Flexible Server**. This is a fully managed service — Azure handles backups, high availability, and storage automatically. Data persists independently of the application container.
### Automatic Restart
Azure App Service automatically restarts the container if it crashes or exits. The platform monitors the \`/health\` endpoint and restarts unhealthy instances without manual intervention.
### Configuration
All secrets (database host, user, password) are stored as **App Service Application Settings** — Azure's equivalent of environment variables. They are injected into the container at runtime and never stored in code or GIT.
---
## 3. Cost Analysis — 1,000 Users/Day, 50 GB Data (Azure, 1 Year)
| Resource | Specification | Unit Price | Billing | Annual Cost |
|---|---|---|---|---|
| App Service Plan | Linux B1 (1 vCPU, 1.75 GB RAM) | ~\$13.14/month | Monthly | **~\$157.68** |
| PostgreSQL Flexible Server | Burstable B1ms (1 vCPU, 2 GB) | ~\$12.41/month | Monthly | **~\$148.92** |
| PostgreSQL Storage | 50 GB | ~\$5.76/month | Monthly | **~\$69.12** |
| Azure Container Registry | Basic tier | ~\$5.00/month | Monthly | **~\$60.00** |
| Outbound bandwidth | First 100 GB free, ~60 GB/month at 1k users | \$0 | Per GB | **~\$0** |
| HTTPS certificate | Free (azurewebsites.net) | \$0 | — | **\$0** |
| **Total** | | | | **≈ \$435.72 / year** |
> For the exam demo period the actual cost is ~\$35/month (B1 plan + B1ms PostgreSQL). Delete resources after the exam with \`remove-app.sh\` to stop billing.
---
## 4. Uploaded Files and Their Content
\`\`\`
sk1/
├── app/
│ ├── app.py Flask app — routes, DB connection, Kanban UI, /health endpoint
│ ├── Dockerfile Builds taskflow-web image (Python 3.11, Gunicorn, non-root)
│ └── requirements.txt Python deps: flask, gunicorn, psycopg2-binary
├── db/
│ └── init.sql SQL schema — tasks table, trigger, sample data
├── setup.sh Full deployment — writes files, creates all Azure resources
├── prepare-app.sh Rebuilds and redeploys container after code changes
├── remove-app.sh Deletes entire Azure resource group
├── backup.sh pg_dump backup and restore via psql
├── .env.example Template for required environment variables (no secrets)
├── .gitignore Excludes .env and backups from GIT
└── README.md This file
\`\`\`
---
## 5. Configuration Description
### Secrets and Environment Variables
Secrets are stored as **Azure App Service Application Settings** — never in GIT.
| Variable | Where stored | Used by |
|---|---|---|
| \`POSTGRES_HOST\` | App Service settings | Flask app |
| \`POSTGRES_USER\` | App Service settings | Flask app |
| \`POSTGRES_PASSWORD\` | App Service settings | Flask app |
| \`POSTGRES_DB\` | App Service settings | Flask app |
| \`POSTGRES_PORT\` | App Service settings | Flask app |
| \`FLASK_ENV\` | App Service settings | Gunicorn |
| \`WEBSITES_PORT\` | App Service settings | Azure (tells it app runs on 5000) |
| \`DB_PASSWORD\` | Shell env var at deploy time | \`setup.sh\` only, not stored |
A local \`.env\` file is written during deployment for use by \`backup.sh\`. It is excluded from GIT by \`.gitignore\`.
---
## 6. How to View and Use the Application
### Prerequisites
- Azure CLI installed and \`az login\` completed
- Docker installed and running
- \`psql\` installed (for backup/restore and schema init)
- \`DB_PASSWORD\` environment variable set
### Deploy (first time — creates all Azure resources)
\`\`\`bash
export DB_PASSWORD="YourStrongPassword123!"
chmod +x setup.sh && ./setup.sh
\`\`\`
Takes approximately **5–8 minutes**.
### Open in Browser
\`\`\`
https://${APP_NAME}.azurewebsites.net
\`\`\`
### Redeploy after code changes
\`\`\`bash
./prepare-app.sh
\`\`\`
### Remove all Azure resources
\`\`\`bash
./remove-app.sh
\`\`\`
---
## 7. How to Perform a Data Backup
Install psql if needed: \`sudo apt install postgresql-client\`
\`\`\`bash
# Create a timestamped backup
source .env && ./backup.sh
# List backups
ls -lh backups/
# Restore
source .env && ./backup.sh --restore backups/taskflow-20250601-120000.sql
\`\`\`
---
## 8. How to View Access Logs from the Internet
Azure App Service logs all HTTP requests. View them via Azure CLI:
\`\`\`bash
# Stream live access logs (all internet requests to the app)
az webapp log tail --resource-group taskflow-rg --name ${APP_NAME}
# Enable detailed logging first if needed
az webapp log config \
--resource-group taskflow-rg \
--name ${APP_NAME} \
--application-logging filesystem \
--web-server-logging filesystem \
--level information
# Download logs as zip
az webapp log download --resource-group taskflow-rg --name ${APP_NAME}
\`\`\`
Each log entry contains: timestamp, client IP, HTTP method, path, status code, response time.
---
## 9. Conditions for Running prepare-app.sh and remove-app.sh
### prepare-app.sh
- Azure CLI installed and \`az login\` completed
- Docker installed and running
- \`.env\` file must exist in the same directory (created by \`setup.sh\`)
- ACR name and App name are hardcoded from the initial deployment — re-run \`setup.sh\` to create fresh resources
### remove-app.sh
- Azure CLI installed and \`az login\` completed
- Resource group \`taskflow-rg\` must exist
- Prompts for confirmation before deleting
- All data will be lost unless \`backup.sh\` was run first
---
## 10. External Resources and Use of Generative AI
### External Resources
| Resource | Type | How Used |
|---|---|---|
| [Azure App Service Documentation](https://learn.microsoft.com/en-us/azure/app-service/) | Official docs | Web app creation, container config, app settings, logging |
| [Azure PostgreSQL Flexible Server](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/) | Official docs | Server creation, firewall, connection strings |
| [Azure Container Registry](https://learn.microsoft.com/en-us/azure/container-registry/) | Official docs | Image push, admin credentials |
| [Flask Documentation](https://flask.palletsprojects.com/) | Official docs | Routing, templates |
| [PostgreSQL pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) | Official docs | Backup commands |
| [Azure Pricing Calculator](https://azure.microsoft.com/pricing/calculator/) | Tool | Cost analysis |
| Course lectures (azure, certbot, compose) | Lecture slides | Architecture, PaaS concepts, IaaS vs PaaS |
### Generative AI Usage
**Tool:** Claude by Anthropic (claude.ai)
**Type:** Productivity assistance — Azure CLI commands, shell scripts, Dockerfile, Flask app, README.
**Method:** Student described requirements, reviewed all output, verified understanding before submission. All architectural decisions directed by the student. AI accelerated writing; understanding was the student's own.
READMEEOF
ok "README.md"
# ---------------------------------------------------------------------------
# Done!
# ---------------------------------------------------------------------------
echo ""
echo -e "${BOLD}=================================================${RESET}"
echo -e "${GREEN}${BOLD} TaskFlow is LIVE!${RESET}"
echo -e "${BOLD}=================================================${RESET}"
echo -e " ${BOLD}URL:${RESET} ${GREEN}${APP_URL}${RESET}"
echo -e " ${BOLD}Database:${RESET} ${DB_SERVER}.postgres.database.azure.com"
echo -e " ${BOLD}Registry:${RESET} ${ACR_NAME}.azurecr.io"
echo -e ""
echo -e " Logs: az webapp log tail --resource-group ${RG} --name ${APP_NAME}"
echo -e " Backup: source .env && ./backup.sh"
echo -e " Remove: ./remove-app.sh"
echo -e "${BOLD}=================================================${RESET}"