diff --git a/SK1/README.md b/SK1/README.md
new file mode 100644
index 0000000..b12aa32
--- /dev/null
+++ b/SK1/README.md
@@ -0,0 +1,269 @@
+# π PasteVault
+
+A secure, self-hosted code and text sharing tool with syntax highlighting, expiring links, and optional password protection. Think Pastebin β but private, expiring, and deployed on Azure.
+
+---
+
+## What the Application Does
+
+PasteVault allows users to:
+- Paste code or text and receive a unique shareable URL
+- Select a programming language for syntax highlighting (15 languages supported)
+- Set an expiry time (1 hour, 1 day, 7 days, 30 days, or never)
+- Optionally password-protect a paste
+- Share the link β only people with the URL (and password if set) can view it
+- Copy paste content or the share URL with one click
+- View metadata: creation time, expiry countdown, view count
+
+Pastes are automatically deleted from the database when they expire.
+
+---
+
+## Architecture Overview
+
+The application uses a **hybrid cloud architecture**:
+
+- **Azure VM** (`Standard_D2s_v3` β 2 vCPU, 8 GB RAM, West Europe) runs all application containers via Docker Compose
+- **Azure PostgreSQL Flexible Server** (`Standard_B1MS`, North Europe) stores all paste data persistently
+- **Azure Blob Storage** (North Europe) stores database backup dumps
+- **Cloudflare** (external, pre-existing) provides DNS, HTTPS/TLS termination, and access logs
+
+### Containers (3 total)
+
+| Container | Image | Role |
+|---|---|---|
+| `pastevault-frontend` | Custom (Nginx + React) | Serves the UI and proxies `/api/` to backend |
+| `pastevault-backend` | Custom (Python FastAPI) | REST API β paste CRUD, expiry logic |
+| `pastevault-redis` | `redis:7-alpine` | Caching paste reads, reducing DB load |
+
+### Communication Flow
+
+```
+Browser β Cloudflare (HTTPS) β VM port 80 β Nginx (frontend container)
+ β /api/* proxy
+ FastAPI (backend container)
+ β
+ Redis (cache, 30s TTL)
+ β cache miss
+ Azure PostgreSQL (persistent store)
+```
+
+---
+
+## Cloud Services Used
+
+| Service | Purpose | Tier |
+|---|---|---|
+| Azure VM `Standard_D2s_v3` | Runs Docker Compose with all 3 containers | Pay-as-you-go |
+| Azure PostgreSQL Flexible Server | Managed relational database | Burstable B1MS (free tier) |
+| Azure Blob Storage | Backup storage for `pg_dump` files | Standard LRS |
+| Cloudflare (pre-existing) | DNS, HTTPS certificate, access logs | Free plan |
+
+### Persistent Storage
+- **PostgreSQL** stores all paste records (id, title, content, language, expiry, views, password hash)
+- **Redis Docker volume** (`redis_data`) persists cache data across restarts using `--appendonly yes`
+- **Azure Blob Storage** holds scheduled database backups
+
+### Auto-restart
+All three containers use `restart: always` in `docker-compose.yml`. If any container crashes, Docker restarts it automatically without any manual intervention.
+
+---
+
+## File Structure
+
+```
+sk1/
+βββ backend/
+β βββ main.py # FastAPI app β all API endpoints, DB logic, Redis caching
+β βββ requirements.txt # Python dependencies
+β βββ Dockerfile # Python 3.12 slim image
+βββ frontend/
+β βββ src/
+β β βββ main.jsx # React entry point
+β β βββ App.jsx # Router β two routes: / and /paste/:id
+β β βββ CreatePaste.jsx # Paste creation form
+β β βββ ViewPaste.jsx # Paste viewer with syntax highlighting
+β β βββ index.css # Dark theme stylesheet
+β βββ index.html # HTML shell
+β βββ package.json # Node dependencies (React, Vite, highlight.js)
+β βββ vite.config.js # Vite build config with API proxy for local dev
+β βββ nginx.conf # Nginx config β serves React, proxies /api/ to backend
+β βββ Dockerfile # Multi-stage: Node build β Nginx serve
+βββ scripts/
+β βββ prepare-app.sh # Full Azure provisioning + deployment script
+β βββ remove-app.sh # Teardown script β stops containers + deletes all Azure resources
+β βββ backup.sh # pg_dump β Azure Blob Storage
+βββ docker-compose.yml # Defines frontend, backend, redis services
+βββ .env.example # Template for environment variables (safe to commit)
+βββ .gitignore # Excludes .env, node_modules, __pycache__, dist
+βββ README.md # This file
+```
+
+---
+
+## Configuration
+
+All configuration is done via environment variables in the `.env` file. This file is **never committed to Git**.
+
+| Variable | Description |
+|---|---|
+| `DATABASE_URL` | PostgreSQL connection string (without `?ssl=`) |
+| `REDIS_URL` | Redis connection string (default: `redis://redis:6379`) |
+| `SECRET_KEY` | Used to salt password hashes |
+| `DB_PASS` | PostgreSQL admin password (used by backup script) |
+
+Copy `.env.example` to `.env` and fill in your values before running any scripts.
+
+---
+
+## How to Run the Application
+
+### Prerequisites
+- Azure CLI installed and logged in (`az login`)
+- SSH key at `~/.ssh/id_rsa`
+- `.env` file filled in
+
+### Deploy
+```bash
+cd sk1/
+bash scripts/prepare-app.sh
+```
+
+This script automatically:
+1. Creates the Azure resource group
+2. Provisions the VM (Standard_D2s_v3, West Europe)
+3. Creates PostgreSQL Flexible Server (B1MS, North Europe)
+4. Creates Azure Blob Storage for backups
+5. Installs Docker on the VM
+6. Copies app files to the VM
+7. Builds and starts all containers
+
+### Teardown
+```bash
+bash scripts/remove-app.sh
+```
+
+Stops all containers and deletes the entire Azure resource group (VM, database, storage, IP).
+
+### Conditions to run scripts
+- `prepare-app.sh`: Azure CLI logged in, `.env` present, SSH key at `~/.ssh/id_rsa`, no existing `pastevault-rg` resource group
+- `remove-app.sh`: Azure CLI logged in, SSH access to VM still working
+
+---
+
+## Using the Application
+
+1. Open the app URL in a browser (`https://yourdomain.com`)
+2. Enter a title, paste your code or text into the editor
+3. Select the language for syntax highlighting
+4. Choose an expiry time (or never)
+5. Optionally set a password
+6. Click **"π Create Paste"**
+7. You are redirected to the paste URL β copy it with the **"π Share"** button
+8. Anyone with the link can view the paste (and password if set)
+
+---
+
+## Backup
+
+Run from the project root on your local machine (requires `postgresql-client`):
+
+```bash
+bash scripts/backup.sh
+```
+
+This dumps the entire `pastevault` database with `pg_dump` and uploads it to Azure Blob Storage.
+
+**List all backups:**
+```bash
+az storage blob list \
+ --account-name pastevaultstorage \
+ --container-name backups \
+ --output table
+```
+
+**Download a specific backup:**
+```bash
+az storage blob download \
+ --account-name pastevaultstorage \
+ --container-name backups \
+ --name pastevault_backup_YYYYMMDD_HHMMSS.sql \
+ --file ./restore.sql
+```
+
+**Restore from backup:**
+```bash
+PGPASSWORD=yourpassword psql \
+ --host=pastevault-db.postgres.database.azure.com \
+ --username=pvadmin \
+ --dbname=pastevault \
+ --file=./restore.sql \
+ --sslmode=require
+```
+
+---
+
+## Viewing Access Logs
+
+### From Cloudflare (internet traffic)
+Cloudflare dashboard β your domain β **Analytics** tab shows:
+- Total requests, unique visitors
+- Geographic breakdown
+- Traffic over time
+- Blocked threats
+
+### From Nginx (per-request logs)
+```bash
+ssh azureuser@YOUR_VM_IP
+docker logs pastevault-frontend
+```
+
+Each line shows: IP address, timestamp, HTTP method, path, status code, response size.
+
+### From the backend
+```bash
+docker logs pastevault-backend
+```
+
+### Live log stream
+```bash
+docker logs -f pastevault-frontend
+```
+
+---
+
+## Cost Analysis β 1000 Users/Day, 50 GB Data
+
+| Resource | Spec | Billing | Monthly | Annual |
+|---|---|---|---|---|
+| Azure VM `Standard_D2s_v3` | 2 vCPU, 8 GB, West Europe | Per hour | ~$7.30 | ~$87.60 |
+| Azure PostgreSQL B1MS | 1 vCPU, 2 GB, 50 GB storage | Per hour + GB | ~$13.80 | ~$165.60 |
+| Azure Blob Storage | 50 GB backups, LRS | Per GB/month | ~$1.00 | ~$12.00 |
+| Azure Public IP | Standard static IP | Per hour | ~$3.00 | ~$36.00 |
+| Cloudflare | DNS + HTTPS + CDN | β | $0 | $0 |
+| **Total** | | | **~$25.10** | **~$301.20** |
+
+At 1000 users/day the PostgreSQL B1MS handles load comfortably. For 10,000+ users/day, scaling to `Standard_D2s_v3` for the DB (~$75/month) would be recommended.
+
+The biggest cost saving in this architecture is using Cloudflare as the HTTPS/CDN layer for free instead of an Azure Application Gateway (~$120/month).
+
+---
+
+## Known Issues Encountered During Deployment
+
+- **Azure B-series VMs unavailable in West Europe and North Europe** β at time of deployment, `Standard_B1s`, `Standard_B2s`, and `Standard_B2ats_v2` had capacity restrictions across multiple European regions. Resolved by using `Standard_D2s_v3` in West Europe.
+- **PostgreSQL SSL configuration conflict** β `asyncpg` does not accept `?ssl=require` in the connection URL when also passed as a keyword argument. Resolved by keeping `ssl=True` in code only and removing it from the `DATABASE_URL`.
+- **Docker Compose `version` field warning** β Docker Compose v2 treats the `version` key as obsolete. Harmless warning, does not affect functionality.
+
+---
+
+## External Resources
+
+| Resource | Type | Usage |
+|---|---|---|
+| FastAPI documentation | Official docs | API structure, startup events, dependency injection |
+| asyncpg documentation | Official docs | PostgreSQL async connection pool configuration |
+| highlight.js | Open source library | Client-side syntax highlighting for 15 languages |
+| Azure CLI documentation | Official docs | VM, PostgreSQL, and storage provisioning commands |
+| Docker documentation | Official docs | Multi-stage Dockerfile, Compose configuration |
+| **Claude (Anthropic)** | Generative AI | Used to generate application code, Dockerfiles, deployment scripts, and this documentation. All output reviewed and tested manually. |
diff --git a/SK1/backend/Dockerfile b/SK1/backend/Dockerfile
new file mode 100644
index 0000000..b3a66cf
--- /dev/null
+++ b/SK1/backend/Dockerfile
@@ -0,0 +1,12 @@
+FROM python:3.12-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+EXPOSE 8000
+
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/SK1/backend/main.py b/SK1/backend/main.py
new file mode 100644
index 0000000..5d490ef
--- /dev/null
+++ b/SK1/backend/main.py
@@ -0,0 +1,169 @@
+from fastapi import FastAPI, HTTPException, Query
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from typing import Optional
+import asyncpg
+import redis.asyncio as aioredis
+import uuid
+import hashlib
+import os
+import json
+from datetime import datetime, timedelta
+
+app = FastAPI(title="PasteVault API")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+DATABASE_URL = os.getenv("DATABASE_URL")
+REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379")
+SECRET_KEY = os.getenv("SECRET_KEY", "changeme")
+
+db_pool = None
+redis_client = None
+
+
+@app.on_event("startup")
+async def startup():
+ global db_pool, redis_client
+ db_pool = await asyncpg.create_pool(DATABASE_URL, ssl="require")
+ redis_client = await aioredis.from_url(REDIS_URL, decode_responses=True)
+ async with db_pool.acquire() as conn:
+ await conn.execute("""
+ CREATE TABLE IF NOT EXISTS pastes (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ title VARCHAR(255) NOT NULL DEFAULT 'Untitled',
+ content TEXT NOT NULL,
+ language VARCHAR(50) DEFAULT 'plaintext',
+ password_hash VARCHAR(255),
+ expires_at TIMESTAMP,
+ created_at TIMESTAMP DEFAULT NOW(),
+ views INTEGER DEFAULT 0
+ )
+ """)
+
+
+@app.on_event("shutdown")
+async def shutdown():
+ await db_pool.close()
+ await redis_client.aclose()
+
+
+class PasteCreate(BaseModel):
+ title: str = "Untitled"
+ content: str
+ language: str = "plaintext"
+ password: Optional[str] = None
+ expiry: Optional[str] = None
+
+
+def hash_password(password: str) -> str:
+ return hashlib.sha256((password + SECRET_KEY).encode()).hexdigest()
+
+
+def get_expiry(expiry_str: Optional[str]) -> Optional[datetime]:
+ mapping = {"1h": timedelta(hours=1), "1d": timedelta(days=1),
+ "7d": timedelta(days=7), "30d": timedelta(days=30)}
+ if not expiry_str or expiry_str not in mapping:
+ return None
+ return datetime.utcnow() + mapping[expiry_str]
+
+
+@app.get("/api/health")
+async def health():
+ return {"status": "ok", "service": "PasteVault"}
+
+
+@app.post("/api/pastes")
+async def create_paste(paste: PasteCreate):
+ if not paste.content.strip():
+ raise HTTPException(status_code=400, detail="Content cannot be empty")
+ expires_at = get_expiry(paste.expiry)
+ password_hash = hash_password(paste.password) if paste.password else None
+ async with db_pool.acquire() as conn:
+ row = await conn.fetchrow("""
+ INSERT INTO pastes (title, content, language, password_hash, expires_at)
+ VALUES ($1, $2, $3, $4, $5)
+ RETURNING id, title, language, expires_at, created_at
+ """, paste.title, paste.content, paste.language, password_hash, expires_at)
+ return {
+ "id": str(row["id"]),
+ "title": row["title"],
+ "language": row["language"],
+ "expires_at": row["expires_at"].isoformat() if row["expires_at"] else None,
+ "created_at": row["created_at"].isoformat(),
+ "has_password": password_hash is not None,
+ }
+
+
+@app.get("/api/pastes/{paste_id}")
+async def get_paste(paste_id: str, password: Optional[str] = Query(default=None)):
+ try:
+ pid = uuid.UUID(paste_id)
+ except ValueError:
+ raise HTTPException(status_code=400, detail="Invalid paste ID")
+
+ cached = await redis_client.get(f"paste:{paste_id}")
+ if cached:
+ paste = json.loads(cached)
+ else:
+ async with db_pool.acquire() as conn:
+ row = await conn.fetchrow("SELECT * FROM pastes WHERE id = $1", pid)
+ if not row:
+ raise HTTPException(status_code=404, detail="Paste not found")
+ paste = dict(row)
+ paste["id"] = str(paste["id"])
+ paste["expires_at"] = paste["expires_at"].isoformat() if paste["expires_at"] else None
+ paste["created_at"] = paste["created_at"].isoformat()
+ await redis_client.setex(f"paste:{paste_id}", 30, json.dumps(paste, default=str))
+
+ if paste["expires_at"]:
+ if datetime.fromisoformat(paste["expires_at"]) < datetime.utcnow():
+ async with db_pool.acquire() as conn:
+ await conn.execute("DELETE FROM pastes WHERE id = $1", pid)
+ await redis_client.delete(f"paste:{paste_id}")
+ raise HTTPException(status_code=404, detail="Paste has expired")
+
+ if paste["password_hash"]:
+ if not password:
+ return {"id": paste["id"], "title": paste["title"], "requires_password": True}
+ if hash_password(password) != paste["password_hash"]:
+ raise HTTPException(status_code=403, detail="Wrong password")
+
+ async with db_pool.acquire() as conn:
+ await conn.execute("UPDATE pastes SET views = views + 1 WHERE id = $1", pid)
+ await redis_client.delete(f"paste:{paste_id}")
+
+ return {
+ "id": paste["id"],
+ "title": paste["title"],
+ "content": paste["content"],
+ "language": paste["language"],
+ "expires_at": paste["expires_at"],
+ "created_at": paste["created_at"],
+ "views": paste["views"] + 1,
+ "has_password": paste["password_hash"] is not None,
+ "requires_password": False,
+ }
+
+
+@app.delete("/api/pastes/{paste_id}")
+async def delete_paste(paste_id: str, password: Optional[str] = Query(default=None)):
+ try:
+ pid = uuid.UUID(paste_id)
+ except ValueError:
+ raise HTTPException(status_code=400, detail="Invalid paste ID")
+ async with db_pool.acquire() as conn:
+ row = await conn.fetchrow("SELECT * FROM pastes WHERE id = $1", pid)
+ if not row:
+ raise HTTPException(status_code=404, detail="Paste not found")
+ if row["password_hash"] and (not password or hash_password(password) != row["password_hash"]):
+ raise HTTPException(status_code=403, detail="Wrong password")
+ async with db_pool.acquire() as conn:
+ await conn.execute("DELETE FROM pastes WHERE id = $1", pid)
+ await redis_client.delete(f"paste:{paste_id}")
+ return {"message": "Paste deleted successfully"}
diff --git a/SK1/backend/requirements.txt b/SK1/backend/requirements.txt
new file mode 100644
index 0000000..36caa61
--- /dev/null
+++ b/SK1/backend/requirements.txt
@@ -0,0 +1,5 @@
+fastapi==0.111.0
+uvicorn[standard]==0.30.0
+asyncpg==0.29.0
+redis==5.0.4
+pydantic==2.7.1
diff --git a/SK1/docker-compose.yml b/SK1/docker-compose.yml
new file mode 100644
index 0000000..d4576d7
--- /dev/null
+++ b/SK1/docker-compose.yml
@@ -0,0 +1,34 @@
+version: '3.8'
+
+services:
+
+ frontend:
+ build: ./frontend
+ container_name: pastevault-frontend
+ restart: always
+ ports:
+ - "80:80"
+ depends_on:
+ - backend
+
+ backend:
+ build: ./backend
+ container_name: pastevault-backend
+ restart: always
+ expose:
+ - "8000"
+ env_file:
+ - .env
+ depends_on:
+ - redis
+
+ redis:
+ image: redis:7-alpine
+ container_name: pastevault-redis
+ restart: always
+ volumes:
+ - redis_data:/data
+ command: redis-server --appendonly yes
+
+volumes:
+ redis_data:
diff --git a/SK1/frontend/Dockerfile b/SK1/frontend/Dockerfile
new file mode 100644
index 0000000..e4dc3f2
--- /dev/null
+++ b/SK1/frontend/Dockerfile
@@ -0,0 +1,20 @@
+# Stage 1 β Build React app
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+COPY package.json ./
+RUN npm install
+
+COPY . .
+RUN npm run build
+
+# Stage 2 β Serve with Nginx
+FROM nginx:alpine
+
+COPY --from=builder /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/SK1/frontend/index.html b/SK1/frontend/index.html
new file mode 100644
index 0000000..9a2b5c4
--- /dev/null
+++ b/SK1/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ PasteVault β Secure Code Sharing
+
+
+
+
+
+
+
diff --git a/SK1/frontend/nginx.conf b/SK1/frontend/nginx.conf
new file mode 100644
index 0000000..b40a5d7
--- /dev/null
+++ b/SK1/frontend/nginx.conf
@@ -0,0 +1,31 @@
+server {
+ listen 80;
+ server_name _;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Serve React app β handle client-side routing
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Proxy all /api/* requests to FastAPI backend
+ location /api/ {
+ proxy_pass http://backend:8000;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 60s;
+ }
+
+ # Access logs β viewable with: docker logs pastevault-frontend
+ access_log /var/log/nginx/access.log;
+ error_log /var/log/nginx/error.log;
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN";
+ add_header X-Content-Type-Options "nosniff";
+}
diff --git a/SK1/frontend/package.json b/SK1/frontend/package.json
new file mode 100644
index 0000000..dc8b1cb
--- /dev/null
+++ b/SK1/frontend/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "pastevault",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.23.1",
+ "highlight.js": "^11.9.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.0",
+ "vite": "^5.2.12"
+ }
+}
diff --git a/SK1/frontend/src/App.jsx b/SK1/frontend/src/App.jsx
new file mode 100644
index 0000000..5c1a58c
--- /dev/null
+++ b/SK1/frontend/src/App.jsx
@@ -0,0 +1,31 @@
+import React from 'react'
+import { Routes, Route, Link } from 'react-router-dom'
+import CreatePaste from './CreatePaste'
+import ViewPaste from './ViewPaste'
+
+export default function App() {
+ return (
+
+
+
+ π
+ PasteVault
+
+
+
+
+
+
+ } />
+ } />
+
+
+
+
+
+ )
+}
diff --git a/SK1/frontend/src/CreatePaste.jsx b/SK1/frontend/src/CreatePaste.jsx
new file mode 100644
index 0000000..d426a0f
--- /dev/null
+++ b/SK1/frontend/src/CreatePaste.jsx
@@ -0,0 +1,127 @@
+import React, { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+
+const LANGUAGES = [
+ 'plaintext', 'bash', 'cpp', 'css', 'dockerfile',
+ 'go', 'html', 'java', 'javascript', 'json',
+ 'python', 'rust', 'sql', 'typescript', 'yaml',
+]
+
+const EXPIRY_OPTIONS = [
+ { value: '', label: 'βΎ Never' },
+ { value: '1h', label: 'β± 1 Hour' },
+ { value: '1d', label: 'π
1 Day' },
+ { value: '7d', label: 'π
7 Days' },
+ { value: '30d', label: 'π
30 Days' },
+]
+
+export default function CreatePaste() {
+ const navigate = useNavigate()
+ const [form, setForm] = useState({
+ title: '', content: '', language: 'plaintext', password: '', expiry: '',
+ })
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState('')
+
+ const set = (key, val) => setForm(f => ({ ...f, [key]: val }))
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+ if (!form.content.trim()) { setError('Content cannot be empty.'); return }
+ setLoading(true)
+ setError('')
+ try {
+ const res = await fetch('/api/pastes', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ title: form.title || 'Untitled',
+ content: form.content,
+ language: form.language,
+ password: form.password || null,
+ expiry: form.expiry || null,
+ }),
+ })
+ if (!res.ok) {
+ const err = await res.json()
+ throw new Error(err.detail || 'Server error')
+ }
+ const data = await res.json()
+ navigate(`/paste/${data.id}`)
+ } catch (err) {
+ setError(err.message || 'Failed to create paste.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
Share code securely.
+
Create an expiring, password-protected paste in seconds.
+
+
+
+ {error &&
{error}
}
+
+
+
+
+ )
+}
diff --git a/SK1/frontend/src/ViewPaste.jsx b/SK1/frontend/src/ViewPaste.jsx
new file mode 100644
index 0000000..b8568f9
--- /dev/null
+++ b/SK1/frontend/src/ViewPaste.jsx
@@ -0,0 +1,167 @@
+import React, { useState, useEffect, useRef } from 'react'
+import { useParams, useNavigate } from 'react-router-dom'
+import hljs from 'highlight.js'
+import 'highlight.js/styles/github-dark.css'
+
+export default function ViewPaste() {
+ const { id } = useParams()
+ const navigate = useNavigate()
+ const codeRef = useRef(null)
+
+ const [paste, setPaste] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState('')
+ const [requiresPassword, setRequiresPassword] = useState(false)
+ const [password, setPassword] = useState('')
+ const [pwdError, setPwdError] = useState('')
+ const [copied, setCopied] = useState(false)
+ const [shareUrl] = useState(window.location.href)
+ const [urlCopied, setUrlCopied] = useState(false)
+
+ const fetchPaste = async (pwd = '') => {
+ try {
+ const url = pwd
+ ? `/api/pastes/${id}?password=${encodeURIComponent(pwd)}`
+ : `/api/pastes/${id}`
+ const res = await fetch(url)
+ const data = await res.json()
+
+ if (res.status === 404) { setError(data.detail || 'Paste not found or expired.'); return }
+ if (res.status === 403) { setPwdError('Wrong password. Try again.'); return }
+ if (data.requires_password) { setRequiresPassword(true); return }
+
+ setPaste(data)
+ setRequiresPassword(false)
+ } catch {
+ setError('Failed to load paste.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => { fetchPaste() }, [id])
+
+ useEffect(() => {
+ if (paste && codeRef.current) {
+ delete codeRef.current.dataset.highlighted
+ hljs.highlightElement(codeRef.current)
+ }
+ }, [paste])
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(paste.content)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ const handleShareCopy = () => {
+ navigator.clipboard.writeText(shareUrl)
+ setUrlCopied(true)
+ setTimeout(() => setUrlCopied(false), 2000)
+ }
+
+ const handlePwdSubmit = (e) => {
+ e.preventDefault()
+ setPwdError('')
+ setLoading(true)
+ fetchPaste(password)
+ }
+
+ const formatDate = (iso) => new Date(iso).toLocaleString()
+
+ const formatExpiry = (iso) => {
+ if (!iso) return 'Never'
+ const diff = new Date(iso) - Date.now()
+ if (diff < 0) return 'Expired'
+ const h = Math.floor(diff / 3600000)
+ if (h < 1) return `${Math.floor(diff / 60000)}m remaining`
+ if (h < 24) return `${h}h remaining`
+ return `${Math.floor(h / 24)}d remaining`
+ }
+
+ if (loading) return (
+
+ )
+
+ if (requiresPassword) return (
+
+
+
π
+
Password Required
+
This paste is password protected.
+ {pwdError &&
{pwdError}
}
+
+
+
+ )
+
+ if (error) return (
+
+
+
π¨
+
Not Found
+
{error}
+
+
+
+ )
+
+ if (!paste) return null
+
+ return (
+
+
+
+
+
{paste.title}
+
+ π
{formatDate(paste.created_at)}
+ β³ {formatExpiry(paste.expires_at)}
+ π {paste.views} {paste.views === 1 ? 'view' : 'views'}
+ {paste.has_password && π Protected}
+ {paste.language}
+
+
+
+
+
+
+
+
+
+
+
+ {paste.content.split('\n').length} lines Β· {paste.content.length} chars
+
+
+
+
+
+
+ {paste.content}
+
+
+
+
+
+ )
+}
diff --git a/SK1/frontend/src/index.css b/SK1/frontend/src/index.css
new file mode 100644
index 0000000..ee82a2d
--- /dev/null
+++ b/SK1/frontend/src/index.css
@@ -0,0 +1,302 @@
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+:root {
+ --bg: #0d1117;
+ --surface: #161b22;
+ --surface2: #21262d;
+ --border: #30363d;
+ --text: #e6edf3;
+ --muted: #7d8590;
+ --accent: #7c3aed;
+ --accent-hover: #6d28d9;
+ --accent-light: rgba(124, 58, 237, 0.15);
+ --danger: #f85149;
+ --danger-bg: rgba(248, 81, 73, 0.1);
+ --success: #3fb950;
+ --radius: 10px;
+}
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
+ font-size: 15px;
+ line-height: 1.6;
+ min-height: 100vh;
+}
+
+/* ---- LAYOUT ---- */
+.app { display: flex; flex-direction: column; min-height: 100vh; }
+
+header {
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+ padding: 0 28px;
+ height: 58px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.logo {
+ font-size: 1.15rem;
+ font-weight: 700;
+ color: var(--text);
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ letter-spacing: -0.3px;
+}
+
+.logo-icon { font-size: 1.2rem; }
+
+.nav-btn {
+ background: var(--accent);
+ color: #fff;
+ text-decoration: none;
+ padding: 7px 16px;
+ border-radius: 8px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ transition: background 0.2s;
+}
+.nav-btn:hover { background: var(--accent-hover); }
+
+main { flex: 1; padding: 28px 16px; }
+
+footer {
+ background: var(--surface);
+ border-top: 1px solid var(--border);
+ padding: 14px 28px;
+ text-align: center;
+ color: var(--muted);
+ font-size: 0.78rem;
+}
+
+/* ---- CONTAINER & CARD ---- */
+.container { max-width: 980px; margin: 0 auto; }
+
+.hero {
+ text-align: center;
+ padding: 20px 0 28px;
+}
+.hero h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ background: linear-gradient(135deg, #e6edf3, #7c3aed);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ margin-bottom: 8px;
+}
+.hero p { color: var(--muted); font-size: 1rem; }
+
+.card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 24px;
+}
+
+.center-card {
+ max-width: 420px;
+ margin: 60px auto;
+ text-align: center;
+}
+.center-card h2 { font-size: 1.3rem; margin-bottom: 6px; }
+.lock-icon { font-size: 3rem; margin-bottom: 12px; }
+.spinner {
+ width: 36px; height: 36px;
+ border: 3px solid var(--border);
+ border-top-color: var(--accent);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin: 0 auto 16px;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+
+/* ---- FORMS ---- */
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-bottom: 16px;
+}
+
+.form-group label {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.form-group input,
+.form-group select,
+.form-group textarea {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ color: var(--text);
+ padding: 9px 13px;
+ font-size: 0.9rem;
+ font-family: inherit;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ outline: none;
+ width: 100%;
+}
+
+.form-group input:focus,
+.form-group select:focus,
+.form-group textarea:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-light);
+}
+
+.form-group textarea {
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
+ font-size: 0.85rem;
+ resize: vertical;
+ line-height: 1.65;
+ min-height: 320px;
+}
+
+.form-row {
+ display: flex;
+ gap: 14px;
+ align-items: flex-start;
+ flex-wrap: wrap;
+}
+.form-row .form-group { flex: 1; min-width: 140px; }
+
+.form-footer {
+ margin-top: 4px;
+ align-items: flex-end;
+}
+.submit-area {
+ display: flex;
+ align-items: flex-end;
+ padding-bottom: 16px;
+}
+
+/* ---- BUTTONS ---- */
+.btn-primary {
+ background: var(--accent);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 22px;
+ font-size: 0.9rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s, transform 0.1s;
+ white-space: nowrap;
+}
+.btn-primary:hover { background: var(--accent-hover); }
+.btn-primary:active { transform: scale(0.98); }
+.btn-primary:disabled { opacity: 0.55; cursor: not-allowed; }
+
+.btn-secondary {
+ background: var(--surface2);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 8px 16px;
+ font-size: 0.85rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.2s;
+ white-space: nowrap;
+}
+.btn-secondary:hover { background: var(--border); }
+
+/* ---- ALERTS ---- */
+.alert {
+ border-radius: 8px;
+ padding: 10px 14px;
+ font-size: 0.85rem;
+ margin-bottom: 16px;
+}
+.alert-error {
+ background: var(--danger-bg);
+ border: 1px solid rgba(248, 81, 73, 0.35);
+ color: var(--danger);
+}
+
+/* ---- PASTE VIEW ---- */
+.paste-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 16px;
+ margin-bottom: 12px;
+ flex-wrap: wrap;
+}
+
+.paste-title {
+ font-size: 1.25rem;
+ font-weight: 700;
+ margin-bottom: 8px;
+}
+
+.paste-meta {
+ display: flex;
+ gap: 14px;
+ flex-wrap: wrap;
+ font-size: 0.78rem;
+ color: var(--muted);
+ align-items: center;
+}
+
+.lang-badge {
+ background: var(--accent-light);
+ color: #a78bfa;
+ border: 1px solid rgba(124, 58, 237, 0.3);
+ border-radius: 5px;
+ padding: 1px 8px;
+ font-family: monospace;
+ font-size: 0.75rem;
+}
+
+.paste-actions {
+ display: flex;
+ gap: 8px;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+
+.code-toolbar {
+ padding: 6px 0 10px;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 0;
+}
+
+.code-wrapper {
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ margin-top: 12px;
+}
+
+.code-wrapper pre { margin: 0; overflow-x: auto; }
+
+.code-wrapper code {
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace !important;
+ font-size: 0.84rem !important;
+}
+
+.hljs { background: #0d1117 !important; padding: 20px !important; }
+
+.muted { color: var(--muted); }
+
+/* ---- RESPONSIVE ---- */
+@media (max-width: 640px) {
+ .form-row { flex-direction: column; }
+ .paste-header { flex-direction: column; }
+ .paste-actions { width: 100%; justify-content: flex-start; }
+ .hero h1 { font-size: 1.5rem; }
+}
diff --git a/SK1/frontend/src/main.jsx b/SK1/frontend/src/main.jsx
new file mode 100644
index 0000000..8901eca
--- /dev/null
+++ b/SK1/frontend/src/main.jsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import App from './App'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+)
diff --git a/SK1/frontend/vite.config.js b/SK1/frontend/vite.config.js
new file mode 100644
index 0000000..6318ceb
--- /dev/null
+++ b/SK1/frontend/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': 'http://localhost:8000'
+ }
+ }
+})
diff --git a/SK1/scripts/backup.sh b/SK1/scripts/backup.sh
new file mode 100644
index 0000000..3ce4b56
--- /dev/null
+++ b/SK1/scripts/backup.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+set -e
+
+# =============================================================
+# PasteVault - backup.sh
+# Dumps the PostgreSQL database and uploads to Azure Blob Storage.
+#
+# Prerequisites:
+# - postgresql-client installed (sudo apt install postgresql-client)
+# - Azure CLI installed and logged in
+# - .env file present with DB_PASS set
+#
+# Usage:
+# cd sk1/
+# bash scripts/backup.sh
+# =============================================================
+
+source .env
+
+DB_HOST="pastevault-db.postgres.database.azure.com"
+DB_USER="pvadmin"
+DB_NAME="pastevault"
+STORAGE_ACCOUNT="pastevaultstorage"
+CONTAINER="backups"
+TIMESTAMP=$(date +%Y%m%d_%H%M%S)
+BACKUP_FILE="pastevault_backup_${TIMESTAMP}.sql"
+TMP_PATH="/tmp/$BACKUP_FILE"
+
+echo "π PasteVault Backup β $TIMESTAMP"
+echo ""
+
+# Check pg_dump is available
+if ! command -v pg_dump &> /dev/null; then
+ echo "Installing postgresql-client..."
+ sudo apt-get install -y postgresql-client -qq
+fi
+
+# Dump the database
+echo "1/2 β Dumping PostgreSQL database..."
+PGPASSWORD="$DB_PASS" pg_dump \
+ --host=$DB_HOST \
+ --username=$DB_USER \
+ --dbname=$DB_NAME \
+ --no-password \
+ --clean \
+ --if-exists \
+ --file=$TMP_PATH \
+ --sslmode=require
+
+echo " β
Dump complete: $BACKUP_FILE ($(du -sh $TMP_PATH | cut -f1))"
+
+# Upload to Azure Blob Storage
+echo ""
+echo "2/2 β Uploading to Azure Blob Storage..."
+az storage blob upload \
+ --account-name $STORAGE_ACCOUNT \
+ --container-name $CONTAINER \
+ --name $BACKUP_FILE \
+ --file $TMP_PATH \
+ --overwrite \
+ --output none
+
+rm $TMP_PATH
+
+echo " β
Upload complete"
+echo ""
+echo "β
Backup saved: $BACKUP_FILE"
+echo ""
+echo " List all backups:"
+echo " az storage blob list --account-name $STORAGE_ACCOUNT --container-name $CONTAINER -o table"
+echo ""
+echo " Download a backup:"
+echo " az storage blob download --account-name $STORAGE_ACCOUNT --container-name $CONTAINER --name $BACKUP_FILE --file ./restore.sql"
diff --git a/SK1/scripts/prepare-app.sh b/SK1/scripts/prepare-app.sh
new file mode 100644
index 0000000..6ef207e
--- /dev/null
+++ b/SK1/scripts/prepare-app.sh
@@ -0,0 +1,172 @@
+#!/bin/bash
+set -e
+
+# =============================================================
+# PasteVault - prepare-app.sh
+# Provisions all Azure infrastructure and deploys the app.
+#
+# Prerequisites:
+# - Azure CLI installed and logged in (az login)
+# - .env file present in project root (copy from .env.example)
+# - SSH key at ~/.ssh/id_rsa
+#
+# Usage:
+# cd sk1/
+# bash scripts/prepare-app.sh
+# =============================================================
+
+echo "π PasteVault β Starting full deployment..."
+
+# ---------- CONFIGURATION ----------
+RG="pastevault-rg"
+VM_LOCATION="westeurope"
+DB_LOCATION="northeurope"
+VM_NAME="pastevault-vm"
+VM_SIZE="Standard_D2s_v3"
+VM_USER="azureuser"
+DB_SERVER="pastevault-db"
+DB_NAME="pastevault"
+DB_USER="pvadmin"
+STORAGE_ACCOUNT="pastevaultstorage"
+SSH_KEY="$HOME/.ssh/id_rsa"
+
+# ---------- CHECKS ----------
+if [ ! -f .env ]; then
+ echo "β .env file not found."
+ echo " Copy .env.example to .env and fill in DB_PASS and SECRET_KEY."
+ exit 1
+fi
+
+source .env
+
+if [ -z "$DB_PASS" ]; then
+ echo "β DB_PASS is not set in .env"
+ exit 1
+fi
+
+if [ ! -f "$SSH_KEY" ]; then
+ echo "βοΈ No SSH key found. Generating one..."
+ ssh-keygen -t rsa -b 4096 -f "$SSH_KEY" -N ""
+fi
+
+# ---------- RESOURCE GROUP ----------
+echo ""
+echo "1/6 β Creating resource group '$RG'..."
+az group create \
+ --name $RG \
+ --location $VM_LOCATION \
+ --output none
+echo " β
Resource group ready"
+
+# ---------- VIRTUAL MACHINE ----------
+echo ""
+echo "2/6 β Creating VM ($VM_SIZE Β· 2 vCPU Β· 8GB RAM)..."
+az vm create \
+ --resource-group $RG \
+ --name $VM_NAME \
+ --image Ubuntu2204 \
+ --size $VM_SIZE \
+ --location $VM_LOCATION \
+ --admin-username $VM_USER \
+ --ssh-key-value "$SSH_KEY.pub" \
+ --public-ip-sku Standard \
+ --output none
+
+az vm open-port --resource-group $RG --name $VM_NAME --port 80 --priority 1001 --output none
+az vm open-port --resource-group $RG --name $VM_NAME --port 443 --priority 1002 --output none
+
+VM_IP=$(az vm show -d -g $RG -n $VM_NAME --query publicIps -o tsv)
+echo " β
VM ready β IP: $VM_IP"
+
+# ---------- POSTGRESQL ----------
+echo ""
+echo "3/6 β Creating PostgreSQL Flexible Server..."
+az postgres flexible-server create \
+ --resource-group $RG \
+ --name $DB_SERVER \
+ --location $DB_LOCATION \
+ --admin-user $DB_USER \
+ --admin-password "$DB_PASS" \
+ --sku-name Standard_B1ms \
+ --tier Burstable \
+ --storage-size 32 \
+ --version 15 \
+ --yes \
+ --output none
+
+az postgres flexible-server db create \
+ --resource-group $RG \
+ --server-name $DB_SERVER \
+ --database-name $DB_NAME \
+ --output none
+
+az postgres flexible-server firewall-rule create \
+ --resource-group $RG \
+ --name $DB_SERVER \
+ --rule-name allow-vm \
+ --start-ip-address $VM_IP \
+ --end-ip-address $VM_IP \
+ --output none
+
+echo " β
PostgreSQL ready β $DB_SERVER.postgres.database.azure.com"
+
+# ---------- STORAGE ----------
+echo ""
+echo "4/6 β Creating storage account for backups..."
+az storage account create \
+ --name $STORAGE_ACCOUNT \
+ --resource-group $RG \
+ --location $DB_LOCATION \
+ --sku Standard_LRS \
+ --kind StorageV2 \
+ --output none
+
+az storage container create \
+ --name backups \
+ --account-name $STORAGE_ACCOUNT \
+ --output none
+
+echo " β
Storage account ready"
+
+# ---------- INSTALL DOCKER ON VM ----------
+echo ""
+echo "5/6 β Installing Docker on VM..."
+ssh -i $SSH_KEY -o StrictHostKeyChecking=no $VM_USER@$VM_IP << 'ENDSSH'
+sudo apt-get update -y -qq
+sudo apt-get install -y -qq ca-certificates curl gnupg
+sudo install -m 0755 -d /etc/apt/keyrings
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
+sudo chmod a+r /etc/apt/keyrings/docker.gpg
+echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
+ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+sudo apt-get update -y -qq
+sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
+sudo usermod -aG docker azureuser
+ENDSSH
+echo " β
Docker installed"
+
+# ---------- DEPLOY APP ----------
+echo ""
+echo "6/6 β Deploying PasteVault containers..."
+ssh -i $SSH_KEY -o StrictHostKeyChecking=no $VM_USER@$VM_IP "mkdir -p ~/pastevault"
+
+scp -i $SSH_KEY -o StrictHostKeyChecking=no -r \
+ backend frontend docker-compose.yml .env \
+ $VM_USER@$VM_IP:~/pastevault/
+
+ssh -i $SSH_KEY -o StrictHostKeyChecking=no $VM_USER@$VM_IP << 'ENDSSH'
+cd ~/pastevault
+sudo docker compose build --no-cache
+sudo docker compose up -d
+sleep 8
+sudo docker compose ps
+ENDSSH
+
+echo ""
+echo "β
====================================================="
+echo " PasteVault is live!"
+echo " URL: http://$VM_IP"
+echo ""
+echo " Next: Point your Cloudflare DNS A record to $VM_IP"
+echo " Then access via HTTPS at your domain."
+echo "======================================================="
diff --git a/SK1/scripts/remove-app.sh b/SK1/scripts/remove-app.sh
new file mode 100644
index 0000000..f19fe4d
--- /dev/null
+++ b/SK1/scripts/remove-app.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+set -e
+
+# =============================================================
+# PasteVault - remove-app.sh
+# Stops all containers and deletes ALL Azure resources.
+#
+# WARNING: This permanently deletes the database and all data.
+# Take a backup first with: bash scripts/backup.sh
+#
+# Usage:
+# cd sk1/
+# bash scripts/remove-app.sh
+# =============================================================
+
+RG="pastevault-rg"
+VM_NAME="pastevault-vm"
+VM_USER="azureuser"
+SSH_KEY="$HOME/.ssh/id_rsa"
+
+echo "β οΈ WARNING: This will permanently delete all PasteVault"
+echo " resources including the database and all paste data."
+echo ""
+read -p " Type 'yes' to confirm: " CONFIRM
+
+if [ "$CONFIRM" != "yes" ]; then
+ echo "Aborted. Nothing was deleted."
+ exit 0
+fi
+
+# Stop containers on VM first (clean shutdown)
+echo ""
+echo "π Stopping containers on VM..."
+VM_IP=$(az vm show -d -g $RG -n $VM_NAME --query publicIps -o tsv 2>/dev/null || echo "")
+if [ -n "$VM_IP" ]; then
+ ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o ConnectTimeout=10 $VM_USER@$VM_IP \
+ "cd ~/pastevault && sudo docker compose down -v --remove-orphans" 2>/dev/null || true
+ echo " β
Containers stopped"
+else
+ echo " β οΈ VM not reachable, skipping container shutdown"
+fi
+
+# Delete entire resource group β removes VM, DB, storage, IPs, everything
+echo ""
+echo "ποΈ Deleting Azure resource group '$RG'..."
+echo " (This takes 2-3 minutes, running in background)"
+az group delete --name $RG --yes --no-wait
+
+echo ""
+echo "β
Teardown initiated."
+echo " All Azure resources are being deleted."
+echo " Verify with: az group list -o table"
diff --git a/noc-docker/Documentation.pdf b/Z1/Documentation.pdf
similarity index 100%
rename from noc-docker/Documentation.pdf
rename to Z1/Documentation.pdf
diff --git a/noc-docker/README.md b/Z1/README.md
similarity index 100%
rename from noc-docker/README.md
rename to Z1/README.md
diff --git a/noc-docker/backend/Dockerfile b/Z1/backend/Dockerfile
similarity index 100%
rename from noc-docker/backend/Dockerfile
rename to Z1/backend/Dockerfile
diff --git a/noc-docker/backend/app.py b/Z1/backend/app.py
similarity index 100%
rename from noc-docker/backend/app.py
rename to Z1/backend/app.py
diff --git a/noc-docker/backend/requirements.txt b/Z1/backend/requirements.txt
similarity index 100%
rename from noc-docker/backend/requirements.txt
rename to Z1/backend/requirements.txt
diff --git a/noc-docker/docker-compose.yml b/Z1/docker-compose.yml
similarity index 100%
rename from noc-docker/docker-compose.yml
rename to Z1/docker-compose.yml
diff --git a/noc-docker/frontend/Dockerfile b/Z1/frontend/Dockerfile
similarity index 100%
rename from noc-docker/frontend/Dockerfile
rename to Z1/frontend/Dockerfile
diff --git a/noc-docker/frontend/app.py b/Z1/frontend/app.py
similarity index 100%
rename from noc-docker/frontend/app.py
rename to Z1/frontend/app.py
diff --git a/noc-docker/frontend/requirements.txt b/Z1/frontend/requirements.txt
similarity index 100%
rename from noc-docker/frontend/requirements.txt
rename to Z1/frontend/requirements.txt
diff --git a/noc-docker/frontend/templates/index.html b/Z1/frontend/templates/index.html
similarity index 100%
rename from noc-docker/frontend/templates/index.html
rename to Z1/frontend/templates/index.html
diff --git a/noc-docker/prepare-app.sh b/Z1/prepare-app.sh
similarity index 100%
rename from noc-docker/prepare-app.sh
rename to Z1/prepare-app.sh
diff --git a/noc-docker/probe/Dockerfile b/Z1/probe/Dockerfile
similarity index 100%
rename from noc-docker/probe/Dockerfile
rename to Z1/probe/Dockerfile
diff --git a/noc-docker/probe/probe.py b/Z1/probe/probe.py
similarity index 100%
rename from noc-docker/probe/probe.py
rename to Z1/probe/probe.py
diff --git a/noc-docker/remove-app.sh b/Z1/remove-app.sh
similarity index 100%
rename from noc-docker/remove-app.sh
rename to Z1/remove-app.sh
diff --git a/noc-docker/start-app.sh b/Z1/start-app.sh
similarity index 100%
rename from noc-docker/start-app.sh
rename to Z1/start-app.sh
diff --git a/noc-docker/stop-app.sh b/Z1/stop-app.sh
similarity index 100%
rename from noc-docker/stop-app.sh
rename to Z1/stop-app.sh
diff --git a/Z2/z2/port-forward.log b/Z2/z2/port-forward.log
index 1f3b0a8..7f95345 100644
--- a/Z2/z2/port-forward.log
+++ b/Z2/z2/port-forward.log
@@ -132,3 +132,243 @@ Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
E0422 08:16:00.690760 146224 portforward.go:404] "Unhandled Error" err="error copying from local connection to remote stream: writeto tcp6 [::1]:8080->[::1]:39644: read tcp6 [::1]:8080->[::1]:39644: read: connection reset by peer"
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+E0422 08:43:28.891335 146224 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 8f982ab356928acdf2867b50e355a28049680afccea2c3c130d494ba1dd2e942, uid : container not running (8f982ab356928acdf2867b50e355a28049680afccea2c3c130d494ba1dd2e942)"
+E0422 08:43:28.891347 146224 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 8f982ab356928acdf2867b50e355a28049680afccea2c3c130d494ba1dd2e942, uid : container not running (8f982ab356928acdf2867b50e355a28049680afccea2c3c130d494ba1dd2e942)"
+error: lost connection to pod
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): services "notes-web-service" not found
+port-forward crashed, retrying in 2s...
+error: unable to forward port because pod is not running. Current status=Pending
+port-forward crashed, retrying in 2s...
+Forwarding from 127.0.0.1:8080 -> 5000
+Forwarding from [::1]:8080 -> 5000
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+error: lost connection to pod
+port-forward crashed, retrying in 2s...
+error: error upgrading connection: unable to upgrade connection: pod does not exist
+port-forward crashed, retrying in 2s...
+Forwarding from 127.0.0.1:8080 -> 5000
+Forwarding from [::1]:8080 -> 5000
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+E0422 11:16:27.685596 193095 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 0ff84adf12ccacc61cef4f388e65da09d877fa8fc41c1e535bbe39f1e49b5ecc, uid : container not running (0ff84adf12ccacc61cef4f388e65da09d877fa8fc41c1e535bbe39f1e49b5ecc)"
+E0422 11:16:27.686186 193095 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 0ff84adf12ccacc61cef4f388e65da09d877fa8fc41c1e535bbe39f1e49b5ecc, uid : container not running (0ff84adf12ccacc61cef4f388e65da09d877fa8fc41c1e535bbe39f1e49b5ecc)"
+error: lost connection to pod
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+error: unable to forward port because pod is not running. Current status=Pending
+port-forward crashed, retrying in 2s...
+Forwarding from 127.0.0.1:8080 -> 5000
+Forwarding from [::1]:8080 -> 5000
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+E0422 11:17:42.409430 195920 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod bc2514b8244d5e4a40432c5491da27757c33408c656764e5b3c1ae8c01b3f940, uid : container not running (bc2514b8244d5e4a40432c5491da27757c33408c656764e5b3c1ae8c01b3f940)"
+error: lost connection to pod
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): namespaces "notes-app" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): services "notes-web-service" not found
+port-forward crashed, retrying in 2s...
+Error from server (NotFound): services "notes-web-service" not found
+port-forward crashed, retrying in 2s...
+Forwarding from 127.0.0.1:8080 -> 5000
+Forwarding from [::1]:8080 -> 5000
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
diff --git a/Z2/z2/show-url.sh b/Z2/z2/show-url.sh
deleted file mode 100644
index 6bae7f7..0000000
--- a/Z2/z2/show-url.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-if command -v minikube &>/dev/null; then
- echo "==> Minikube detected. Opening service URL..."
- minikube service notes-web-service -n notes-app --url
-else
- NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
- echo "==> App available at: http://${NODE_IP}:30080"
-fi