Initial Commit

This commit is contained in:
2025-11-27 00:00:50 +00:00
commit b7e68a9057
43 changed files with 3445 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from flask import Blueprint
board_bp = Blueprint("board", __name__, template_folder="templates")

52
modules/board/routes.py Normal file
View File

@@ -0,0 +1,52 @@
import os, time, requests
GUILD = os.getenv("DISCORD_GUILD_ID","")
CHANNEL = os.getenv("DISCORD_CHANNEL_ID","")
WEBHOOK = os.getenv("DISCORD_WEBHOOK_URL","")
_cache = {"ts":0, "messages":[]}
TTL = 10
def _headers():
if not BOT: raise RuntimeError("Missing DISCORD_BOT_TOKEN")
return {"Authorization": f"Bot {BOT}"}
@board_bp.get("/")
@require_perms("board.view")
def index():
return render_template("board/index.html")
@board_bp.get("/api/messages")
@require_perms("board.view")
def api_messages():
now = time.time()
if now - _cache["ts"] < TTL and _cache["messages"]:
return jsonify(_cache["messages"])
url = f"{DISCORD_API}/channels/{CHANNEL}/messages"
r = requests.get(url, headers=_headers(), params={"limit":40}, timeout=10)
msgs = []
if r.status_code == 200:
for m in reversed(r.json()):
a = m.get("author", {})
msgs.append({
"id": m.get("id"),
"content": m.get("content",""),
"username": a.get("global_name") or a.get("username","user"),
"timestamp": m.get("timestamp"),
})
_cache.update({"ts":now, "messages":msgs})
return jsonify(msgs)
@board_bp.post("/api/post")
@require_perms("board.post")
def api_post():
data = request.get_json(force=True)
content = (data.get("content") or "").strip()
if not content: return jsonify({"ok":False,"error":"Empty"}),400
if not WEBHOOK: return jsonify({"ok":False,"error":"No webhook"}),500
r = requests.post(WEBHOOK, json={"content": content[:1800]}, timeout=10)
return (jsonify({"ok":True}), 200) if r.status_code in (200,204) else (jsonify({"ok":False}), 502)

View File

@@ -0,0 +1,41 @@
{% extends 'core/base.html' %}
{% block title %}Intercom — Portal{% endblock %}
{% block content %}
<section class="max-w-4xl mx-auto">
<header class="flex items-center justify-between mb-4">
<h1 class="text-2xl font-bold">Discord Intercom</h1>
<button id="refresh" class="btn">Refresh</button>
</header>
<div id="list" class="space-y-3"></div>
<div class="mt-6 card glass p-4">
<textarea id="composer" rows="3" class="w-full"></textarea>
<div class="mt-2 text-right"><button id="send" class="btn bg-accent font-semibold">Post</button></div>
</div>
</section>
<script>
async function load(){
const r = await fetch('/board/api/messages');
const data = await r.json();
const root = document.getElementById('list');
root.innerHTML = data.map(m=>`
<article class="card glass p-4">
<div class="text-sm text-white/60">${(m.timestamp||'').replace('T',' ').replace('Z',' UTC')}</div>
<div class="font-semibold">${m.username||'user'}</div>
<div class="mt-1 whitespace-pre-wrap">${m.content||''}</div>
</article>`).join('');
}
async function post(){
const v = document.getElementById('composer').value.trim();
if(!v) return;
const r = await fetch('/board/api/post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:v})});
if(r.ok){ document.getElementById('composer').value=''; load(); }
else alert('Not allowed or failed');
}
load();
setInterval(load,15000);
refresh.onclick = load;
send.onclick = post;
</script>
{% endblock %}

View File

@@ -0,0 +1,2 @@
from flask import Blueprint
memos_bp = Blueprint("memos", __name__, template_folder="templates")

27
modules/memos/models.py Normal file
View File

@@ -0,0 +1,27 @@
from core.models import db
from datetime import datetime
class Memo(db.Model):
__tablename__ = "memos"
id = db.Column(db.Integer, primary_key=True)
memo = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Note(db.Model):
__tablename__ = "notes"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), unique=True, nullable=False)
slug = db.Column(db.String(255), unique=True, nullable=False)
note = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Journal(db.Model):
__tablename__ = "journal"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), unique=True, nullable=False)
slug = db.Column(db.String(255), unique=True, nullable=False)
entry = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)

70
modules/memos/routes.py Normal file
View File

@@ -0,0 +1,70 @@
from flask import render_template, request, redirect, url_for
from . import memos_bp
from .models import db, Memo, Note, Journal
from core.auth import require_perms
@memos_bp.get("/")
@require_perms("memos.read")
def index():
memos = Memo.query.order_by(Memo.created_at.desc()).all()
return render_template("memos/index.html", memos=memos)
@memos_bp.post("/add")
@require_perms("memos.write")
def add():
v = (request.form.get("memo") or "").strip()
if v:
db.session.add(Memo(memo=v)); db.session.commit()
return redirect(url_for("memos.index"))
@memos_bp.get("/notes")
@require_perms("memos.read")
def notes():
notes = Note.query.order_by(Note.created_at.desc()).all()
return render_template("memos/notes.html", notes=notes)
@memos_bp.post("/notes/new")
@require_perms("memos.write")
def notes_new():
name = (request.form.get("name") or "").strip()
text = (request.form.get("note") or "").strip()
if name:
slug = name.lower().replace(" ","-")
db.session.add(Note(name=name, slug=slug, note=text)); db.session.commit()
return redirect(url_for("memos.notes"))
@memos_bp.get("/notes/<slug>")
@require_perms("memos.read")
def view_note(slug):
n = Note.query.filter_by(slug=slug).first_or_404()
return render_template("memos/view_note.html", note=n)
@memos_bp.get("/journal")
@require_perms("memos.read")
def journal():
entries = Journal.query.order_by(Journal.created_at.desc()).all()
return render_template("memos/journal.html", journal=entries)
@memos_bp.post("/journal/new")
@require_perms("memos.write")
def journal_new():
name = (request.form.get("name") or "").strip()
text = (request.form.get("entry") or "").strip()
if name:
slug = name.lower().replace(" ","-")
db.session.add(Journal(name=name, slug=slug, entry=text)); db.session.commit()
return redirect(url_for("memos.journal"))
@memos_bp.get("/journal/<slug>")
@require_perms("memos.read")
def view_journal(slug):
e = Journal.query.filter_by(slug=slug).first_or_404()
return render_template("memos/view_journal.html", entry=e)

View File

@@ -0,0 +1,18 @@
{% extends 'core/base.html' %}
{% block title %}Memos — Portal{% endblock %}
{% block content %}
<section class="max-w-3xl mx-auto">
<div class="card glass p-6">
<h1 class="text-2xl font-bold">Memos</h1>
<form method="post" action="/memos/add" class="mt-3 flex gap-2">
<input name="memo" class="flex-1" placeholder="New memo…">
<button class="btn">Add</button>
</form>
</div>
<div class="mt-6 space-y-3">
{% for m in memos %}
<article class="card glass p-4"><div class="text-sm text-white/60">{{ m.created_at }}</div><div class="mt-1">{{ m.memo }}</div></article>
{% endfor %}
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'core/base.html' %}
{% block title %}Journal — Portal{% endblock %}
{% block content %}
<section class="max-w-4xl mx-auto grid md:grid-cols-2 gap-6">
<div class="card glass p-6">
<h1 class="text-xl font-semibold">New Entry</h1>
<form method="post" action="/memos/journal/new" class="mt-3 grid gap-3">
<input name="name" placeholder="Entry title" required>
<textarea name="entry" rows="8" placeholder="Whats up…"></textarea>
<button class="btn bg-accent font-semibold">Save</button>
</form>
</div>
<div class="space-y-3">
{% for e in journal %}
<a href="/memos/journal/{{ e.slug }}" class="block card glass p-4">
<div class="text-sm text-white/60">{{ e.created_at }}</div>
<div class="font-semibold">{{ e.name }}</div>
</a>
{% endfor %}
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'core/base.html' %}
{% block title %}Notes — Portal{% endblock %}
{% block content %}
<section class="max-w-4xl mx-auto grid md:grid-cols-2 gap-6">
<div class="card glass p-6">
<h1 class="text-xl font-semibold">New Note</h1>
<form method="post" action="/memos/notes/new" class="mt-3 grid gap-3">
<input name="name" placeholder="Note title" required>
<textarea name="note" rows="6" placeholder="Text…"></textarea>
<button class="btn bg-accent font-semibold">Save</button>
</form>
</div>
<div class="space-y-3">
{% for n in notes %}
<a href="/memos/notes/{{ n.slug }}" class="block card glass p-4">
<div class="text-sm text-white/60">{{ n.created_at }}</div>
<div class="font-semibold">{{ n.name }}</div>
</a>
{% endfor %}
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends 'core/base.html' %}
{% block title %}{{ entry.name }} — Journal{% endblock %}
{% block content %}
<article class="max-w-3xl mx-auto card glass p-6">
<h1 class="text-2xl font-bold">{{ entry.name }}</h1>
<div class="mt-3 whitespace-pre-wrap">{{ entry.entry }}</div>
</article>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends 'core/base.html' %}
{% block title %}{{ note.name }} — Note{% endblock %}
{% block content %}
<article class="max-w-3xl mx-auto card glass p-6">
<h1 class="text-2xl font-bold">{{ note.name }}</h1>
<div class="mt-3 whitespace-pre-wrap">{{ note.note }}</div>
</article>
{% endblock %}

View File

@@ -0,0 +1,2 @@
from flask import Blueprint
publish_bp = Blueprint("publish", __name__, template_folder="templates")

View File

@@ -0,0 +1,9 @@
from flask import render_template
from . import publish_bp
from core.auth import require_perms
@publish_bp.get("/")
@require_perms("publish.use")
def index():
return render_template("publish/index.html")

View File

@@ -0,0 +1,51 @@
{% extends 'core/base.html' %}
{% block title %}Publish Once — Portal{% endblock %}
{% block content %}
<section class="grid lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 card glass p-6">
<h1 class="text-xl font-semibold">Compose</h1>
<div class="mt-3 grid gap-3">
<input id="title" placeholder="Title" class="w-full">
<input id="hero" placeholder="Hero image URL (optional)" class="w-full">
<input id="cta" placeholder="Canonical URL" class="w-full">
<textarea id="body" rows="8" placeholder="Write your story…" class="w-full"></textarea>
<input id="tags" placeholder="Tags (comma)" class="w-full">
</div>
</div>
<aside class="space-y-4">
<div class="card glass p-4">
<h2 class="font-semibold">Blog FrontMatter</h2>
<button class="btn mt-2" onclick="copy('front')">Copy</button>
<pre id="front" class="mt-2 text-xs"></pre>
</div>
<div class="card glass p-4">
<h2 class="font-semibold">Facebook (Group/Page)</h2>
<button class="btn mt-2" onclick="copy('fb')">Copy</button>
<pre id="fb" class="mt-2 text-xs"></pre>
</div>
<div class="card glass p-4">
<h2 class="font-semibold">Instagram / TikTok</h2>
<button class="btn mt-2" onclick="copy('ig')">Copy</button>
<pre id="ig" class="mt-2 text-xs"></pre>
</div>
</aside>
</section>
<script>
function slugify(t){return (t||'').toLowerCase().trim().replace(/[^a-z0-9\s-]/g,'').replace(/\s+/g,'-').replace(/-+/g,'-')}
function yaml(s){return '"'+String(s||'').replaceAll('"','\\"')+'"'}
function utm(u,src){try{const x=new URL(u);x.searchParams.set('utm_source',src);x.searchParams.set('utm_medium','social');x.searchParams.set('utm_campaign','portal');return x+''}catch(e){return u}}
async function copy(id){const t=document.getElementById(id).innerText; await navigator.clipboard.writeText(t)}
function render(){
const title=t.value, body=bd.value, hero=h.value, cta=c.value, tags=tg.value
const ex=(body||'').replace(/\s+/g,' ').trim(); const short=ex.length<=160?ex:ex.slice(0,159)+'…'
const slug=slugify(title||'post')
front.textContent = `---\ntitle: ${yaml(title)}\nslug: ${slug}\nhero: ${yaml(hero)}\ntags: [${(tags||'').split(',').map(s=>s.trim()).filter(Boolean).map(x=>yaml(x)).join(', ')}]\nexcerpt: ${yaml(short)}\n---`
fb.textContent = `${title}\n\n${short}\n\nRead more → ${utm(cta,'facebook')}`
const hashtags=(tags||'').split(',').map(s=>s.trim()).filter(Boolean).map(x=>'#'+slugify(x)).slice(0,12).join(' ')
ig.textContent = `${title}\n\n${short}\n\n${hashtags}\n\nLink: ${utm(cta,'instagram')}`
}
const t=title, h=hero, c=cta, bd=body, tg=tags; [t,h,c,bd,tg].forEach(el=>el.addEventListener('input',render)); render()
</script>
{% endblock %}

View File

@@ -0,0 +1,2 @@
from flask import Blueprint
quotes_bp = Blueprint("quotes", __name__, template_folder="templates")

26
modules/quotes/routes.py Normal file
View File

@@ -0,0 +1,26 @@
from flask import render_template, request, redirect, url_for, flash
from . import quotes_bp
from core.auth import require_perms
@quotes_bp.get("/")
def index():
return render_template("quotes/index.html")
@quotes_bp.post("/estimate")
def estimate():
name = request.form.get("name","")
email = request.form.get("email","")
need = request.form.get("need","not-sure")
size = request.form.get("size","small")
hours = {"simple":10,"pro":18,"custom":28}.get(need,8) * {"small":1,"medium":1.4,"large":2}.get(size,1)
cost = round(hours*95,2)
flash(f"Estimated {hours:.1f}h — ${cost}", "ok")
return redirect(url_for("quotes.index"))
@quotes_bp.get("/admin")
@require_perms("quotes.admin")
def admin():
return render_template("quotes/admin.html")

View File

@@ -0,0 +1,6 @@
{% extends 'core/base.html' %}
{% block title %}Quotes Admin — Portal{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold mb-4">Quotes Admin</h1>
<p class="text-white/70">Wire your DB-backed list here later.</p>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'core/base.html' %}
{% block title %}Quotes — Portal{% endblock %}
{% block content %}
<section class="max-w-3xl mx-auto">
<div class="card glass p-6">
<h1 class="text-2xl font-bold">Quick Estimate</h1>
<form method="post" action="/quotes/estimate" class="mt-4 grid gap-3">
<input name="name" placeholder="Your name" required>
<input name="email" type="email" placeholder="Email" required>
<select name="need">
<option value="simple">Simple site</option>
<option value="pro">Pro site</option>
<option value="custom">Custom app</option>
</select>
<select name="size">
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
<button class="btn bg-accent font-semibold">Estimate</button>
</form>
</div>
</section>
{% endblock %}