diff --git a/app.py b/app.py index 2dd1b98..5b14976 100644 --- a/app.py +++ b/app.py @@ -2,27 +2,21 @@ from flask import Flask, request, redirect, url_for, render_template, session, f from flask_sqlalchemy import SQLAlchemy from sqlalchemy import func from dotenv import load_dotenv -import os +import os from werkzeug.utils import secure_filename -from flask_login import LoginManager, login_required, logout_user, UserMixin, login_user +from flask_login import LoginManager, login_required, logout_user, UserMixin, login_user, current_user from flask_mail import Mail, Message -import json -from slugify import slugify +from slugify import slugify from datetime import datetime -import hmac, hashlib, json, time from pathlib import Path -import json, hmac, hashlib, requests, time - +import hmac, hashlib, json, requests, time +import markdown as md_lib load_dotenv(Path(__file__).with_name(".env")) - - - app = Flask(__name__, static_folder="static", static_url_path="/static") -HUB_PUSH_SECRET = os.getenv("HUB_PUSH_SECRET") # match the api_secret you set in the Hub - +HUB_PUSH_SECRET = os.getenv("HUB_PUSH_SECRET") CONTACT = { "name": os.environ.get("BH_CONTACT_NAME", "Benjamin Mosley"), @@ -31,7 +25,7 @@ CONTACT = { "phone": os.environ.get("BH_CONTACT_PHONE", "(806) 655 2300"), "city": os.environ.get("BH_CONTACT_CITY", "Canyon / Amarillo / Borger / Remote"), "cal": os.environ.get("BH_CONTACT_CAL", "https://calendly.com/bennyshouse24/30min"), - "link": os.environ.get("BH_CONTACT_LINK", "https://www.linkedin.com/in/benjamin-mosley-849643329/"), + "link": os.environ.get("BH_CONTACT_LINK", "https://www.linkedin.com/in/benjamin-mosley-849643329/"), "site": os.environ.get("BH_CONTACT_SITE", "https://bennyshouse.net"), "hours": os.environ.get("BH_CONTACT_HOURS", "Mon–Fri, 9a–5p CT"), } @@ -60,10 +54,8 @@ def load_user(user_id): return User() return None - @app.context_processor def inject_now(): - # lets you use {{ now().year }} in any template return {"now": datetime.now} class BlogPost(db.Model): @@ -71,23 +63,32 @@ class BlogPost(db.Model): title = db.Column(db.String(255), nullable=False) slug = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) - images = db.Column(db.Text, nullable=True) + images = db.Column(db.Text, nullable=True) category = db.Column(db.String(100), nullable=True) tags = db.Column(db.Text, nullable=True) pinned = db.Column(db.Boolean, default=False) + draft = db.Column(db.Boolean, default=False) class ProjectPost(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) slug = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) - images = db.Column(db.Text, nullable=True) + images = db.Column(db.Text, nullable=True) category = db.Column(db.String(100), nullable=True) tags = db.Column(db.Text, nullable=True) pinned = db.Column(db.Boolean, default=False) + draft = db.Column(db.Boolean, default=False) with app.app_context(): db.create_all() + with db.engine.connect() as conn: + for table in ('blog_post', 'project_post'): + try: + conn.execute(db.text(f'ALTER TABLE {table} ADD COLUMN draft BOOLEAN NOT NULL DEFAULT 0')) + conn.commit() + except Exception: + pass @app.template_filter('from_json') def from_json(value): @@ -96,71 +97,31 @@ def from_json(value): except (TypeError, json.JSONDecodeError): return [] +@app.template_filter('markdown') +def render_markdown(value): + return md_lib.markdown(value or '', extensions=['fenced_code', 'tables', 'nl2br']) def push_to_domain(push_url: str, api_secret: str, payload: dict): - # Serialize deterministically body = json.dumps(payload, separators=(',', ':'), ensure_ascii=False).encode('utf-8') - - # Compute signature over the exact bytes sig = hmac.new(api_secret.encode('utf-8'), body, hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json", "X-Hub-Signature-256": f"sha256={sig}", - # Optional if you later check timestamps: - # "X-Timestamp": str(int(time.time())), } - - # Send the *exact same* bytes you signed - r = requests.post(push_url, headers=headers, data=body, timeout=15) - return r - + return requests.post(push_url, headers=headers, data=body, timeout=15) def _verify_hub_signature(raw: bytes) -> None: if not HUB_PUSH_SECRET: abort(403, description="Missing HUB_PUSH_SECRET") - # Accept both header names sig_hdr = (request.headers.get("X-Signature") or request.headers.get("X-Hub-Signature-256") or "").strip() - # Accept either "sha256=" or just "" provided = sig_hdr if provided.lower().startswith("sha256="): provided = provided.split("=", 1)[1].strip() - if not provided or any(c.isspace() for c in provided): - abort(403, description="Bad signature format") - - expected = hmac.new(HUB_PUSH_SECRET.encode(), raw, hashlib.sha256).hexdigest() - if not hmac.compare_digest(provided, expected): - abort(403, description="Signature mismatch") - - ts = request.headers.get("X-Timestamp") - if ts and ts.isdigit() and abs(time.time() - int(ts)) > 300: - abort(403, description="Stale request") - -def _to_slug(text: str) -> str: - s = slugify(text or "") - return s or f"post-{int(time.time())}" -def _to_slug(text: str) -> str: - s = slugify(text or "") - return s or f"post-{int(time.time())}" -def _verify_hub_signature(raw: bytes) -> None: - if not HUB_PUSH_SECRET: - abort(403, description="Missing HUB_PUSH_SECRET") - - # Accept both header names - sig_hdr = (request.headers.get("X-Signature") - or request.headers.get("X-Hub-Signature-256") - or "").strip() - - # Accept either "sha256=" or just "" - provided = sig_hdr - if provided.lower().startswith("sha256="): - provided = provided.split("=", 1)[1].strip() - - # must be 64 hex chars, no spaces if not provided or any(c.isspace() for c in provided) or len(provided) != 64: abort(403, description="Bad signature format") @@ -172,7 +133,6 @@ def _verify_hub_signature(raw: bytes) -> None: if ts and ts.isdigit() and abs(time.time() - int(ts)) > 300: abort(403, description="Stale request") - def _to_slug(text: str) -> str: s = slugify(text or "") return s or f"post-{int(time.time())}" @@ -188,74 +148,44 @@ def api_publish(): kind = (data.get("kind") or "blog").lower() title = data.get("title") or "Untitled" - summary = data.get("summary") content_html = data.get("content_html") or "" content_text = data.get("content_text") or "" tags = ",".join(data.get("tags") or []) images = data.get("images") or [] pinned = bool(data.get("pinned")) - external_id = data.get("external_id") # optional stable id (slug-like) + external_id = data.get("external_id") slug = _to_slug(external_id or title) - # Normalize content to your existing schema body = content_html or content_text.replace("\n", "
") images_json = json.dumps(images) if images else None if kind == "project": obj = ProjectPost( - title=title, - slug=slug, - content=body, - category=None, - tags=tags, - pinned=pinned, - images=images_json + title=title, slug=slug, content=body, + category=None, tags=tags, pinned=pinned, images=images_json ) else: obj = BlogPost( - title=title, - slug=slug, - content=body, - category=None, - tags=tags, - pinned=pinned, - images=images_json + title=title, slug=slug, content=body, + category=None, tags=tags, pinned=pinned, images=images_json ) db.session.add(obj) db.session.commit() - # Return the canonical URL so the hub can log it if you want url = url_for("view_project" if kind == "project" else "view_blog", slug=slug, _external=True) return jsonify({"ok": True, "url": url, "id": obj.id, "slug": slug}) - - computed = hmac.new( - (HUB_PUSH_SECRET or "").encode(), raw, hashlib.sha256 - ).hexdigest() - - return jsonify({ - "saw_header": sig_hdr, - "provided": provided, - "len_provided": len(provided), - "computed": computed, - "matches": hmac.compare_digest(provided, computed), - "body_len": len(raw), - }) - - @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": username = request.form["username"] password = request.form["password"] - if username == USERNAME and password == PASSWORD: user = User() login_user(user) session.permanent = True return redirect(url_for("admin_panel")) - flash("Invalid email or password!", "danger") return render_template("login.html") @@ -265,7 +195,6 @@ def logout(): logout_user() return redirect(url_for("index")) - @app.route('/admin') @login_required def admin_panel(): @@ -278,10 +207,11 @@ def admin_panel(): def new_blog(): if request.method == 'POST': title = request.form['title'] - content = request.form['content'].replace("\n", "
") + content = request.form['content'] category = request.form['category'] tags = request.form['tags'] pinned = 'pinned' in request.form + is_draft = request.form.get('action') == 'draft' images = request.files.getlist('images') slug = slugify(title) @@ -290,17 +220,13 @@ def new_blog(): image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) new_post = BlogPost( - title=title, - slug=slug, - content=content, - category=category, - tags=tags, - pinned=pinned, - images=json.dumps(image_filenames) + title=title, slug=slug, content=content, + category=category, tags=tags, pinned=pinned, + images=json.dumps(image_filenames), draft=is_draft ) db.session.add(new_post) db.session.commit() - return redirect(url_for('blog')) + return redirect(url_for('view_blog', slug=slug)) return render_template('new_blog.html') @@ -309,10 +235,11 @@ def new_blog(): def new_project(): if request.method == 'POST': title = request.form['title'] - content = request.form['content'].replace("\n", "
") + content = request.form['content'] category = request.form['category'] tags = request.form['tags'] pinned = 'pinned' in request.form + is_draft = request.form.get('action') == 'draft' images = request.files.getlist('images') slug = slugify(title) @@ -321,17 +248,13 @@ def new_project(): image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) new_post = ProjectPost( - title=title, - slug=slug, - content=content, - category=category, - tags=tags, - pinned=pinned, - images=json.dumps(image_filenames) + title=title, slug=slug, content=content, + category=category, tags=tags, pinned=pinned, + images=json.dumps(image_filenames), draft=is_draft ) db.session.add(new_post) db.session.commit() - return redirect(url_for('projects')) + return redirect(url_for('view_project', slug=slug)) return render_template('new_project.html') @@ -339,18 +262,16 @@ def new_project(): @login_required def edit_blog(slug): blogpost = BlogPost.query.filter_by(slug=slug).first_or_404() - if request.method == 'POST': blogpost.title = request.form['title'] blogpost.slug = slugify(blogpost.title) - blogpost.content = request.form['content'].replace("\n", "
") + blogpost.content = request.form['content'] blogpost.category = request.form['category'] blogpost.tags = request.form['tags'] blogpost.pinned = 'pinned' in request.form images = request.files.getlist('images') image_filenames = json.loads(blogpost.images) if blogpost.images else [] - for image in images: if image.filename: filename = secure_filename(image.filename) @@ -367,18 +288,16 @@ def edit_blog(slug): @login_required def edit_project(slug): projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404() - if request.method == 'POST': projectpost.title = request.form['title'] projectpost.slug = slugify(projectpost.title) - projectpost.content = request.form['content'].replace("\n", "
") + projectpost.content = request.form['content'] projectpost.category = request.form['category'] projectpost.tags = request.form['tags'] projectpost.pinned = 'pinned' in request.form images = request.files.getlist('images') image_filenames = json.loads(projectpost.images) if projectpost.images else [] - for image in images: if image.filename: filename = secure_filename(image.filename) @@ -419,36 +338,56 @@ def delete_project(slug): @app.route('/blogtag/') def view_tag(tag): - tag_lower = tag.lower() # Normalize case + tag_lower = tag.lower() blogpost = BlogPost.query.filter(func.lower(BlogPost.tags).contains(tag_lower)).order_by(BlogPost.id.desc()).all() return render_template('blog_tag_results.html', blogpost=blogpost, tag=tag) @app.route('/projecttag/') def view_project_tag(tag): - tag_lower = tag.lower() # Normalize case + tag_lower = tag.lower() projectpost = ProjectPost.query.filter(func.lower(ProjectPost.tags).contains(tag_lower)).order_by(ProjectPost.id.desc()).all() return render_template('project_tag_results.html', projectpost=projectpost, tag=tag) @app.route('/blog') def blog(): - blogpost = BlogPost.query.order_by(BlogPost.id.desc()).all() + blogpost = BlogPost.query.filter_by(draft=False).order_by(BlogPost.id.desc()).all() return render_template('blog.html', blogpost=blogpost) @app.route('/projects') def projects(): - projectpost = ProjectPost.query.order_by(ProjectPost.id.desc()).all() + projectpost = ProjectPost.query.filter_by(draft=False).order_by(ProjectPost.id.desc()).all() return render_template('project.html', projectpost=projectpost) @app.route('/blog/') def view_blog(slug): blogpost = BlogPost.query.filter_by(slug=slug).first_or_404() + if blogpost.draft and not current_user.is_authenticated: + abort(404) return render_template('view_blog.html', blogpost=blogpost) @app.route('/project/') def view_project(slug): projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404() + if projectpost.draft and not current_user.is_authenticated: + abort(404) return render_template('view_project.html', projectpost=projectpost) +@app.route('/publish-blog/', methods=['POST']) +@login_required +def publish_blog(slug): + blogpost = BlogPost.query.filter_by(slug=slug).first_or_404() + blogpost.draft = False + db.session.commit() + return redirect(url_for('view_blog', slug=slug)) + +@app.route('/publish-project/', methods=['POST']) +@login_required +def publish_project(slug): + projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404() + projectpost.draft = False + db.session.commit() + return redirect(url_for('view_project', slug=slug)) + @app.route('/contact') def contact(): return render_template('contact.html', CONTACT=CONTACT) @@ -465,32 +404,5 @@ def about(): def index(): return render_template('index.html') -# ---- patched helpers (appended) ---- -def _verify_hub_signature(raw: bytes) -> None: - if not HUB_PUSH_SECRET: - abort(403, description="Missing HUB_PUSH_SECRET") - - # Accept both header names and both formats - sig_hdr = (request.headers.get("X-Signature") - or request.headers.get("X-Hub-Signature-256") - or "").strip() - - provided = sig_hdr - if provided.lower().startswith("sha256="): - provided = provided.split("=", 1)[1].strip() - - # must be a 64-char hex, no whitespace - if not provided or any(c.isspace() for c in provided) or len(provided) != 64: - abort(403, description="Bad signature format") - - expected = hmac.new(HUB_PUSH_SECRET.encode(), raw, hashlib.sha256).hexdigest() - if not hmac.compare_digest(provided, expected): - abort(403, description="Signature mismatch") - - ts = request.headers.get("X-Timestamp") - if ts and ts.isdigit() and abs(time.time() - int(ts)) > 300: - abort(403, description="Stale request") - -def _to_slug(text: str) -> str: - s = slugify(text or "") - return s or f"post-{int(time.time())}" +if __name__ == '__main__': + app.run(debug=True) diff --git a/instance/posts.db b/instance/posts.db index d0d2653..a884df0 100644 Binary files a/instance/posts.db and b/instance/posts.db differ diff --git a/requirements.txt b/requirements.txt index 73710d1..2668675 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ flask-mail python-dotenv python-slugify werkzeug +requests +markdown \ No newline at end of file diff --git a/static/Headshot.jpeg b/static/Headshot.jpeg new file mode 100644 index 0000000..52af3b2 Binary files /dev/null and b/static/Headshot.jpeg differ diff --git a/static/Headshot.jpg b/static/Headshot.jpg deleted file mode 100644 index af7760e..0000000 Binary files a/static/Headshot.jpg and /dev/null differ diff --git a/templates/.index.html.swp b/templates/.index.html.swp deleted file mode 100644 index 92102b6..0000000 Binary files a/templates/.index.html.swp and /dev/null differ diff --git a/templates/admin.html b/templates/admin.html index 01fa9b8..7288441 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -32,11 +32,21 @@ {% for projectpost in projectpost %} - {{ projectpost.title }} + + {{ projectpost.title }} + {% if projectpost.draft %} + Draft + {% endif %} + {{ projectpost.category }} - View + Preview Edit + {% if projectpost.draft %} +
+ +
+ {% endif %}
@@ -74,11 +84,21 @@ {% for blogpost in blogpost %} - {{ blogpost.title }} + + {{ blogpost.title }} + {% if blogpost.draft %} + Draft + {% endif %} + {{ blogpost.category }} - View + Preview Edit + {% if blogpost.draft %} +
+ +
+ {% endif %}
diff --git a/templates/edit_blog.html b/templates/edit_blog.html index 1db1852..8f78194 100644 --- a/templates/edit_blog.html +++ b/templates/edit_blog.html @@ -1,29 +1,111 @@ {% extends "base.html" %} -{% block title %}Edit Blog{% endblock %} +{% block title %}Edit — {{ blogpost.title }}{% endblock %} + +{% block head %} + +{% endblock %} {% block content %} -
- - + +
+

Edit Blog Post

+

{{ blogpost.title }}

+
- - + +
+
+ - - + +
+ + +
- - + +
+ + +
+ +
+ + +
- - + +
+ + +
- - + +
+ + +
+ + {% if blogpost.images %} + {% set imgs = blogpost.images|from_json %} + {% if imgs %} +
+

Current Images

+
+ {% for img in imgs %} +
+ {{ img }} +

{{ img }}

+
+ {% endfor %} +
+
+ {% endif %} + {% endif %} + +
+ + +

New images are added alongside existing ones.

+
+ + +
+ + + Cancel + +
+ +
+
+ + + {% endblock %} diff --git a/templates/edit_project.html b/templates/edit_project.html index ee78603..4d5cec0 100644 --- a/templates/edit_project.html +++ b/templates/edit_project.html @@ -1,26 +1,111 @@ {% extends "base.html" %} -{% block title %}Edit Project{% endblock %} +{% block title %}Edit — {{ projectpost.title }}{% endblock %} + +{% block head %} + +{% endblock %} {% block content %} -
- - - - + +
+

Edit Project Post

+

{{ projectpost.title }}

+
- - + +
+
+ - - + +
+ + +
+ +
+ + +
- - + +
+ + +
- - + +
+ + +
+ + +
+ + +
+ + + {% if projectpost.images %} + {% set imgs = projectpost.images|from_json %} + {% if imgs %} +
+

Current Images

+
+ {% for img in imgs %} +
+ {{ img }} +

{{ img }}

+
+ {% endfor %} +
+
+ {% endif %} + {% endif %} + + +
+ + +

New images are added alongside existing ones.

+
+ + +
+ + + Cancel + +
+ +
+
+ + + {% endblock %} diff --git a/templates/index.html b/templates/index.html index f28a41e..6516b7f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -24,7 +24,7 @@
- Portrait of Benjamin Mosley
diff --git a/templates/new_blog.html b/templates/new_blog.html index 0d09497..c968ad8 100644 --- a/templates/new_blog.html +++ b/templates/new_blog.html @@ -2,6 +2,10 @@ {% block title %}New Blog Post{% endblock %} +{% block head %} + +{% endblock %} + {% block content %} @@ -12,21 +16,21 @@
-
+
+ class="text-black bg-white w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
@@ -34,14 +38,14 @@
+ class="text-black bg-white w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
+ class="text-black bg-white w-full border border-gray-300 rounded px-4 py-2 focus:ring-2 focus:ring-orange-400 focus:outline-none">
@@ -52,10 +56,14 @@
-
- +
@@ -67,18 +75,13 @@
- + diff --git a/templates/new_project.html b/templates/new_project.html index 6712b5e..d9d83ca 100644 --- a/templates/new_project.html +++ b/templates/new_project.html @@ -2,6 +2,10 @@ {% block title %}New Project Post{% endblock %} +{% block head %} + +{% endblock %} + {% block content %} @@ -12,7 +16,7 @@
-
+
@@ -52,10 +56,14 @@
-
- +
@@ -66,18 +74,13 @@
- + diff --git a/templates/resume.html b/templates/resume.html index b4fa409..09ad78c 100644 --- a/templates/resume.html +++ b/templates/resume.html @@ -7,31 +7,30 @@

Benjamin Mosley

-

E-Commerce Manager - CIS Student

+

IT Support · Network & Systems · CIS Student

- Canyon, TX 79015 + Amarillo, TX 79119 benjaymos@proton.me benjaminmosley.com - {# Optional: add a real static path if you place the PDF under /static/ #} - {# Download PDF #}
- -
+ +
-

About Me

-

- I’ve been working with computers for as long as I can remember—what started as curiosity has grown into a passion. - From backend scripting and networking to frontend design, I enjoy creating efficient and user-friendly solutions. -

+

Leadership & Awards

+
    +
  • Carey Lyles Emerging Technology Award — West Texas A&M University (2026)
  • +
  • Eagle Scout (Dec 2021)
  • +
  • National Youth Leadership Training (2017–2019)
  • +
- +
@@ -40,40 +39,52 @@
-

Programming

+

Endpoint & Systems

    +
  • PC imaging & deployment
  • +
  • Windows Server / AD
  • +
  • Microsoft 365 / Entra ID
  • +
  • Endpoint patching
  • +
  • Asset tracking
  • +
+
+ +
+

Networking

+
    +
  • Switch & AP config
  • +
  • Firewall rules
  • +
  • Fixed wireless (Cambium)
  • +
  • Fiber / Adtran
  • +
  • TCP/IP, DNS, DHCP
  • +
+
+ +
+

Scripting & Dev

+
    +
  • PowerShell / Bash
  • Python
  • -
  • C#, ASP.NET Core
  • -
-
- -
-

Web & Database

-
    -
  • Flask, Tailwind CSS
  • -
  • SQL (MySQL, SQLite), EF Core
  • -
-
- -
-

Systems

-
    -
  • Linux, Windows Server
  • -
  • Networking & Security
  • +
  • C# / ASP.NET
  • +
  • Flask / SQL
  • +
  • Git / GitHub

Professional

    -
  • Leadership & Team Ops
  • -
  • Process Improvement
  • +
  • Ticketing & documentation
  • +
  • Root cause analysis
  • +
  • Vendor coordination
  • +
  • Team leadership
  • +
  • Process improvement
- +

Education

@@ -81,14 +92,6 @@

BBA in Computer Information Systems

Expected May 2026

- -
-

Accomplishments

-
    -
  • Eagle Scout (Dec 2021)
  • -
  • National Youth Leadership Training (2017–2019)
  • -
-
@@ -99,63 +102,79 @@

Experience

- +
-

E-Commerce Manager — United Supermarkets #532

-

Aug 2025 – Present

+

Level II Technical Support · AMA TechTel

+

Mar 2026 – Present

-

Canyon, TX

+

Amarillo, TX

    -
  • Own day-to-day online grocery operations (order flow, substitutions, delivery/pickup SLA).
  • -
  • Lead and schedule the e-commerce team; coach on accuracy, speed, and CX.
  • -
  • Monitor inventory/pricing anomalies and coordinate fixes with department leads.
  • -
  • Track KPIs (fill rate, OTIF, cancels, customer feedback) and drive process improvements.
  • -
  • Partner with IT/vendors to keep handhelds, printers, and store systems humming.
  • +
  • Provide L2 support for a CLEC network managing Cambium fixed wireless and Adtran fiber equipment; diagnose and resolve connectivity, hardware, and provisioning issues via inbound phone support.
  • +
  • Document cases, resolutions, and escalation paths in Rev.io ticketing system with consistent root-cause and resolution detail.
  • +
  • Coordinate escalations with fiber resellers and dispatch/on-site technicians for issues requiring physical remediation.
- +
-

IT Intern / Junior System Administrator — Hutchinson County Court House

+

IT Intern / Junior System Administrator · Hutchinson County Court House

May 2025 – Aug 2025

Stinnett, TX

    -
  • Assisted with Windows Server/Active Directory admin and routine maintenance.
  • -
  • Resolved helpdesk tickets; imaged PCs and handled hardware/software issues.
  • -
  • Helped configure switches/APs/firewall rules; supported backups and updates.
  • -
  • Contributed to security policy rollouts and small infrastructure upgrades.
  • +
  • Assisted with Windows Server and Active Directory administration — user accounts, GPO, and routine maintenance.
  • +
  • Resolved helpdesk tickets; imaged and deployed PCs; handled hardware and software troubleshooting.
  • +
  • Configured managed switches, wireless APs, and firewall rules in a county government (regulated) environment; supported patching, backups, and security policy rollouts.
- +
-

United Supermarkets & The Pergola Shop — Various Roles

-

2019 – 2025

+

E-Commerce Dept. Manager & Various Roles · United Supermarkets

+

Nov 2019 – Mar 2026

+

Canyon & Borger, TX

    -
  • Bookkeeper, Lead Stocker, Wood Cutter — leadership, reliability, and strong customer service.
  • +
  • Advanced from Sacker (2019) → Grocery Team Lead (2022), then Service Desk Clerk/Bookkeeper during college.
  • +
  • Promoted to E-Commerce Department Manager (Aug 2025 – Mar 2026); owned online order flow, delivery/pickup SLA, team scheduling, and KPI tracking (fill rate, OTIF, cancellations).
  • +
  • Partnered with IT and vendors to maintain handheld devices, printers, and store systems; first point of contact for technology issues on the floor.
- +
-
-

Accomplishments

-
    -
  • Eagle Scout (Dec 2021)
  • -
  • National Youth Leadership Training (2017–2019)
  • -
+
+

Projects & Homelab

+ + +
+

Proxmox Homelab — Windows Server / Active Directory & Linux

+
    +
  • Self-managed Proxmox host running a Windows Server VM with AD DS and a Linux NAS providing Samba, Gitea, and Syncthing; configured Entra ID Connect to sync on-prem AD to a Microsoft 365 developer tenant.
  • +
  • Manages VM provisioning, snapshots, network segmentation, patching, and a self-hosted credential vault with client-side encryption (security-first design practice).
  • +
+
+ + +
+
+

BillFlow — Self-Hosted SaaS Invoicing Platform

+ netdeploy.net +
+
    +
  • Built and deployed a multi-tenant invoicing platform (Flask/SQLite, nginx/Gunicorn, Vultr VPS); managed full deployment lifecycle including TLS, reverse proxy config, and ongoing maintenance.
  • +
+
diff --git a/templates/view_blog.html b/templates/view_blog.html index d544cca..3e8d1ea 100644 --- a/templates/view_blog.html +++ b/templates/view_blog.html @@ -3,6 +3,22 @@ {% block content %} +{% if blogpost.draft %} +
+ ⚠ This post is a draft — only you can see it. +
+ Edit +
+ +
+
+
+{% endif %} +
@@ -50,7 +66,7 @@
- {{ blogpost.content | safe }} + {{ blogpost.content | markdown | safe }}
diff --git a/templates/view_project.html b/templates/view_project.html index bbdf19c..509211a 100644 --- a/templates/view_project.html +++ b/templates/view_project.html @@ -3,6 +3,22 @@ {% block content %} +{% if projectpost.draft %} +
+ ⚠ This post is a draft — only you can see it. +
+ Edit +
+ +
+
+
+{% endif %} +
@@ -48,7 +64,7 @@
- {{ projectpost.content | safe }} + {{ projectpost.content | markdown | safe }}