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_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>

View File

@ -111,31 +111,16 @@ st.info("""
- **Settings** - Configure Git and Hexo integration
""")
# Quick actions
# Quick actions - simplified without switch_page
st.subheader("Quick Actions")
col1, col2, col3 = st.columns(3)
with col1:
if st.button("📝 New Post", use_container_width=True):
st.switch_page("pages/1_editor.py")
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)
st.markdown("""
Use the **sidebar** on the left to navigate to:
- 📝 **1_editor** - Write new posts
- 📚 **2_posts** - Manage posts
- 🖼 **3_media** - Media library
- **4_settings** - Settings
""")
# Footer
st.divider()

View File

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

View File

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

View File

@ -4,32 +4,28 @@ Metabolizer CMS - Settings
import streamlit as st
import subprocess
import json
import os
from pathlib import Path
st.set_page_config(page_title="Settings - Metabolizer", page_icon="⚙️", layout="wide")
st.title("⚙️ Settings")
def get_status():
"""Get metabolizer status"""
# Paths
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:
result = subprocess.run(
['/usr/sbin/metabolizerctl', 'status'],
['git'] + args,
cwd=cwd or CONTENT_PATH,
capture_output=True, text=True
)
return json.loads(result.stdout)
except:
return {}
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()
return result.returncode == 0, result.stdout, result.stderr
except Exception as e:
return False, "", str(e)
# Pipeline Status
st.subheader("📊 Pipeline Status")
@ -37,43 +33,45 @@ st.subheader("📊 Pipeline Status")
col1, col2, col3 = st.columns(3)
with col1:
gitea_status = status.get('gitea', {}).get('status', 'unknown')
st.metric(
"Gitea",
gitea_status.upper(),
delta="OK" if gitea_status == "running" else "DOWN"
)
st.metric("Gitea", "EXTERNAL", delta="Host")
with col2:
streamlit_status = status.get('streamlit', {}).get('status', 'unknown')
st.metric(
"Streamlit",
streamlit_status.upper(),
delta="OK" if streamlit_status == "running" else "DOWN"
)
st.metric("Streamlit", "RUNNING", delta="OK")
with col3:
hexo_status = status.get('hexo', {}).get('status', 'unknown')
st.metric(
"HexoJS",
hexo_status.upper(),
delta="OK" if hexo_status == "running" else "DOWN"
)
st.metric("HexoJS", "EXTERNAL", delta="Host")
st.divider()
# Content Repository
st.subheader("📁 Content Repository")
content = status.get('content', {})
col1, col2 = st.columns(2)
with col1:
st.text_input("Repository", value=content.get('repo', 'blog-content'), disabled=True)
st.text_input("Path", value=content.get('path', '/srv/metabolizer/content'), disabled=True)
st.text_input("Content Path", value=str(CONTENT_PATH), 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:
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
st.subheader("🔗 Git Operations")
@ -83,7 +81,7 @@ col1, col2, col3 = st.columns(3)
with col1:
if st.button("🔄 Pull Latest", use_container_width=True):
with st.spinner("Pulling..."):
success, stdout, stderr = run_command(['sync'])
success, stdout, stderr = git_command(['pull', 'origin', 'master'])
if success:
st.success("Pulled latest changes")
else:
@ -91,96 +89,49 @@ with col1:
with col2:
if st.button("📊 Git Status", use_container_width=True):
import os
os.chdir("/srv/metabolizer/content")
result = subprocess.run(['git', 'status', '--short'], capture_output=True, text=True)
if result.stdout:
st.code(result.stdout)
success, stdout, stderr = git_command(['status', '--short'])
if stdout:
st.code(stdout)
else:
st.info("Working tree clean")
with col3:
github_url = st.text_input("Mirror from GitHub URL")
if st.button("🔗 Mirror", use_container_width=True):
if github_url:
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 st.button("📤 Push Changes", use_container_width=True):
with st.spinner("Pushing..."):
success, stdout, stderr = git_command(['push', 'origin', 'master'])
if success:
st.success("Published to portal")
st.success("Pushed changes")
else:
st.error(f"Publish 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.error(f"Push failed: {stderr}")
st.divider()
# Raw Status JSON
with st.expander("🔧 Debug: Raw Status"):
st.json(status)
# Initialize Repository
st.subheader("🆕 Initialize Content Repository")
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', ''),
})