decommission: Remove Decap CMS entirely

Decap CMS didn't work for any of us — clunky UI, exposed YAML frontmatter,
bad mobile experience. Task management moved to PostgreSQL + Discord ChatOps
+ Trinity Console. Document browsing happens via Gitea directly.

Removed: admin/config.yml, admin/index.html, admin/mobile.html, admin/tasks.html

Chronicler #78 | firefrost-website
This commit is contained in:
Claude
2026-04-11 14:28:00 +00:00
parent f98489c9bc
commit d134445977
4 changed files with 0 additions and 1577 deletions

View File

@@ -1,384 +0,0 @@
backend:
name: gitea
repo: firefrost-gaming/firefrost-operations-manual
api_root: https://git.firefrostgaming.com/api/v1
base_url: https://git.firefrostgaming.com
app_id: ad439d72-e724-4f88-ad24-a1187c52b313
use_pkce: true
branch: master
auth_endpoint: login/oauth/authorize
token_endpoint: login/oauth/access_token
# Site settings
site_url: https://firefrostgaming.com
display_url: https://firefrostgaming.com
logo_url: /assets/images/2026/02/Light-logo.png
# Disable preview pane globally (dark mode compatibility - font/background color issue)
editor:
preview: false
# Media library points to branding assets
media_folder: "docs/branding"
public_folder: "/branding"
collections:
# ═══════════════════════════════════════════════════════════
# CORE DOCUMENTS (Single Files)
# ═══════════════════════════════════════════════════════════
- name: "core_docs"
label: "📌 Core Documents"
files:
- label: "Session Handoff"
name: "handoff"
file: "SESSION-HANDOFF-NEXT.md"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- label: "Infrastructure Manifest"
name: "infrastructure"
file: "docs/core/infrastructure-manifest.md"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- label: "Document Index"
name: "doc-index"
file: "DOCUMENT-INDEX.md"
fields:
- {label: "Content", name: "body", widget: "markdown"}
# ═══════════════════════════════════════════════════════════
# PLANNING & STRATEGY
# ═══════════════════════════════════════════════════════════
- name: "planning"
label: "📋 Planning"
folder: "docs/planning"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "milestones"
label: "🏆 Milestones"
folder: "docs/milestones"
create: true
extension: "md"
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
identifier_field: "name"
summary: "{{filename}}"
sortable_fields: ["filename"]
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "vision"
label: "🔮 Vision"
folder: "docs/vision"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
# ═══════════════════════════════════════════════════════════
# TECHNICAL DOCUMENTATION
# ═══════════════════════════════════════════════════════════
- name: "implementation"
label: "🔧 Implementation Guides"
folder: "docs/implementation"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "infrastructure"
label: "🖥️ Infrastructure"
folder: "docs/infrastructure"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "deployment"
label: "🚀 Deployment"
folder: "docs/deployment"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "services"
label: "⚙️ Services"
folder: "docs/services"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "troubleshooting"
label: "🔍 Troubleshooting"
folder: "docs/troubleshooting"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
# ═══════════════════════════════════════════════════════════
# PROCEDURES & STANDARDS
# ═══════════════════════════════════════════════════════════
- name: "procedures"
label: "📝 Procedures"
folder: "docs/procedures"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "standards"
label: "📏 Standards"
folder: "docs/standards"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "guides"
label: "📖 Guides"
folder: "docs/guides"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "templates"
label: "📄 Templates"
folder: "docs/templates"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
# ═══════════════════════════════════════════════════════════
# RELATIONSHIP & CHRONICLERS
# ═══════════════════════════════════════════════════════════
- name: "relationship"
label: "💜 Relationship"
folder: "docs/relationship"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "sessions"
label: "📅 Sessions"
folder: "docs/sessions"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
# ═══════════════════════════════════════════════════════════
# MARKETING & SOCIAL
# ═══════════════════════════════════════════════════════════
- name: "marketing"
label: "📣 Marketing"
folder: "docs/marketing"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "social-media"
label: "📱 Social Media"
folder: "docs/social-media"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "branding"
label: "🎨 Branding"
folder: "docs/branding"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "branding_assets"
label: "🎨 Branding Assets"
files:
- label: "Trinity Final (PNG)"
name: "trinity-final"
file: "docs/branding/trinity-final.png"
fields:
- {label: "Image", name: "image", widget: "image"}
- label: "Trinity Fixed (WebP)"
name: "trinity-fixed"
file: "docs/branding/trinity-fixed.webp"
fields:
- {label: "Image", name: "image", widget: "image"}
- label: "Trinity Image (WebP)"
name: "trinity-image"
file: "docs/branding/trinity-image.webp"
fields:
- {label: "Image", name: "image", widget: "image"}
- label: "YouTube Banner"
name: "youtube-banner"
file: "docs/branding/youtube-banner-2560x1440.png"
fields:
- {label: "Image", name: "image", widget: "image"}
- label: "YouTube Banner (Minecraft)"
name: "youtube-banner-minecraft"
file: "docs/branding/youtube-banner-minecraft-2560x1440.png"
fields:
- {label: "Image", name: "image", widget: "image"}
- name: "trinity_skins"
label: "🎮 Trinity Skins"
files:
- label: "Frost Wizard (Frostystyle)"
name: "frost-wizard"
file: "docs/branding/trinity-skins/frost-wizard-frostystyle.png"
fields:
- {label: "Image", name: "image", widget: "image"}
- label: "Fire Emissary (Gingerfury)"
name: "fire-emissary"
file: "docs/branding/trinity-skins/fire-emissary-gingerfury.png"
fields:
- {label: "Image", name: "image", widget: "image"}
- label: "Arcane Catalyst (Unicorn20089)"
name: "arcane-catalyst"
file: "docs/branding/trinity-skins/arcane-catalyst-unicorn20089.png"
fields:
- {label: "Image", name: "image", widget: "image"}
- label: "Skin Viewer"
name: "skin-viewer"
file: "docs/branding/trinity-skins/minecraft_skin_viewer.html"
fields:
- {label: "Content", name: "body", widget: "code"}
# ═══════════════════════════════════════════════════════════
# LEGAL & EMERGENCY
# ═══════════════════════════════════════════════════════════
- name: "legal"
label: "⚖️ Legal"
folder: "docs/legal"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "emergency"
label: "🚨 Emergency Protocols"
folder: "docs/emergency-protocols"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
# ═══════════════════════════════════════════════════════════
# REFERENCE & RESEARCH
# ═══════════════════════════════════════════════════════════
- name: "reference"
label: "📚 Reference"
folder: "docs/reference"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "research"
label: "🔬 Research"
folder: "docs/research"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "consultations"
label: "💬 Consultations"
folder: "docs/consultations"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
# ═══════════════════════════════════════════════════════════
# TOOLS & TRAINING
# ═══════════════════════════════════════════════════════════
- name: "tools"
label: "🛠️ Tools"
folder: "docs/tools"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "training"
label: "🎓 Training"
folder: "docs/training"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}
- name: "learning"
label: "📝 Learning"
folder: "docs/learning"
create: true
extension: "md"
identifier_field: "name"
summary: "{{filename}}"
fields:
- {label: "Content", name: "body", widget: "markdown"}

View File

@@ -1,223 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>Firefrost CMS</title>
<style>
/* ═══════════════════════════════════════════════════════════
FIREFROST GAMING - DECAP CMS MOBILE OPTIMIZATIONS
═══════════════════════════════════════════════════════════ */
/* Basic mobile viewport setup */
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* ═══════════════════════════════════════════════════════════
📱 MOBILE RESPONSIVE OPTIMIZATIONS
═══════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
/* Sidebar - Show in main content area on mobile (not hidden) */
aside,
[class*="Sidebar"],
nav[class*="Sidebar"] {
position: static !important;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
border: none !important;
margin: 0 !important;
padding: 8px !important;
}
/* Collections list items - More compact */
aside li,
aside a,
[class*="Sidebar"] li,
[class*="Sidebar"] a {
padding: 8px 12px !important;
font-size: 14px !important;
line-height: 1.4 !important;
margin: 4px 0 !important;
}
/* Collections heading - Smaller */
aside h2,
[class*="Sidebar"] h2 {
font-size: 18px !important;
margin: 8px 0 !important;
padding: 0 !important;
}
/* Search box - More compact */
aside input,
[class*="Sidebar"] input {
padding: 8px !important;
font-size: 14px !important;
}
/* Main content takes full width */
main,
[class*="MainContent"],
[class*="Content"] {
margin-left: 0 !important;
width: 100% !important;
padding: 16px 12px !important;
}
/* Header adjustments */
header,
[class*="AppHeader"] {
padding: 12px !important;
}
/* Collection cards - Stack vertically, larger touch targets */
[class*="ListCard"],
[class*="EntryCard"],
li[class*="Entry"] {
padding: 16px 12px !important;
margin: 12px 0 !important;
min-height: 60px !important;
}
/* Buttons - Larger touch targets */
button {
min-height: 44px !important;
padding: 12px 20px !important;
font-size: 16px !important;
}
/* Input fields - Larger, easier to tap */
input,
textarea,
select {
min-height: 44px !important;
font-size: 16px !important;
padding: 10px 12px !important;
}
/* Text areas - More vertical space */
textarea {
min-height: 120px !important;
}
/* Search box */
input[type="search"],
input[placeholder*="Search"] {
width: 100% !important;
margin: 8px 0 !important;
}
/* Collection grid - Single column on mobile */
[class*="CardGrid"],
[class*="Grid"] {
grid-template-columns: 1fr !important;
}
/* Editor - Full width, no side-by-side */
[class*="EditorContainer"],
[class*="SplitPane"] {
flex-direction: column !important;
}
[class*="ControlPane"],
[class*="PreviewPane"] {
width: 100% !important;
max-width: 100% !important;
}
/* Toolbar - Wrap items, don't overflow */
[class*="Toolbar"],
[class*="EditorToolbar"] {
flex-wrap: wrap !important;
padding: 8px !important;
}
[class*="Toolbar"] button {
margin: 4px !important;
}
/* Dropdowns and modals - Full width */
[class*="Dropdown"],
[class*="Modal"],
[class*="Dialog"] {
max-width: 95vw !important;
margin: 0 auto !important;
}
/* Collection header */
h1 {
font-size: 24px !important;
}
h2 {
font-size: 20px !important;
}
h3 {
font-size: 18px !important;
}
/* List items - Better spacing */
ul[class*="List"] > li {
margin: 12px 0 !important;
}
/* Reduce padding on small screens */
[class*="Container"],
[class*="Wrapper"] {
padding: 12px !important;
}
/* Form fields - Stack vertically */
[class*="FormControl"],
[class*="FieldWrapper"] {
margin-bottom: 20px !important;
}
/* Labels - Clearer */
label {
font-size: 14px !important;
font-weight: 600 !important;
margin-bottom: 8px !important;
display: block !important;
}
}
@media (max-width: 480px) {
/* Extra small devices - even more compact */
body {
font-size: 14px !important;
}
h1 {
font-size: 20px !important;
}
h2 {
font-size: 18px !important;
}
/* Reduce all padding */
main,
[class*="MainContent"] {
padding: 8px !important;
}
[class*="ListCard"],
[class*="EntryCard"] {
padding: 12px 8px !important;
}
}
</style>
</head>
<body>
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
</body>
</html>

View File

@@ -1,485 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tasks - Firefrost Gaming</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#root {
min-height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React;
const GITEA_API = 'https://git.firefrostgaming.com/api/v1';
const REPO_OWNER = 'firefrost-gaming';
const REPO_NAME = 'firefrost-operations-manual';
const TOKEN = 'e0e330cba1749b01ab505093a160e4423ebbbe36';
const parseMarkdown = (content) => {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) return { frontmatter: {}, body: content };
const frontmatter = {};
const yamlLines = frontmatterMatch[1].split('\n');
yamlLines.forEach(line => {
const match = line.match(/^(\w+):\s*(.+)$/);
if (match) {
const [, key, value] = match;
if (value === 'true') frontmatter[key] = true;
else if (value === 'false') frontmatter[key] = false;
else if (!isNaN(value) && value.trim() !== '') frontmatter[key] = parseInt(value);
else frontmatter[key] = value.trim();
}
});
return { frontmatter, body: frontmatterMatch[2] };
};
const serializeMarkdown = (frontmatter, body) => {
const yamlLines = Object.entries(frontmatter).map(([key, value]) => {
return `${key}: ${value}`;
});
return `---\n${yamlLines.join('\n')}\n---\n${body}`;
};
const MobileTaskManager = () => {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expandedTask, setExpandedTask] = useState(null);
const [saving, setSaving] = useState(false);
const [filter, setFilter] = useState('all');
const loadTasks = async () => {
try {
setLoading(true);
const response = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/tasks?ref=master`,
{
headers: {
'Authorization': `token ${TOKEN}`,
'Accept': 'application/json'
}
}
);
if (!response.ok) throw new Error('Failed to load tasks');
const files = await response.json();
const taskFiles = files.filter(f => f.name.startsWith('task-') && f.name.endsWith('.md'));
const taskPromises = taskFiles.map(async (file) => {
const contentResponse = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${file.path}?ref=master`,
{
headers: {
'Authorization': `token ${TOKEN}`,
'Accept': 'application/json'
}
}
);
const fileData = await contentResponse.json();
const content = atob(fileData.content);
const { frontmatter, body } = parseMarkdown(content);
return {
path: file.path,
sha: fileData.sha,
name: file.name,
frontmatter,
body,
number: frontmatter.number || 0
};
});
const loadedTasks = await Promise.all(taskPromises);
const priorityOrder = { 'P0-Blocker': 0, 'P1-High': 1, 'P1': 1, 'P2-Medium': 2, 'P3-Low': 3, 'P4-Personal': 4 };
loadedTasks.sort((a, b) => {
const priorityA = priorityOrder[a.frontmatter.priority] ?? 5;
const priorityB = priorityOrder[b.frontmatter.priority] ?? 5;
if (priorityA !== priorityB) return priorityA - priorityB;
return a.number - b.number;
});
setTasks(loadedTasks);
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
useEffect(() => {
loadTasks();
}, []);
const saveTask = async (task) => {
try {
setSaving(true);
const content = serializeMarkdown(task.frontmatter, task.body);
const base64Content = btoa(content);
const response = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${task.path}`,
{
method: 'PUT',
headers: {
'Authorization': `token ${TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: base64Content,
sha: task.sha,
message: `Update Task #${task.number} via mobile task manager`,
branch: 'master'
})
}
);
if (!response.ok) throw new Error('Failed to save task');
await loadTasks();
setExpandedTask(null);
setSaving(false);
} catch (err) {
alert('Error saving task: ' + err.message);
setSaving(false);
}
};
const updateTaskField = (task, field, value) => {
const updatedTask = {
...task,
frontmatter: {
...task.frontmatter,
[field]: value
}
};
saveTask(updatedTask);
};
const getPriorityColor = (priority) => {
switch (priority) {
case 'P0-Blocker': return '#dc3545';
case 'P1-High': return '#FF6B35';
case 'P1': return '#FF6B35';
case 'P2-Medium': return '#ffc107';
case 'P3-Low': return '#4ECDC4';
case 'P4-Personal': return '#A855F7';
default: return '#6c757d';
}
};
const getStatusColor = (status) => {
switch (status) {
case 'Complete': return '#28a745';
case 'In Progress': return '#4ECDC4';
case 'Blocked': return '#dc3545';
case 'Planned': return '#6c757d';
default: return '#6c757d';
}
};
const filteredTasks = tasks.filter(task => {
if (filter === 'all') return true;
if (filter === 'blocker') return task.frontmatter.blocker === true;
if (filter === 'active') return task.frontmatter.status === 'In Progress';
if (filter === 'complete') return task.frontmatter.status === 'Complete';
return true;
});
if (loading) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '10px' }}>🔥</div>
<div>Loading tasks...</div>
</div>
);
}
if (error) {
return (
<div style={{ padding: '20px', textAlign: 'center', color: '#dc3545' }}>
<div style={{ fontSize: '24px', marginBottom: '10px' }}></div>
<div>Error: {error}</div>
<button onClick={loadTasks} style={{ marginTop: '20px', padding: '10px 20px', fontSize: '16px', borderRadius: '8px', border: 'none', backgroundColor: '#4ECDC4', color: 'white', cursor: 'pointer' }}>
Retry
</button>
</div>
);
}
return (
<div style={{
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
maxWidth: '100%',
margin: '0',
padding: '0',
backgroundColor: '#f5f5f5',
minHeight: '100vh'
}}>
<div style={{
backgroundColor: '#0F0F1E',
color: 'white',
padding: '16px',
position: 'sticky',
top: 0,
zIndex: 100,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h1 style={{ margin: '0 0 12px 0', fontSize: '20px', fontWeight: '700' }}>
🔥 Firefrost Tasks
</h1>
<div style={{ display: 'flex', gap: '8px', overflowX: 'auto', paddingBottom: '4px' }}>
{[
{ key: 'all', label: 'All', count: tasks.length },
{ key: 'blocker', label: 'Blockers', count: tasks.filter(t => t.frontmatter.blocker).length },
{ key: 'active', label: 'Active', count: tasks.filter(t => t.frontmatter.status === 'In Progress').length },
{ key: 'complete', label: 'Done', count: tasks.filter(t => t.frontmatter.status === 'Complete').length }
].map(({ key, label, count }) => (
<button
key={key}
onClick={() => setFilter(key)}
style={{
padding: '8px 16px',
borderRadius: '20px',
border: 'none',
backgroundColor: filter === key ? '#4ECDC4' : 'rgba(255,255,255,0.2)',
color: 'white',
fontSize: '14px',
fontWeight: filter === key ? '600' : '400',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}
>
{label} ({count})
</button>
))}
</div>
</div>
<div style={{ padding: '12px' }}>
{filteredTasks.map(task => (
<div
key={task.path}
style={{
backgroundColor: 'white',
borderRadius: '12px',
marginBottom: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden'
}}
>
<div
onClick={() => setExpandedTask(expandedTask === task.path ? null : task.path)}
style={{
padding: '16px',
cursor: 'pointer',
borderLeft: `4px solid ${getPriorityColor(task.frontmatter.priority)}`
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
Task #{task.number}
</div>
<div style={{ fontSize: '16px', fontWeight: '600', color: '#0F0F1E' }}>
{task.frontmatter.title}
</div>
</div>
<div style={{ fontSize: '20px', marginLeft: '8px' }}>
{expandedTask === task.path ? '▼' : '▶'}
</div>
</div>
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: getPriorityColor(task.frontmatter.priority),
color: 'white'
}}>
{task.frontmatter.priority}
</span>
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: getStatusColor(task.frontmatter.status),
color: 'white'
}}>
{task.frontmatter.status}
</span>
{task.frontmatter.blocker && (
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: '#dc3545',
color: 'white'
}}>
🚨 BLOCKER
</span>
)}
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
backgroundColor: '#e9ecef',
color: '#495057'
}}>
{task.frontmatter.owner}
</span>
</div>
</div>
{expandedTask === task.path && (
<div style={{
padding: '16px',
borderTop: '1px solid #e9ecef',
backgroundColor: '#f8f9fa'
}}>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Status
</label>
<select
value={task.frontmatter.status}
onChange={(e) => updateTaskField(task, 'status', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="Planned">Planned</option>
<option value="In Progress">In Progress</option>
<option value="Blocked">Blocked</option>
<option value="Complete">Complete</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Priority
</label>
<select
value={task.frontmatter.priority}
onChange={(e) => updateTaskField(task, 'priority', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="P0-Blocker">P0 - Blocker</option>
<option value="P1-High">P1 - High</option>
<option value="P2-Medium">P2 - Medium</option>
<option value="P3-Low">P3 - Low</option>
<option value="P4-Personal">P4 - Personal</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Owner
</label>
<select
value={task.frontmatter.owner}
onChange={(e) => updateTaskField(task, 'owner', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="Michael">Michael</option>
<option value="Meg">Meg</option>
<option value="Holly">Holly</option>
<option value="Trinity">Trinity</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'flex', alignItems: 'center', fontSize: '14px', fontWeight: '600', color: '#495057' }}>
<input
type="checkbox"
checked={task.frontmatter.blocker === true}
onChange={(e) => updateTaskField(task, 'blocker', e.target.checked)}
disabled={saving}
style={{ marginRight: '8px', width: '20px', height: '20px' }}
/>
Launch Blocker
</label>
</div>
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: 'white', borderRadius: '8px', fontSize: '14px', lineHeight: '1.6', color: '#495057', whiteSpace: 'pre-wrap' }}>
{task.body.split('\n').slice(0, 5).join('\n')}
{task.body.split('\n').length > 5 && <div style={{ marginTop: '8px', fontStyle: 'italic', color: '#6c757d' }}>...</div>}
</div>
{saving && (
<div style={{ marginTop: '12px', textAlign: 'center', color: '#4ECDC4', fontSize: '14px' }}>
💾 Saving...
</div>
)}
</div>
)}
</div>
))}
{filteredTasks.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#6c757d' }}>
No tasks found
</div>
)}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<MobileTaskManager />);
</script>
</body>
</html>

View File

@@ -1,485 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tasks - Firefrost Gaming</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#root {
min-height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React;
const GITEA_API = 'https://git.firefrostgaming.com/api/v1';
const REPO_OWNER = 'firefrost-gaming';
const REPO_NAME = 'firefrost-operations-manual';
const TOKEN = 'e0e330cba1749b01ab505093a160e4423ebbbe36';
const parseMarkdown = (content) => {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) return { frontmatter: {}, body: content };
const frontmatter = {};
const yamlLines = frontmatterMatch[1].split('\n');
yamlLines.forEach(line => {
const match = line.match(/^(\w+):\s*(.+)$/);
if (match) {
const [, key, value] = match;
if (value === 'true') frontmatter[key] = true;
else if (value === 'false') frontmatter[key] = false;
else if (!isNaN(value) && value.trim() !== '') frontmatter[key] = parseInt(value);
else frontmatter[key] = value.trim();
}
});
return { frontmatter, body: frontmatterMatch[2] };
};
const serializeMarkdown = (frontmatter, body) => {
const yamlLines = Object.entries(frontmatter).map(([key, value]) => {
return `${key}: ${value}`;
});
return `---\n${yamlLines.join('\n')}\n---\n${body}`;
};
const MobileTaskManager = () => {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expandedTask, setExpandedTask] = useState(null);
const [saving, setSaving] = useState(false);
const [filter, setFilter] = useState('all');
const loadTasks = async () => {
try {
setLoading(true);
const response = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/tasks?ref=master`,
{
headers: {
'Authorization': `token ${TOKEN}`,
'Accept': 'application/json'
}
}
);
if (!response.ok) throw new Error('Failed to load tasks');
const files = await response.json();
const taskFiles = files.filter(f => f.name.startsWith('task-') && f.name.endsWith('.md'));
const taskPromises = taskFiles.map(async (file) => {
const contentResponse = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${file.path}?ref=master`,
{
headers: {
'Authorization': `token ${TOKEN}`,
'Accept': 'application/json'
}
}
);
const fileData = await contentResponse.json();
const content = atob(fileData.content);
const { frontmatter, body } = parseMarkdown(content);
return {
path: file.path,
sha: fileData.sha,
name: file.name,
frontmatter,
body,
number: frontmatter.number || 0
};
});
const loadedTasks = await Promise.all(taskPromises);
const priorityOrder = { 'P0-Blocker': 0, 'P1-High': 1, 'P1': 1, 'P2-Medium': 2, 'P3-Low': 3, 'P4-Personal': 4 };
loadedTasks.sort((a, b) => {
const priorityA = priorityOrder[a.frontmatter.priority] ?? 5;
const priorityB = priorityOrder[b.frontmatter.priority] ?? 5;
if (priorityA !== priorityB) return priorityA - priorityB;
return a.number - b.number;
});
setTasks(loadedTasks);
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
useEffect(() => {
loadTasks();
}, []);
const saveTask = async (task) => {
try {
setSaving(true);
const content = serializeMarkdown(task.frontmatter, task.body);
const base64Content = btoa(content);
const response = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${task.path}`,
{
method: 'PUT',
headers: {
'Authorization': `token ${TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: base64Content,
sha: task.sha,
message: `Update Task #${task.number} via mobile task manager`,
branch: 'master'
})
}
);
if (!response.ok) throw new Error('Failed to save task');
await loadTasks();
setExpandedTask(null);
setSaving(false);
} catch (err) {
alert('Error saving task: ' + err.message);
setSaving(false);
}
};
const updateTaskField = (task, field, value) => {
const updatedTask = {
...task,
frontmatter: {
...task.frontmatter,
[field]: value
}
};
saveTask(updatedTask);
};
const getPriorityColor = (priority) => {
switch (priority) {
case 'P0-Blocker': return '#dc3545';
case 'P1-High': return '#FF6B35';
case 'P1': return '#FF6B35';
case 'P2-Medium': return '#ffc107';
case 'P3-Low': return '#4ECDC4';
case 'P4-Personal': return '#A855F7';
default: return '#6c757d';
}
};
const getStatusColor = (status) => {
switch (status) {
case 'Complete': return '#28a745';
case 'In Progress': return '#4ECDC4';
case 'Blocked': return '#dc3545';
case 'Planned': return '#6c757d';
default: return '#6c757d';
}
};
const filteredTasks = tasks.filter(task => {
if (filter === 'all') return true;
if (filter === 'blocker') return task.frontmatter.blocker === true;
if (filter === 'active') return task.frontmatter.status === 'In Progress';
if (filter === 'complete') return task.frontmatter.status === 'Complete';
return true;
});
if (loading) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '10px' }}>🔥</div>
<div>Loading tasks...</div>
</div>
);
}
if (error) {
return (
<div style={{ padding: '20px', textAlign: 'center', color: '#dc3545' }}>
<div style={{ fontSize: '24px', marginBottom: '10px' }}></div>
<div>Error: {error}</div>
<button onClick={loadTasks} style={{ marginTop: '20px', padding: '10px 20px', fontSize: '16px', borderRadius: '8px', border: 'none', backgroundColor: '#4ECDC4', color: 'white', cursor: 'pointer' }}>
Retry
</button>
</div>
);
}
return (
<div style={{
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
maxWidth: '100%',
margin: '0',
padding: '0',
backgroundColor: '#f5f5f5',
minHeight: '100vh'
}}>
<div style={{
backgroundColor: '#0F0F1E',
color: 'white',
padding: '16px',
position: 'sticky',
top: 0,
zIndex: 100,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h1 style={{ margin: '0 0 12px 0', fontSize: '20px', fontWeight: '700' }}>
🔥 Firefrost Tasks
</h1>
<div style={{ display: 'flex', gap: '8px', overflowX: 'auto', paddingBottom: '4px' }}>
{[
{ key: 'all', label: 'All', count: tasks.length },
{ key: 'blocker', label: 'Blockers', count: tasks.filter(t => t.frontmatter.blocker).length },
{ key: 'active', label: 'Active', count: tasks.filter(t => t.frontmatter.status === 'In Progress').length },
{ key: 'complete', label: 'Done', count: tasks.filter(t => t.frontmatter.status === 'Complete').length }
].map(({ key, label, count }) => (
<button
key={key}
onClick={() => setFilter(key)}
style={{
padding: '8px 16px',
borderRadius: '20px',
border: 'none',
backgroundColor: filter === key ? '#4ECDC4' : 'rgba(255,255,255,0.2)',
color: 'white',
fontSize: '14px',
fontWeight: filter === key ? '600' : '400',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}
>
{label} ({count})
</button>
))}
</div>
</div>
<div style={{ padding: '12px' }}>
{filteredTasks.map(task => (
<div
key={task.path}
style={{
backgroundColor: 'white',
borderRadius: '12px',
marginBottom: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden'
}}
>
<div
onClick={() => setExpandedTask(expandedTask === task.path ? null : task.path)}
style={{
padding: '16px',
cursor: 'pointer',
borderLeft: `4px solid ${getPriorityColor(task.frontmatter.priority)}`
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
Task #{task.number}
</div>
<div style={{ fontSize: '16px', fontWeight: '600', color: '#0F0F1E' }}>
{task.frontmatter.title}
</div>
</div>
<div style={{ fontSize: '20px', marginLeft: '8px' }}>
{expandedTask === task.path ? '▼' : '▶'}
</div>
</div>
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: getPriorityColor(task.frontmatter.priority),
color: 'white'
}}>
{task.frontmatter.priority}
</span>
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: getStatusColor(task.frontmatter.status),
color: 'white'
}}>
{task.frontmatter.status}
</span>
{task.frontmatter.blocker && (
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: '#dc3545',
color: 'white'
}}>
🚨 BLOCKER
</span>
)}
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
backgroundColor: '#e9ecef',
color: '#495057'
}}>
{task.frontmatter.owner}
</span>
</div>
</div>
{expandedTask === task.path && (
<div style={{
padding: '16px',
borderTop: '1px solid #e9ecef',
backgroundColor: '#f8f9fa'
}}>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Status
</label>
<select
value={task.frontmatter.status}
onChange={(e) => updateTaskField(task, 'status', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="Planned">Planned</option>
<option value="In Progress">In Progress</option>
<option value="Blocked">Blocked</option>
<option value="Complete">Complete</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Priority
</label>
<select
value={task.frontmatter.priority}
onChange={(e) => updateTaskField(task, 'priority', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="P0-Blocker">P0 - Blocker</option>
<option value="P1-High">P1 - High</option>
<option value="P2-Medium">P2 - Medium</option>
<option value="P3-Low">P3 - Low</option>
<option value="P4-Personal">P4 - Personal</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Owner
</label>
<select
value={task.frontmatter.owner}
onChange={(e) => updateTaskField(task, 'owner', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="Michael">Michael</option>
<option value="Meg">Meg</option>
<option value="Holly">Holly</option>
<option value="Trinity">Trinity</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'flex', alignItems: 'center', fontSize: '14px', fontWeight: '600', color: '#495057' }}>
<input
type="checkbox"
checked={task.frontmatter.blocker === true}
onChange={(e) => updateTaskField(task, 'blocker', e.target.checked)}
disabled={saving}
style={{ marginRight: '8px', width: '20px', height: '20px' }}
/>
Launch Blocker
</label>
</div>
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: 'white', borderRadius: '8px', fontSize: '14px', lineHeight: '1.6', color: '#495057', whiteSpace: 'pre-wrap' }}>
{task.body.split('\n').slice(0, 5).join('\n')}
{task.body.split('\n').length > 5 && <div style={{ marginTop: '8px', fontStyle: 'italic', color: '#6c757d' }}>...</div>}
</div>
{saving && (
<div style={{ marginTop: '12px', textAlign: 'center', color: '#4ECDC4', fontSize: '14px' }}>
💾 Saving...
</div>
)}
</div>
)}
</div>
))}
{filteredTasks.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#6c757d' }}>
No tasks found
</div>
)}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<MobileTaskManager />);
</script>
</body>
</html>