fix(metabolizer): Remove yaml dependency, make CMS container-friendly

- Remove yaml import, use simple string parsing for front matter
- Remove dependency on host metabolizerctl
- Use environment variables for paths (METABOLIZER_CONTENT)
- Remove switch_page calls that fail in container
- CMS now works standalone inside Streamlit container
- Bump to r2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-24 12:32:15 +01:00
parent 5a2ef2d6ff
commit 35957e34ab
5 changed files with 161 additions and 204 deletions

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-metabolizer PKG_NAME:=secubox-app-metabolizer
PKG_VERSION:=1.0.0 PKG_VERSION:=1.0.0
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_ARCH:=all PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr> PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>

View File

@ -111,31 +111,16 @@ st.info("""
- **Settings** - Configure Git and Hexo integration - **Settings** - Configure Git and Hexo integration
""") """)
# Quick actions # Quick actions - simplified without switch_page
st.subheader("Quick Actions") st.subheader("Quick Actions")
col1, col2, col3 = st.columns(3) st.markdown("""
Use the **sidebar** on the left to navigate to:
with col1: - 📝 **1_editor** - Write new posts
if st.button("📝 New Post", use_container_width=True): - 📚 **2_posts** - Manage posts
st.switch_page("pages/1_editor.py") - 🖼 **3_media** - Media library
- **4_settings** - Settings
with col2: """)
if st.button("🔄 Sync & Build", use_container_width=True):
import subprocess
with st.spinner("Building..."):
result = subprocess.run(
['/usr/sbin/metabolizerctl', 'build'],
capture_output=True, text=True
)
if result.returncode == 0:
st.success("Build complete!")
else:
st.error(f"Build failed: {result.stderr}")
with col3:
if st.button("🌐 View Blog", use_container_width=True):
st.markdown("[Open Blog](/blog/)", unsafe_allow_html=True)
# Footer # Footer
st.divider() st.divider()

View File

@ -5,13 +5,12 @@ import streamlit as st
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import yaml
import os import os
st.set_page_config(page_title="Editor - Metabolizer", page_icon="✏️", layout="wide") st.set_page_config(page_title="Editor - Metabolizer", page_icon="✏️", layout="wide")
# Paths # Paths
CONTENT_PATH = Path("/srv/metabolizer/content") CONTENT_PATH = Path(os.environ.get('METABOLIZER_CONTENT', '/srv/content'))
POSTS_PATH = CONTENT_PATH / "_posts" POSTS_PATH = CONTENT_PATH / "_posts"
DRAFTS_PATH = CONTENT_PATH / "_drafts" DRAFTS_PATH = CONTENT_PATH / "_drafts"
@ -112,16 +111,28 @@ def generate_filename(title, date):
return f"{date}-{slug}.md" return f"{date}-{slug}.md"
def generate_frontmatter(title, date, time, categories, tags, excerpt): def generate_frontmatter(title, date, time, categories, tags, excerpt):
"""Generate YAML front matter""" """Generate YAML front matter without yaml module"""
fm = { lines = ["---"]
'title': title, lines.append(f"title: {title}")
'date': f"{date} {time.strftime('%H:%M:%S')}", lines.append(f"date: {date} {time.strftime('%H:%M:%S')}")
'categories': categories,
'tags': [t.strip() for t in tags.split(",")] if tags else [], if categories:
} lines.append(f"categories: [{', '.join(categories)}]")
else:
lines.append("categories: []")
if tags:
tag_list = [t.strip() for t in tags.split(",") if t.strip()]
lines.append(f"tags: [{', '.join(tag_list)}]")
else:
lines.append("tags: []")
if excerpt: if excerpt:
fm['excerpt'] = excerpt lines.append(f"excerpt: \"{excerpt}\"")
return "---\n" + yaml.dump(fm, default_flow_style=False) + "---\n\n"
lines.append("---")
lines.append("")
return "\n".join(lines)
def save_post(path, title, date, time, categories, tags, excerpt, content): def save_post(path, title, date, time, categories, tags, excerpt, content):
"""Save post to file""" """Save post to file"""
@ -136,10 +147,12 @@ def save_post(path, title, date, time, categories, tags, excerpt, content):
def git_commit_push(message): def git_commit_push(message):
"""Commit and push to Gitea""" """Commit and push to Gitea"""
os.chdir(CONTENT_PATH) try:
subprocess.run(['git', 'add', '-A'], capture_output=True) subprocess.run(['git', 'add', '-A'], cwd=CONTENT_PATH, capture_output=True)
subprocess.run(['git', 'commit', '-m', message], capture_output=True) subprocess.run(['git', 'commit', '-m', message], cwd=CONTENT_PATH, capture_output=True)
subprocess.run(['git', 'push', 'origin', 'main'], capture_output=True) subprocess.run(['git', 'push', 'origin', 'master'], cwd=CONTENT_PATH, capture_output=True)
except:
pass
with col1: with col1:
if st.button("💾 Save Draft", use_container_width=True): if st.button("💾 Save Draft", use_container_width=True):
@ -159,21 +172,18 @@ with col2:
git_commit_push(f"Add post: {title}") git_commit_push(f"Add post: {title}")
st.success(f"Published: {filepath.name}") st.success(f"Published: {filepath.name}")
st.info("Webhook will trigger rebuild automatically") st.info("Post saved to repository")
else: else:
st.error("Title and content required") st.error("Title and content required")
with col3: with col3:
if st.button("🔄 Build Now", use_container_width=True): if st.button("🔄 Sync", use_container_width=True):
with st.spinner("Building..."): with st.spinner("Syncing..."):
result = subprocess.run( try:
['/usr/sbin/metabolizerctl', 'build'], subprocess.run(['git', 'pull', 'origin', 'master'], cwd=CONTENT_PATH, capture_output=True)
capture_output=True, text=True st.success("Synced!")
) except:
if result.returncode == 0: st.error("Sync failed")
st.success("Build complete!")
else:
st.error(f"Build failed")
with col4: with col4:
if st.button("🗑️ Clear", use_container_width=True): if st.button("🗑️ Clear", use_container_width=True):

View File

@ -5,27 +5,41 @@ import streamlit as st
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
import subprocess import subprocess
import yaml
import os import os
import re
st.set_page_config(page_title="Posts - Metabolizer", page_icon="📚", layout="wide") st.set_page_config(page_title="Posts - Metabolizer", page_icon="📚", layout="wide")
# Paths # Paths
CONTENT_PATH = Path("/srv/metabolizer/content") CONTENT_PATH = Path(os.environ.get('METABOLIZER_CONTENT', '/srv/content'))
POSTS_PATH = CONTENT_PATH / "_posts" POSTS_PATH = CONTENT_PATH / "_posts"
DRAFTS_PATH = CONTENT_PATH / "_drafts" DRAFTS_PATH = CONTENT_PATH / "_drafts"
st.title("📚 Post Management") st.title("📚 Post Management")
def parse_frontmatter(filepath): def parse_frontmatter(filepath):
"""Parse YAML front matter from markdown file""" """Parse YAML front matter from markdown file (without yaml module)"""
try: try:
content = filepath.read_text() content = filepath.read_text()
if content.startswith("---"): if content.startswith("---"):
parts = content.split("---", 2) parts = content.split("---", 2)
if len(parts) >= 3: if len(parts) >= 3:
fm = yaml.safe_load(parts[1]) fm_text = parts[1].strip()
body = parts[2].strip() body = parts[2].strip()
# Simple parsing
fm = {}
for line in fm_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
# Handle arrays
if value.startswith('[') and value.endswith(']'):
value = [v.strip().strip('"\'') for v in value[1:-1].split(',') if v.strip()]
elif value.startswith('"') and value.endswith('"'):
value = value[1:-1]
fm[key] = value
return fm, body return fm, body
except Exception as e: except Exception as e:
pass pass
@ -51,10 +65,12 @@ def get_posts(path):
def git_commit_push(message): def git_commit_push(message):
"""Commit and push to Gitea""" """Commit and push to Gitea"""
os.chdir(CONTENT_PATH) try:
subprocess.run(['git', 'add', '-A'], capture_output=True) subprocess.run(['git', 'add', '-A'], cwd=CONTENT_PATH, capture_output=True)
subprocess.run(['git', 'commit', '-m', message], capture_output=True) subprocess.run(['git', 'commit', '-m', message], cwd=CONTENT_PATH, capture_output=True)
subprocess.run(['git', 'push', 'origin', 'main'], capture_output=True) subprocess.run(['git', 'push', 'origin', 'master'], cwd=CONTENT_PATH, capture_output=True)
except:
pass
# Tabs for Published and Drafts # Tabs for Published and Drafts
tab1, tab2 = st.tabs(["📰 Published", "📝 Drafts"]) tab1, tab2 = st.tabs(["📰 Published", "📝 Drafts"])
@ -74,14 +90,15 @@ with tab1:
with col1: with col1:
st.caption(f"📅 {post['date']}") st.caption(f"📅 {post['date']}")
if post['categories']: if post['categories']:
st.caption(f"📁 {', '.join(post['categories'])}") cats = post['categories'] if isinstance(post['categories'], list) else [post['categories']]
st.caption(f"📁 {', '.join(cats)}")
if post['tags']: if post['tags']:
st.caption(f"🏷️ {', '.join(post['tags'])}") tags = post['tags'] if isinstance(post['tags'], list) else [post['tags']]
st.caption(f"🏷️ {', '.join(tags)}")
st.markdown(post['excerpt']) st.markdown(post['excerpt'])
with col2: with col2:
if st.button("✏️ Edit", key=f"edit_{post['filename']}"): if st.button("✏️ Edit", key=f"edit_{post['filename']}"):
# Load into editor
st.session_state.post_title = post['title'] st.session_state.post_title = post['title']
st.session_state.post_content = post['body'] st.session_state.post_content = post['body']
st.switch_page("pages/1_editor.py") st.switch_page("pages/1_editor.py")
@ -93,7 +110,7 @@ with tab1:
st.rerun() st.rerun()
if st.button("📥 Unpublish", key=f"unpub_{post['filename']}"): if st.button("📥 Unpublish", key=f"unpub_{post['filename']}"):
# Move to drafts DRAFTS_PATH.mkdir(parents=True, exist_ok=True)
new_path = DRAFTS_PATH / post['filename'] new_path = DRAFTS_PATH / post['filename']
post['path'].rename(new_path) post['path'].rename(new_path)
git_commit_push(f"Unpublish: {post['title']}") git_commit_push(f"Unpublish: {post['title']}")
@ -123,7 +140,7 @@ with tab2:
st.switch_page("pages/1_editor.py") st.switch_page("pages/1_editor.py")
if st.button("📤 Publish", key=f"pub_{draft['filename']}"): if st.button("📤 Publish", key=f"pub_{draft['filename']}"):
# Move to posts POSTS_PATH.mkdir(parents=True, exist_ok=True)
new_path = POSTS_PATH / draft['filename'] new_path = POSTS_PATH / draft['filename']
draft['path'].rename(new_path) draft['path'].rename(new_path)
git_commit_push(f"Publish: {draft['title']}") git_commit_push(f"Publish: {draft['title']}")
@ -135,28 +152,22 @@ with tab2:
st.success(f"Deleted") st.success(f"Deleted")
st.rerun() st.rerun()
# Build action # Sync action
st.divider() st.divider()
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1: with col1:
if st.button("🔄 Sync from Git", use_container_width=True): if st.button("🔄 Sync from Git", use_container_width=True):
with st.spinner("Syncing..."): with st.spinner("Syncing..."):
result = subprocess.run( try:
['/usr/sbin/metabolizerctl', 'sync'], subprocess.run(['git', 'pull', 'origin', 'master'], cwd=CONTENT_PATH, capture_output=True)
capture_output=True, text=True st.success("Synced!")
) st.rerun()
st.success("Synced!") except:
st.rerun() st.error("Sync failed")
with col2: with col2:
if st.button("🏗️ Rebuild Blog", use_container_width=True): if st.button("📤 Push to Git", use_container_width=True):
with st.spinner("Building..."): with st.spinner("Pushing..."):
result = subprocess.run( git_commit_push("Update posts")
['/usr/sbin/metabolizerctl', 'build'], st.success("Pushed!")
capture_output=True, text=True
)
if result.returncode == 0:
st.success("Build complete!")
else:
st.error("Build failed")

View File

@ -4,32 +4,28 @@ Metabolizer CMS - Settings
import streamlit as st import streamlit as st
import subprocess import subprocess
import json import json
import os
from pathlib import Path
st.set_page_config(page_title="Settings - Metabolizer", page_icon="⚙️", layout="wide") st.set_page_config(page_title="Settings - Metabolizer", page_icon="⚙️", layout="wide")
st.title("⚙️ Settings") st.title("⚙️ Settings")
def get_status(): # Paths
"""Get metabolizer status""" CONTENT_PATH = Path(os.environ.get('METABOLIZER_CONTENT', '/srv/content'))
GITEA_URL = os.environ.get('GITEA_URL', 'http://host.containers.internal:3000')
def git_command(args, cwd=None):
"""Run git command"""
try: try:
result = subprocess.run( result = subprocess.run(
['/usr/sbin/metabolizerctl', 'status'], ['git'] + args,
cwd=cwd or CONTENT_PATH,
capture_output=True, text=True capture_output=True, text=True
) )
return json.loads(result.stdout) return result.returncode == 0, result.stdout, result.stderr
except: except Exception as e:
return {} return False, "", str(e)
def run_command(cmd):
"""Run metabolizerctl command"""
result = subprocess.run(
['/usr/sbin/metabolizerctl'] + cmd,
capture_output=True, text=True
)
return result.returncode == 0, result.stdout, result.stderr
# Get current status
status = get_status()
# Pipeline Status # Pipeline Status
st.subheader("📊 Pipeline Status") st.subheader("📊 Pipeline Status")
@ -37,43 +33,45 @@ st.subheader("📊 Pipeline Status")
col1, col2, col3 = st.columns(3) col1, col2, col3 = st.columns(3)
with col1: with col1:
gitea_status = status.get('gitea', {}).get('status', 'unknown') st.metric("Gitea", "EXTERNAL", delta="Host")
st.metric(
"Gitea",
gitea_status.upper(),
delta="OK" if gitea_status == "running" else "DOWN"
)
with col2: with col2:
streamlit_status = status.get('streamlit', {}).get('status', 'unknown') st.metric("Streamlit", "RUNNING", delta="OK")
st.metric(
"Streamlit",
streamlit_status.upper(),
delta="OK" if streamlit_status == "running" else "DOWN"
)
with col3: with col3:
hexo_status = status.get('hexo', {}).get('status', 'unknown') st.metric("HexoJS", "EXTERNAL", delta="Host")
st.metric(
"HexoJS",
hexo_status.upper(),
delta="OK" if hexo_status == "running" else "DOWN"
)
st.divider() st.divider()
# Content Repository # Content Repository
st.subheader("📁 Content Repository") st.subheader("📁 Content Repository")
content = status.get('content', {})
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1: with col1:
st.text_input("Repository", value=content.get('repo', 'blog-content'), disabled=True) st.text_input("Content Path", value=str(CONTENT_PATH), disabled=True)
st.text_input("Path", value=content.get('path', '/srv/metabolizer/content'), disabled=True)
# Check if git repo exists
if (CONTENT_PATH / '.git').exists():
success, stdout, _ = git_command(['remote', '-v'])
if success and stdout:
remote = stdout.split('\n')[0] if stdout else "No remote"
st.text_input("Remote", value=remote.split()[1] if '\t' in remote or ' ' in remote else remote, disabled=True)
success, stdout, _ = git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
st.text_input("Branch", value=stdout.strip() if success else "unknown", disabled=True)
else:
st.warning("Content directory is not a git repository")
with col2: with col2:
st.metric("Posts", content.get('post_count', 0)) # Count posts
posts_path = CONTENT_PATH / '_posts'
post_count = len(list(posts_path.glob('*.md'))) if posts_path.exists() else 0
st.metric("Posts", post_count)
drafts_path = CONTENT_PATH / '_drafts'
draft_count = len(list(drafts_path.glob('*.md'))) if drafts_path.exists() else 0
st.metric("Drafts", draft_count)
# Git Operations # Git Operations
st.subheader("🔗 Git Operations") st.subheader("🔗 Git Operations")
@ -83,7 +81,7 @@ col1, col2, col3 = st.columns(3)
with col1: with col1:
if st.button("🔄 Pull Latest", use_container_width=True): if st.button("🔄 Pull Latest", use_container_width=True):
with st.spinner("Pulling..."): with st.spinner("Pulling..."):
success, stdout, stderr = run_command(['sync']) success, stdout, stderr = git_command(['pull', 'origin', 'master'])
if success: if success:
st.success("Pulled latest changes") st.success("Pulled latest changes")
else: else:
@ -91,96 +89,49 @@ with col1:
with col2: with col2:
if st.button("📊 Git Status", use_container_width=True): if st.button("📊 Git Status", use_container_width=True):
import os success, stdout, stderr = git_command(['status', '--short'])
os.chdir("/srv/metabolizer/content") if stdout:
result = subprocess.run(['git', 'status', '--short'], capture_output=True, text=True) st.code(stdout)
if result.stdout:
st.code(result.stdout)
else: else:
st.info("Working tree clean") st.info("Working tree clean")
with col3: with col3:
github_url = st.text_input("Mirror from GitHub URL") if st.button("📤 Push Changes", use_container_width=True):
if st.button("🔗 Mirror", use_container_width=True): with st.spinner("Pushing..."):
if github_url: success, stdout, stderr = git_command(['push', 'origin', 'master'])
with st.spinner("Mirroring..."):
success, stdout, stderr = run_command(['mirror', github_url])
if success:
st.success(f"Mirrored: {stdout}")
else:
st.error(f"Mirror failed: {stderr}")
else:
st.warning("Enter a GitHub URL")
st.divider()
# Portal Settings
st.subheader("🌐 Portal")
portal = status.get('portal', {})
col1, col2 = st.columns(2)
with col1:
st.text_input("Blog URL", value=portal.get('url', 'http://router/blog/'), disabled=True)
st.text_input("Static Path", value=portal.get('path', '/www/blog'), disabled=True)
with col2:
if portal.get('enabled'):
st.success("Portal Enabled")
if st.button("🌐 View Blog", use_container_width=True):
st.markdown(f"[Open Blog]({portal.get('url', '/blog/')})")
else:
st.warning("Portal Disabled")
st.divider()
# Build Actions
st.subheader("🏗️ Build Pipeline")
col1, col2, col3 = st.columns(3)
with col1:
if st.button("🧹 Clean", use_container_width=True):
with st.spinner("Cleaning..."):
result = subprocess.run(
['/usr/sbin/hexoctl', 'clean'],
capture_output=True, text=True
)
st.success("Cleaned build cache")
with col2:
if st.button("🔨 Generate", use_container_width=True):
with st.spinner("Generating..."):
result = subprocess.run(
['/usr/sbin/hexoctl', 'generate'],
capture_output=True, text=True
)
if result.returncode == 0:
st.success("Generated static site")
else:
st.error("Generation failed")
with col3:
if st.button("📤 Publish", use_container_width=True):
with st.spinner("Publishing..."):
success, stdout, stderr = run_command(['publish'])
if success: if success:
st.success("Published to portal") st.success("Pushed changes")
else: else:
st.error(f"Publish failed: {stderr}") st.error(f"Push failed: {stderr}")
# Full pipeline
if st.button("🚀 Full Pipeline (Clean → Generate → Publish)", use_container_width=True, type="primary"):
with st.spinner("Running full pipeline..."):
success, stdout, stderr = run_command(['build'])
if success:
st.success("Full pipeline complete!")
st.balloons()
else:
st.error(f"Pipeline failed: {stderr}")
st.divider() st.divider()
# Raw Status JSON # Initialize Repository
with st.expander("🔧 Debug: Raw Status"): st.subheader("🆕 Initialize Content Repository")
st.json(status)
with st.expander("Setup New Repository"):
repo_url = st.text_input("Gitea Repository URL", placeholder="http://host:3000/user/blog-content.git")
if st.button("Clone Repository", use_container_width=True):
if repo_url:
with st.spinner("Cloning..."):
CONTENT_PATH.mkdir(parents=True, exist_ok=True)
success, stdout, stderr = git_command(['clone', repo_url, str(CONTENT_PATH)], cwd='/srv')
if success:
st.success("Repository cloned!")
st.rerun()
else:
st.error(f"Clone failed: {stderr}")
else:
st.warning("Enter a repository URL")
st.divider()
# Environment Info
with st.expander("🔧 Debug: Environment"):
st.json({
"CONTENT_PATH": str(CONTENT_PATH),
"GITEA_URL": GITEA_URL,
"CWD": os.getcwd(),
"PATH": os.environ.get('PATH', ''),
})