- Add ConsoleSessionStore for managing console session state including tenants, profile, and token information. - Create OperatorContextService to manage operator context for orchestrator actions. - Implement OperatorMetadataInterceptor to enrich HTTP requests with operator context metadata. - Develop ConsoleProfileComponent to display user profile and session details, including tenant information and access tokens. - Add corresponding HTML and SCSS for ConsoleProfileComponent to enhance UI presentation. - Write unit tests for ConsoleProfileComponent to ensure correct rendering and functionality.
		
			
				
	
	
		
			255 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			255 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
"""Render Markdown documentation under docs/ into a static HTML bundle.
 | 
						|
 | 
						|
The script converts every Markdown file into a standalone HTML document,
 | 
						|
mirroring the original folder structure under the output directory. A
 | 
						|
`manifest.json` file is also produced to list the generated documents and
 | 
						|
surface basic metadata (title, source path, output path).
 | 
						|
 | 
						|
Usage:
 | 
						|
    python scripts/render_docs.py --source docs --output build/docs-site
 | 
						|
 | 
						|
Dependencies:
 | 
						|
    pip install markdown pygments
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import argparse
 | 
						|
import json
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import shutil
 | 
						|
from dataclasses import dataclass
 | 
						|
from datetime import datetime, timezone
 | 
						|
from pathlib import Path
 | 
						|
from typing import Iterable, List
 | 
						|
 | 
						|
import markdown
 | 
						|
 | 
						|
# Enable fenced code blocks, tables, and definition lists. These cover the
 | 
						|
# Markdown constructs heavily used across the documentation set.
 | 
						|
MD_EXTENSIONS = [
 | 
						|
    "markdown.extensions.fenced_code",
 | 
						|
    "markdown.extensions.codehilite",
 | 
						|
    "markdown.extensions.tables",
 | 
						|
    "markdown.extensions.toc",
 | 
						|
    "markdown.extensions.def_list",
 | 
						|
    "markdown.extensions.admonition",
 | 
						|
]
 | 
						|
 | 
						|
HTML_TEMPLATE = """<!DOCTYPE html>
 | 
						|
<html lang=\"en\">
 | 
						|
<head>
 | 
						|
  <meta charset=\"utf-8\" />
 | 
						|
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
 | 
						|
  <title>{title}</title>
 | 
						|
  <style>
 | 
						|
    :root {{
 | 
						|
      color-scheme: light dark;
 | 
						|
      font-family: system-ui, -apple-system, Segoe UI, sans-serif;
 | 
						|
      line-height: 1.6;
 | 
						|
    }}
 | 
						|
    body {{
 | 
						|
      margin: 2.5rem auto;
 | 
						|
      padding: 0 1.5rem;
 | 
						|
      max-width: 70ch;
 | 
						|
      background: var(--background, #1118270d);
 | 
						|
    }}
 | 
						|
    pre {{
 | 
						|
      overflow: auto;
 | 
						|
      padding: 1rem;
 | 
						|
      background: #11182714;
 | 
						|
      border-radius: 0.5rem;
 | 
						|
    }}
 | 
						|
    code {{
 | 
						|
      font-family: SFMono-Regular, Consolas, 'Liberation Mono', monospace;
 | 
						|
      font-size: 0.95em;
 | 
						|
    }}
 | 
						|
    table {{
 | 
						|
      width: 100%;
 | 
						|
      border-collapse: collapse;
 | 
						|
      margin: 1rem 0;
 | 
						|
    }}
 | 
						|
    th, td {{
 | 
						|
      border: 1px solid #4b5563;
 | 
						|
      padding: 0.5rem;
 | 
						|
      text-align: left;
 | 
						|
    }}
 | 
						|
    a {{
 | 
						|
      color: #2563eb;
 | 
						|
    }}
 | 
						|
    footer {{
 | 
						|
      margin-top: 3rem;
 | 
						|
      font-size: 0.85rem;
 | 
						|
      color: #6b7280;
 | 
						|
    }}
 | 
						|
  </style>
 | 
						|
</head>
 | 
						|
<body>
 | 
						|
  <main>
 | 
						|
{body}
 | 
						|
  </main>
 | 
						|
  <footer>
 | 
						|
    <p>Generated on {generated_at} UTC · Source: {source}</p>
 | 
						|
  </footer>
 | 
						|
</body>
 | 
						|
</html>
 | 
						|
"""
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class DocEntry:
 | 
						|
    source: Path
 | 
						|
    output: Path
 | 
						|
    title: str
 | 
						|
 | 
						|
    def to_manifest(self) -> dict[str, str]:
 | 
						|
        return {
 | 
						|
            "source": self.source.as_posix(),
 | 
						|
            "output": self.output.as_posix(),
 | 
						|
            "title": self.title,
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
def discover_markdown_files(source_root: Path) -> Iterable[Path]:
 | 
						|
    for path in source_root.rglob("*.md"):
 | 
						|
        if path.is_file():
 | 
						|
            yield path
 | 
						|
 | 
						|
 | 
						|
def read_title(markdown_text: str, fallback: str) -> str:
 | 
						|
    for raw_line in markdown_text.splitlines():
 | 
						|
        line = raw_line.strip()
 | 
						|
        if line.startswith("#"):
 | 
						|
            return line.lstrip("#").strip() or fallback
 | 
						|
    return fallback
 | 
						|
 | 
						|
 | 
						|
def convert_markdown(path: Path, source_root: Path, output_root: Path) -> DocEntry:
 | 
						|
    relative = path.relative_to(source_root)
 | 
						|
    output_path = output_root / relative.with_suffix(".html")
 | 
						|
    output_path.parent.mkdir(parents=True, exist_ok=True)
 | 
						|
 | 
						|
    text = path.read_text(encoding="utf-8")
 | 
						|
    html_body = markdown.markdown(text, extensions=MD_EXTENSIONS)
 | 
						|
 | 
						|
    title = read_title(text, fallback=relative.stem.replace("_", " "))
 | 
						|
    generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
 | 
						|
 | 
						|
    output_path.write_text(
 | 
						|
        HTML_TEMPLATE.format(
 | 
						|
            title=title,
 | 
						|
            body=html_body,
 | 
						|
            generated_at=generated_at,
 | 
						|
            source=relative.as_posix(),
 | 
						|
        ),
 | 
						|
        encoding="utf-8",
 | 
						|
    )
 | 
						|
 | 
						|
    return DocEntry(source=relative, output=output_path.relative_to(output_root), title=title)
 | 
						|
 | 
						|
 | 
						|
def copy_static_assets(source_root: Path, output_root: Path) -> None:
 | 
						|
    for path in source_root.rglob("*"):
 | 
						|
        if path.is_dir() or path.suffix.lower() == ".md":
 | 
						|
            # Skip Markdown (already rendered separately).
 | 
						|
            continue
 | 
						|
        relative = path.relative_to(source_root)
 | 
						|
        destination = output_root / relative
 | 
						|
        destination.parent.mkdir(parents=True, exist_ok=True)
 | 
						|
        destination.write_bytes(path.read_bytes())
 | 
						|
        logging.info("Copied asset %s", relative)
 | 
						|
 | 
						|
 | 
						|
def write_manifest(entries: Iterable[DocEntry], output_root: Path) -> None:
 | 
						|
    manifest_path = output_root / "manifest.json"
 | 
						|
    manifest = [entry.to_manifest() for entry in entries]
 | 
						|
    manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
 | 
						|
    logging.info("Wrote manifest with %d entries", len(manifest))
 | 
						|
 | 
						|
 | 
						|
def write_index(entries: List[DocEntry], output_root: Path) -> None:
 | 
						|
    index_path = output_root / "index.html"
 | 
						|
    generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
 | 
						|
 | 
						|
    items = "\n".join(
 | 
						|
        f"      <li><a href='{entry.output.as_posix()}'>{entry.title}</a>" f" · <code>{entry.source.as_posix()}</code></li>"
 | 
						|
        for entry in sorted(entries, key=lambda e: e.title.lower())
 | 
						|
    )
 | 
						|
 | 
						|
    html = f"""<!DOCTYPE html>
 | 
						|
<html lang=\"en\">
 | 
						|
<head>
 | 
						|
  <meta charset=\"utf-8\" />
 | 
						|
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
 | 
						|
  <title>Stella Ops Documentation Index</title>
 | 
						|
  <style>
 | 
						|
    body {{
 | 
						|
      margin: 2.5rem auto;
 | 
						|
      padding: 0 1.5rem;
 | 
						|
      max-width: 70ch;
 | 
						|
      font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
 | 
						|
      line-height: 1.6;
 | 
						|
    }}
 | 
						|
    h1 {{ font-size: 2.25rem; margin-bottom: 1rem; }}
 | 
						|
    ul {{ list-style: none; padding: 0; }}
 | 
						|
    li {{ margin-bottom: 0.75rem; }}
 | 
						|
    code {{ background: #11182714; padding: 0.2rem 0.35rem; border-radius: 0.35rem; }}
 | 
						|
  </style>
 | 
						|
</head>
 | 
						|
<body>
 | 
						|
  <h1>Stella Ops Documentation</h1>
 | 
						|
  <p>Generated on {generated_at} UTC</p>
 | 
						|
  <ul>
 | 
						|
{items}
 | 
						|
  </ul>
 | 
						|
</body>
 | 
						|
</html>
 | 
						|
"""
 | 
						|
    index_path.write_text(html, encoding="utf-8")
 | 
						|
    logging.info("Wrote HTML index with %d entries", len(entries))
 | 
						|
 | 
						|
 | 
						|
def parse_args() -> argparse.Namespace:
 | 
						|
    parser = argparse.ArgumentParser(description="Render documentation bundle")
 | 
						|
    parser.add_argument("--source", default="docs", type=Path, help="Directory containing Markdown sources")
 | 
						|
    parser.add_argument("--output", default=Path("build/docs-site"), type=Path, help="Directory for rendered output")
 | 
						|
    parser.add_argument("--clean", action="store_true", help="Remove the output directory before rendering")
 | 
						|
    return parser.parse_args()
 | 
						|
 | 
						|
 | 
						|
def main() -> int:
 | 
						|
    logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
 | 
						|
    args = parse_args()
 | 
						|
 | 
						|
    source_root: Path = args.source.resolve()
 | 
						|
    output_root: Path = args.output.resolve()
 | 
						|
 | 
						|
    if not source_root.exists():
 | 
						|
        logging.error("Source directory %s does not exist", source_root)
 | 
						|
        return os.EX_NOINPUT
 | 
						|
 | 
						|
    if args.clean and output_root.exists():
 | 
						|
        logging.info("Cleaning existing output directory %s", output_root)
 | 
						|
        shutil.rmtree(output_root)
 | 
						|
 | 
						|
    output_root.mkdir(parents=True, exist_ok=True)
 | 
						|
 | 
						|
    entries: List[DocEntry] = []
 | 
						|
    for md_file in discover_markdown_files(source_root):
 | 
						|
        entry = convert_markdown(md_file, source_root, output_root)
 | 
						|
        entries.append(entry)
 | 
						|
        logging.info("Rendered %s -> %s", entry.source, entry.output)
 | 
						|
 | 
						|
    write_manifest(entries, output_root)
 | 
						|
    write_index(entries, output_root)
 | 
						|
    copy_static_assets(source_root, output_root)
 | 
						|
 | 
						|
    logging.info("Documentation bundle available at %s", output_root)
 | 
						|
    return os.EX_OK
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    raise SystemExit(main())
 |