diff --git a/extensions/content-manager/.editorconfig b/extensions/content-manager/.editorconfig
new file mode 100644
index 0000000..1ed453a
--- /dev/null
+++ b/extensions/content-manager/.editorconfig
@@ -0,0 +1,10 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+[*.{js,json,yml}]
+charset = utf-8
+indent_style = space
+indent_size = 2
diff --git a/extensions/content-manager/.gitattributes b/extensions/content-manager/.gitattributes
new file mode 100644
index 0000000..af3ad12
--- /dev/null
+++ b/extensions/content-manager/.gitattributes
@@ -0,0 +1,4 @@
+/.yarn/** linguist-vendored
+/.yarn/releases/* binary
+/.yarn/plugins/**/* binary
+/.pnp.* binary linguist-generated
diff --git a/extensions/content-manager/.gitignore b/extensions/content-manager/.gitignore
new file mode 100644
index 0000000..925b532
--- /dev/null
+++ b/extensions/content-manager/.gitignore
@@ -0,0 +1,313 @@
+# Posit
+.posit/
+
+# Node
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Python
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# PyPI configuration file
+.pypirc
diff --git a/extensions/content-manager/.prettierignore b/extensions/content-manager/.prettierignore
new file mode 100644
index 0000000..78e1938
--- /dev/null
+++ b/extensions/content-manager/.prettierignore
@@ -0,0 +1,5 @@
+# Ignore artifacts:
+.posit
+.venv
+node_modules
+dist
diff --git a/extensions/content-manager/.prettierrc b/extensions/content-manager/.prettierrc
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/extensions/content-manager/.prettierrc
@@ -0,0 +1 @@
+{}
diff --git a/extensions/content-manager/.vscode/extensions.json b/extensions/content-manager/.vscode/extensions.json
new file mode 100644
index 0000000..1453f7f
--- /dev/null
+++ b/extensions/content-manager/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["arcanis.vscode-zipfs"]
+}
diff --git a/extensions/content-manager/.vscode/settings.json b/extensions/content-manager/.vscode/settings.json
new file mode 100644
index 0000000..dce71fb
--- /dev/null
+++ b/extensions/content-manager/.vscode/settings.json
@@ -0,0 +1,9 @@
+{
+ "search.exclude": {
+ "**/.yarn": true,
+ "**/.pnp.*": true
+ },
+ "typescript.tsdk": ".yarn/sdks/typescript/lib",
+ "typescript.enablePromptUseWorkspaceTsdk": true,
+ "cSpell.words": ["oninit", "onremove", "vnode"]
+}
diff --git a/extensions/content-manager/README.md b/extensions/content-manager/README.md
new file mode 100644
index 0000000..f979598
--- /dev/null
+++ b/extensions/content-manager/README.md
@@ -0,0 +1,102 @@
+
+# Publisher Command Center
+
+**Publisher Command Center** is a web-based application designed to help publishers manage and track their content.
+
+## Features
+
+- **Mithril.js** for lightweight and efficient UI rendering
+- **Bootstrap** for responsive and consistent styling
+- **FontAwesome** for scalable vector icons
+- **Vite** for a fast development build system
+- **FastAPI** as the backend framework
+
+## Getting Started
+
+### Prerequisites
+
+Ensure you have the following installed:
+
+- [Node.js](https://nodejs.org/) (for frontend development)
+- [Python](https://www.python.org/) (for backend development)
+- [uv](https://github.com/astral-sh/uv) (for running backend server)
+
+### Installation
+
+Clone the repository and install dependencies:
+
+```sh
+npm install
+```
+
+### Running the Development Server
+
+Start the frontend development server:
+
+```sh
+npm run preview
+```
+
+Start the backend development server:
+
+```sh
+npm run server
+```
+
+Start the watcher to enable continuous rebuilds:
+
+```sh
+npm run watch
+```
+
+
+
+### Building for Production
+
+To build the frontend for production:
+
+```sh
+npm run build
+```
+
+## Technologies Used
+
+### [Mithril.js](https://mithril.js.org/)
+
+Mithril.js is a modern client-side JavaScript framework that focuses on simplicity and performance. It is lightweight (~10kb gzipped) and offers a virtual DOM implementation for efficient rendering.
+
+### [Bootstrap](https://getbootstrap.com/)
+
+Bootstrap is a popular CSS framework that helps create responsive and mobile-first designs with prebuilt components and utilities.
+
+### [FontAwesome](https://fontawesome.com/)
+
+FontAwesome provides scalable vector icons and social logos that can be used in web applications.
+
+### [Date-fns](https://date-fns.org/)
+
+Date-fns is a modern JavaScript date utility library that provides comprehensive, yet simple-to-use functions for date manipulation.
+
+### [Filesize.js](https://filesizejs.com/)
+
+Filesize.js is a small utility for formatting file sizes in human-readable formats.
+
+### [Vite](https://vitejs.dev/)
+
+Vite is a next-generation frontend tooling system that enables fast build times and instant development server start-up.
+
+### [SASS](https://sass-lang.com/)
+
+SASS (Syntactically Awesome Stylesheets) is a CSS preprocessor that extends CSS with features like variables, nested rules, and mixins.
+
+### [FastAPI](https://fastapi.tiangolo.com/)
+
+FastAPI is a modern web framework for building APIs with Python 3.7+ that provides automatic OpenAPI and JSON Schema documentation.
+
+## Code Formatting
+
+This project uses **Prettier** for consistent code formatting. Run the following command to format your code:
+
+```sh
+npx prettier --write .
+```
diff --git a/extensions/content-manager/app.py b/extensions/content-manager/app.py
new file mode 100644
index 0000000..a24a924
--- /dev/null
+++ b/extensions/content-manager/app.py
@@ -0,0 +1,115 @@
+from http import client
+import asyncio
+from fastapi import FastAPI, Header
+from fastapi.staticfiles import StaticFiles
+from posit import connect
+
+from cachetools import TTLCache, cached
+
+client = connect.Client()
+
+app = FastAPI()
+
+# Create cache with TTL=1hour and unlimited size
+client_cache = TTLCache(maxsize=float("inf"), ttl=3600)
+
+
+@cached(client_cache)
+def get_visitor_client(token: str | None) -> connect.Client:
+ """Create and cache API client per token with 1 hour TTL"""
+ if token:
+ return client.with_user_session_token(token)
+ else:
+ return client
+
+
+@app.get("/api/contents")
+async def contents(posit_connect_user_session_token: str = Header(None)):
+ visitor = get_visitor_client(posit_connect_user_session_token)
+
+ response = client.get("metrics/procs")
+ processes = response.json()
+
+ contents = visitor.me.content.find()
+ for content in contents:
+ content["processes"] = [
+ process for process in processes if content["guid"] == process["app_guid"]
+ ]
+
+ return contents
+
+
+@app.get("/api/contents/{content_id}")
+async def content(
+ content_id: str, posit_connect_user_session_token: str = Header(None)
+):
+ visitor = get_visitor_client(posit_connect_user_session_token)
+ return visitor.content.get(content_id)
+
+
+@app.get("/api/contents/{content_id}/processes")
+async def get_content_processes(
+ content_id: str, posit_connect_user_session_token: str = Header(None)
+):
+ visitor = get_visitor_client(posit_connect_user_session_token)
+
+ # Assert the viewer has access to the content
+ assert visitor.content.get(content_id)
+
+ response = client.get("metrics/procs")
+ processes = response.json()
+
+ return [process for process in processes if process.get("app_guid") == content_id]
+
+
+@app.delete("/api/contents/{content_id}/processes/{process_id}")
+async def destroy_process(
+ content_id: str,
+ process_id: str,
+ posit_connect_user_session_token: str = Header(None),
+):
+ visitor = get_visitor_client(posit_connect_user_session_token)
+
+ content = visitor.content.get(content_id)
+ job = content.jobs.find(process_id)
+ if job:
+ job.destroy()
+ for _ in range(30):
+ job = content.jobs.find(process_id)
+ if job["status"] != 0:
+ return
+ await asyncio.sleep(1)
+
+
+@app.get("/api/contents/{content_id}/author")
+async def get_author(
+ content_id,
+ posit_connect_user_session_token: str = Header(None),
+):
+ visitor = get_visitor_client(posit_connect_user_session_token)
+ content = visitor.content.get(content_id)
+ return content.owner
+
+
+@app.get("/api/contents/{content_id}/releases")
+async def get_releases(
+ content_id,
+ posit_connect_user_session_token: str = Header(None),
+):
+ visitor = get_visitor_client(posit_connect_user_session_token)
+ content = visitor.content.get(content_id)
+ return content.bundles.find()
+
+
+@app.get("/api/contents/{content_id}/metrics")
+async def get_metrics(
+ content_id,
+ posit_connect_user_session_token: str = Header(None),
+):
+ visitor = get_visitor_client(posit_connect_user_session_token)
+ content = visitor.content.get(content_id)
+ metrics = visitor.metrics.usage.find(content_guid=content["guid"])
+ return metrics
+
+
+app.mount("/", StaticFiles(directory="dist", html=True), name="static")
diff --git a/extensions/content-manager/index.html b/extensions/content-manager/index.html
new file mode 100644
index 0000000..6b4eb50
--- /dev/null
+++ b/extensions/content-manager/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Publisher Command Center
+
+
+
+
+
+
diff --git a/extensions/content-manager/package-lock.json b/extensions/content-manager/package-lock.json
new file mode 100644
index 0000000..1b3980d
--- /dev/null
+++ b/extensions/content-manager/package-lock.json
@@ -0,0 +1,1244 @@
+{
+ "name": "mithril-app",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mithril-app",
+ "version": "0.0.1",
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^6.7.2",
+ "@fortawesome/free-regular-svg-icons": "^6.7.2",
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
+ "@popperjs/core": "^2.11.8",
+ "bootstrap": "^5.3.3",
+ "date-fns": "^4.1.0",
+ "filesize": "^10.1.6",
+ "mithril": "^2.0.4"
+ },
+ "devDependencies": {
+ "prettier": "3.4.2",
+ "sass": "^1.83.4",
+ "vite": "^2.6.2"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
+ "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
+ "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-free": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
+ "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
+ "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-regular-svg-icons": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
+ "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
+ "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz",
+ "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.0",
+ "@parcel/watcher-darwin-arm64": "2.5.0",
+ "@parcel/watcher-darwin-x64": "2.5.0",
+ "@parcel/watcher-freebsd-x64": "2.5.0",
+ "@parcel/watcher-linux-arm-glibc": "2.5.0",
+ "@parcel/watcher-linux-arm-musl": "2.5.0",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.0",
+ "@parcel/watcher-linux-arm64-musl": "2.5.0",
+ "@parcel/watcher-linux-x64-glibc": "2.5.0",
+ "@parcel/watcher-linux-x64-musl": "2.5.0",
+ "@parcel/watcher-win32-arm64": "2.5.0",
+ "@parcel/watcher-win32-ia32": "2.5.0",
+ "@parcel/watcher-win32-x64": "2.5.0"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz",
+ "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz",
+ "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz",
+ "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz",
+ "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz",
+ "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz",
+ "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz",
+ "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz",
+ "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz",
+ "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz",
+ "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz",
+ "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz",
+ "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz",
+ "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/bootstrap": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
+ "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.8"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
+ "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/linux-loong64": "0.14.54",
+ "esbuild-android-64": "0.14.54",
+ "esbuild-android-arm64": "0.14.54",
+ "esbuild-darwin-64": "0.14.54",
+ "esbuild-darwin-arm64": "0.14.54",
+ "esbuild-freebsd-64": "0.14.54",
+ "esbuild-freebsd-arm64": "0.14.54",
+ "esbuild-linux-32": "0.14.54",
+ "esbuild-linux-64": "0.14.54",
+ "esbuild-linux-arm": "0.14.54",
+ "esbuild-linux-arm64": "0.14.54",
+ "esbuild-linux-mips64le": "0.14.54",
+ "esbuild-linux-ppc64le": "0.14.54",
+ "esbuild-linux-riscv64": "0.14.54",
+ "esbuild-linux-s390x": "0.14.54",
+ "esbuild-netbsd-64": "0.14.54",
+ "esbuild-openbsd-64": "0.14.54",
+ "esbuild-sunos-64": "0.14.54",
+ "esbuild-windows-32": "0.14.54",
+ "esbuild-windows-64": "0.14.54",
+ "esbuild-windows-arm64": "0.14.54"
+ }
+ },
+ "node_modules/esbuild-android-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
+ "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-android-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
+ "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-darwin-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
+ "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-darwin-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
+ "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-freebsd-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
+ "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-freebsd-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
+ "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-32": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
+ "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
+ "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-arm": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
+ "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
+ "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-mips64le": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
+ "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-ppc64le": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
+ "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-riscv64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
+ "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-s390x": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
+ "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-netbsd-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
+ "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-openbsd-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
+ "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-sunos-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
+ "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-32": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
+ "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
+ "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
+ "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/filesize": {
+ "version": "10.1.6",
+ "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz",
+ "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 10.4.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
+ "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mithril": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/mithril/-/mithril-2.2.12.tgz",
+ "integrity": "sha512-EpHb0DkcfwB+83NYvU8WMGBib+/TBtFLzdvYYXijqcW0o1i6cpVPklrCglJi2COXT2MG0Kih81CNPCoZkj0vag==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+ "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz",
+ "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "2.77.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz",
+ "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/sass": {
+ "version": "1.83.4",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz",
+ "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.0.2",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "2.9.18",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.18.tgz",
+ "integrity": "sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.14.27",
+ "postcss": "^8.4.13",
+ "resolve": "^1.22.0",
+ "rollup": ">=2.59.0 <2.78.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": ">=12.2.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "less": "*",
+ "sass": "*",
+ "stylus": "*"
+ },
+ "peerDependenciesMeta": {
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/extensions/content-manager/package.json b/extensions/content-manager/package.json
new file mode 100644
index 0000000..7b93c35
--- /dev/null
+++ b/extensions/content-manager/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "publisher-dashboard",
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build --base ./ --minify false",
+ "watch": "vite build --watch",
+ "preview": "vite preview",
+ "server": "uv run fastapi dev app.py"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^6.7.2",
+ "@fortawesome/free-regular-svg-icons": "^6.7.2",
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
+ "@popperjs/core": "^2.11.8",
+ "bootstrap": "^5.3.3",
+ "date-fns": "^4.1.0",
+ "filesize": "^10.1.6",
+ "mithril": "^2.0.4"
+ },
+ "devDependencies": {
+ "prettier": "3.4.2",
+ "sass": "^1.83.4",
+ "vite": "^2.6.2"
+ }
+}
diff --git a/extensions/content-manager/requirements.txt b/extensions/content-manager/requirements.txt
new file mode 100644
index 0000000..92b8947
--- /dev/null
+++ b/extensions/content-manager/requirements.txt
@@ -0,0 +1,4 @@
+# requirements.txt auto-generated by Posit Publisher
+# using /Users/me/Projects/connect-extensions/extensions/content-manager/.venv/bin/python
+fastapi==0.115.6
+posit_sdk @ git+https://github.com/posit-dev/posit-sdk-py.git@main
diff --git a/extensions/content-manager/scss/index.scss b/extensions/content-manager/scss/index.scss
new file mode 100644
index 0000000..05efff5
--- /dev/null
+++ b/extensions/content-manager/scss/index.scss
@@ -0,0 +1,29 @@
+// index.scss
+
+$blue: #447099;
+$indigo: #3d348b;
+$purple: #9a4665;
+$pink: #d96b94;
+$red: #d44000;
+$orange: #ee6331;
+$yellow: #e7b10a;
+$green: #72994e;
+$teal: #419599;
+$cyan: #53b0ae;
+$gray: #404041;
+
+$colors: (
+ "blue": $blue,
+ "indigo": $indigo,
+ "purple": $purple,
+ "pink": $pink,
+ "red": $red,
+ "orange": $orange,
+ "yellow": $yellow,
+ "green": $green,
+ "teal": $teal,
+ "cyan": $cyan,
+ "gray": $gray,
+);
+
+@import "bootstrap/scss/bootstrap";
diff --git a/extensions/content-manager/src/components/About.js b/extensions/content-manager/src/components/About.js
new file mode 100644
index 0000000..5f29764
--- /dev/null
+++ b/extensions/content-manager/src/components/About.js
@@ -0,0 +1,60 @@
+import m from "mithril";
+
+import { format, formatDistanceToNow } from "date-fns";
+
+import Content from "../models/Content";
+
+const About = {
+ error: null,
+
+ oninit: function (vnode) {
+ try {
+ Content.load(vnode.attrs.content_id);
+ } catch (err) {
+ this.error = "Failed to load author.";
+ console.error(err);
+ }
+ },
+
+ onremove: function () {
+ Content.reset();
+ },
+
+ view: function (vnode) {
+ if (this.error) {
+ return m("div", { class: "error" }, this.error);
+ }
+
+ const content = Content.data;
+ if (content === null) {
+ return "";
+ }
+
+ const desc = content?.description;
+ const updated = content?.last_deployed_time;
+ const created = content?.created_time;
+
+ return m(".pt-3.border-top", [
+ m(".", [
+ m("h5", "About"),
+ m("p", desc || m("i", "No Description")),
+ m(
+ "p",
+ m(
+ "small.text-body-secondary",
+ "Updated " + formatDistanceToNow(updated, { addSuffix: true }),
+ ),
+ ),
+ m(
+ "p",
+ m(
+ "small.text-body-secondary",
+ "Created on " + format(created, "MMMM do, yyyy"),
+ ),
+ ),
+ ]),
+ ]);
+ },
+};
+
+export default About;
diff --git a/extensions/content-manager/src/components/Author.js b/extensions/content-manager/src/components/Author.js
new file mode 100644
index 0000000..043f90f
--- /dev/null
+++ b/extensions/content-manager/src/components/Author.js
@@ -0,0 +1,63 @@
+import m from "mithril";
+
+import { formatDistanceToNow } from "date-fns";
+
+import Author from "../models/Author";
+
+export default {
+ error: null,
+
+ oninit: function (vnode) {
+ try {
+ Author.load(vnode.attrs.content_id);
+ } catch (err) {
+ this.error = "Failed to load author.";
+ console.error(err);
+ }
+ },
+
+ onremove: function () {
+ Author.reset();
+ },
+
+ view: function (vnode) {
+ if (this.error) {
+ return m("div", { class: "error" }, this.error);
+ }
+
+ const author = Author.data;
+ if (author === null) {
+ return "";
+ }
+
+ return m(".pt-3.border-top", [
+ m(".", [
+ m("h5", "Author"),
+ m("p", author?.first_name + " " + author?.last_name),
+ m(
+ "p",
+ m("small.text-body-secondary.align-items-center", [
+ m(".fa-regular.fa-at"),
+ " ",
+ author?.username,
+ ]),
+ ),
+ m(
+ "p",
+ m("small.text-body-secondary.align-items-center", [
+ m(".fa-regular.fa-envelope"),
+ " ",
+ m("a", { href: `mailto:${author?.email}` }, author?.email),
+ ]),
+ ),
+ m(
+ "p",
+ m("small.text-body-secondary", [
+ "Active ",
+ formatDistanceToNow(author?.active_time, { addSuffix: true }),
+ ]),
+ ),
+ ]),
+ ]);
+ },
+};
diff --git a/extensions/content-manager/src/components/ContentsComponent.js b/extensions/content-manager/src/components/ContentsComponent.js
new file mode 100644
index 0000000..ae7e9db
--- /dev/null
+++ b/extensions/content-manager/src/components/ContentsComponent.js
@@ -0,0 +1,95 @@
+import m from "mithril";
+import { format } from "date-fns";
+import Contents from "../models/Contents";
+import Languages from "./Languages";
+
+const ContentsComponent = {
+ error: null,
+
+ oninit: function () {
+ try {
+ Contents.load();
+ } catch (err) {
+ this.error = "Failed to load data.";
+ console.error(err);
+ }
+ },
+
+ view: () => {
+ if (this.error) {
+ return m("div", { class: "error" }, this.error);
+ }
+
+ const contents = Contents.data;
+ if (contents === null) {
+ return;
+ }
+
+ if (contents.length === 0) {
+ return "";
+ }
+
+ return m(
+ "table",
+ { class: "table" },
+ m(
+ "thead",
+ m("tr", [
+ m("th", { scope: "col" }, ""),
+ m("th", { scope: "col" }, "Title"),
+ m("th", { scope: "col" }, "Language"),
+ m("th", { scope: "col" }, "Running Processes"),
+ m("th", { scope: "col" }, "Last Updated"),
+ m("th", { scope: "col" }, "Date Added"),
+ m("th", { scope: "col" }, ""),
+ ]),
+ ),
+ m(
+ "tbody",
+ Contents.data.map((content) => {
+ const guid = content["guid"];
+ return m(
+ "tr",
+ {
+ style: { cursor: "pointer" },
+ onclick: () => m.route.set(`/contents/${guid}`),
+ },
+ [
+ m(
+ "td",
+ m("", {
+ class: "fa-solid fa-gear text-secondary",
+ }),
+ ),
+ m(
+ "td",
+ m(
+ ".link-primary",
+ content["title"] || m("i", "No Name"),
+ ),
+ ),
+ m(
+ "td",
+ m(Languages, content)
+ ),
+ m("td", content?.processes.length),
+ m("td", format(content["last_deployed_time"], "MMM do, yyyy")),
+ m("td", format(content["created_time"], "MMM do, yyyy")),
+ m(
+ "td",
+ m("a", {
+ class: "fa-solid fa-arrow-up-right-from-square",
+ href: content["content_url"],
+ target: "_blank",
+ onclick: (e) => e.stopPropagation(),
+ }),
+ ),
+ ],
+ );
+ }),
+ ),
+ );
+ },
+};
+
+export default ContentsComponent;
diff --git a/extensions/content-manager/src/components/Languages.js b/extensions/content-manager/src/components/Languages.js
new file mode 100644
index 0000000..3244ee6
--- /dev/null
+++ b/extensions/content-manager/src/components/Languages.js
@@ -0,0 +1,79 @@
+import m from "mithril";
+
+// api - R code defining a Plumber API.
+
+// jupyter-voila A Voila interactive dashboard.
+
+const getLanguages = (content) => {
+ const languages = new Set();
+
+ switch (content?.app_mode) {
+ case "jupyter-static":
+ case "jupyter-static":
+ case "python-api":
+ case "python-bokeh":
+ case "python-dash":
+ case "python-fastapi":
+ case "python-gradio":
+ case "python-shiny":
+ case "python-streamlit":
+ case "tensorflow-saved-model":
+ languages.add("Python");
+ break;
+ case "quarto-shiny":
+ case "quarto-static":
+ languages.add("Quarto");
+ break;
+ case "rmd-shiny":
+ case "rmd-static":
+ case "shiny":
+ languages.add("R");
+ break;
+ case "static":
+ languages.add("Static");
+ break;
+ default:
+ break;
+ }
+
+ switch (content?.content_category) {
+ case "plot":
+ languages.add("Static")
+ languages.add("Plot");
+ break;
+ case "pin":
+ languages.add("Static");
+ languages.add("Pin")
+ break;
+ case "rmd-static":
+ languages.add("Static")
+ languages.add("Site")
+ break;
+ default:
+ break;
+ }
+
+ if (content["r_version"] != null && content["r_version"] !== "") {
+ languages.add("R");
+ }
+ if (content["py_version"] != null && content["py_version"] !== "") {
+ languages.add("Python");
+ }
+ if (content["quarto_version"] != null && content["quarto_version"] !== "") {
+ languages.add("Quarto");
+ }
+ if (content["content_category"] === "pin") {
+ languages.add("Pin");
+ }
+
+ return [...languages].sort();
+};
+
+export default {
+ view: function (vnode) {
+ const languages = getLanguages(vnode.attrs);
+ return languages.map((language) => {
+ return m("span", { class: "mx-1 badge text-bg-primary" }, language);
+ });
+ },
+};
diff --git a/extensions/content-manager/src/components/Metrics.js b/extensions/content-manager/src/components/Metrics.js
new file mode 100644
index 0000000..00deaba
--- /dev/null
+++ b/extensions/content-manager/src/components/Metrics.js
@@ -0,0 +1,80 @@
+import m from "mithril";
+import Metrics from "../models/Metrics";
+
+const TimeseriesChart = {
+ oncreate: function (vnode) {
+ // Initialize the chart when the component is created
+ const ctx = vnode.dom.getContext('2d');
+ const labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
+ const data = [10, 20, 15, 25, 30, 20];
+
+ new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: labels,
+ datasets: [{
+ label: 'Views',
+ data: data,
+ borderColor: 'rgba(75, 192, 192, 1)',
+ backgroundColor: 'rgba(75, 192, 192, 0.2)',
+ tension: 0.4 // Smooth line
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: 'Month'
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Value'
+ },
+ beginAtZero: true
+ }
+ }
+ }
+ });
+ },
+ view: function () {
+ return m('canvas', { style: 'width: 100%; height: 400px;' });
+ }
+};
+
+export default {
+ error: null,
+
+ oninit: function (vnode) {
+ try {
+ Metrics.load(vnode.attrs.id);
+ } catch (err) {
+ this.error = "Failed to load data.";
+ console.error(err);
+ }
+ },
+
+ view: function (vnode) {
+ if (this.error) {
+ return m("div", { class: "error" }, this.error);
+ }
+
+ const metrics = Metrics.data;
+ if (metrics === null) {
+ return;
+ }
+
+ const start = Math.min(...metrics.map(metric => new Date(metric?.started)))
+ const ended = Math.max(...metrics.map(metric => new Date(metric?.ended)))
+ console.log(start, ended)
+
+ return m(".pt-3.border-top", [
+ m("h5", "Metrics"),
+ m(TimeseriesChart),
+ ]);
+ },
+};
diff --git a/extensions/content-manager/src/components/Processes.js b/extensions/content-manager/src/components/Processes.js
new file mode 100644
index 0000000..3a0f154
--- /dev/null
+++ b/extensions/content-manager/src/components/Processes.js
@@ -0,0 +1,136 @@
+import m from "mithril";
+import { formatDistanceToNow } from "date-fns";
+import { filesize } from "filesize";
+import Processes from "../models/Processes";
+import Process from "../models/Process";
+
+const StopButton = {
+ oninit(vnode) {
+ vnode.state.isHovered = false;
+ vnode.state.disabled = false;
+ },
+
+ view(vnode) {
+ return m(
+ "button",
+ {
+ class: "btn btn-link text-danger p-0",
+ disabled: vnode.state.disabled,
+ onclick: () => {
+ if (vnode.state.disabled) {
+ return;
+ }
+
+ vnode.state.disabled = true;
+ m.redraw();
+
+ console.log(`Stopping process ${vnode.attrs.process_id}`);
+ Processes.destroy(
+ vnode.attrs.content_id,
+ vnode.attrs.process_id
+ )
+ .then(() => {
+ console.log(`Stopped process ${vnode.attrs.process_id}`);
+ Processes.reset();
+ return Processes.load();
+ })
+ .then(() => {
+ m.redraw(); // Trigger UI refresh after reload
+ })
+ .catch((err) => {
+ console.error("Failed to reload processes:", err);
+ vnode.state.disabled = false; // Re-enable button on error
+ m.redraw();
+ });
+ },
+ title: "Stop Process",
+ onmouseover: () => {
+ vnode.state.isHovered = true;
+ m.redraw();
+ },
+ onmouseout: () => {
+ vnode.state.isHovered = false;
+ m.redraw();
+ },
+ },
+ m(
+ `i.${vnode.state.isHovered ? "fa-solid" : "fa-regular"}.fa-circle-stop`,
+ {
+ style: "font-size: 1.2rem;",
+ }
+ )
+ );
+ },
+};
+
+
+export default {
+ error: null,
+
+ oninit: function (vnode) {
+ try {
+ Processes.load(vnode.attrs.id);
+ } catch (err) {
+ this.error = "Failed to load data.";
+ console.error(err);
+ }
+ },
+
+ onremove: function (vnode) {
+ Processes.reset();
+ },
+
+ view: function (vnode) {
+ if (this.error) {
+ return m("div", { class: "error" }, this.error);
+ }
+
+ const processes = Processes.data;
+ if (processes === null || processes.length === 0) {
+ return m(".pt-3.border-top", [
+ m("h5", "Processes"),
+ m("p.text-dark", "There are no server processes running at this time...")
+ ])
+ }
+
+ return m(".pt-3.border-top", [
+ m("h5", "Processes"),
+ m(
+ "table.table",
+ m(
+ "thead",
+ m("tr", [
+ m("th", { scope: "col" }, ""),
+ m("th", { scope: "col" }, "Id"),
+ m("th", { scope: "col" }, "Started"),
+ m("th", { scope: "col" }, "CPUs"),
+ m("th", { scope: "col" }, "Memory"),
+ m("th", { scope: "col" }, "Hostname"),
+ ]),
+ ),
+ m(
+ "tbody",
+ processes.map((process) => {
+ return m("tr.align-items-center", [
+ m(
+ "td.text-center.py-2",
+ m(StopButton, {
+ content_id: vnode.attrs.id,
+ process_id: process?.job_key,
+ }),
+ ),
+ m("td", process?.pid),
+ m(
+ "td",
+ formatDistanceToNow(process?.start_time, { addSuffix: true }),
+ ),
+ m("td", Number(process?.cpu_current).toFixed(2)),
+ m("td", filesize(process?.ram)),
+ m("td", process?.hostname),
+ ]);
+ }),
+ ),
+ ),
+ ]);
+ },
+};
diff --git a/extensions/content-manager/src/components/Releases.js b/extensions/content-manager/src/components/Releases.js
new file mode 100644
index 0000000..c5bae7f
--- /dev/null
+++ b/extensions/content-manager/src/components/Releases.js
@@ -0,0 +1,102 @@
+import m from "mithril";
+
+import { format } from "date-fns";
+
+import Releases from "../models/Releases";
+
+const Release = {
+ view: function (vnode) {
+ return m(".row.my-3", [
+ m(".d-flex.align-items-center", [
+ m("i.fa-solid.fa-code-commit.me-1"),
+ m(".font-monospace", vnode.attrs?.id),
+ vnode.attrs?.active
+ ? m("i.fa-regular.fa-circle-check.ms-1.text-success")
+ : "",
+ ]),
+ m(
+ "small.text-secondary",
+ format(vnode.attrs?.created_time, "MMM do, yyyy"),
+ ),
+ ]);
+ },
+};
+
+export default {
+ error: null,
+
+ oninit: function (vnode) {
+ try {
+ Releases.load(vnode.attrs.content_id);
+ } catch (err) {
+ this.error = "Failed to load releases.";
+ console.error(err);
+ }
+ },
+
+ onremove: function () {
+ Releases.reset();
+ },
+
+ view: function () {
+ if (this.error) {
+ return m("div", { class: "error" }, this.error);
+ }
+
+ const releases = Releases.data;
+ if (releases === null) {
+ return;
+ }
+
+ if (releases.length === 0) {
+ return;
+ }
+
+ let recent = releases.slice(0, 3);
+ recent = recent.map((release) => {
+ return m(Release, release);
+ });
+
+ let old = releases.slice(3);
+ old = old.map((release) => {
+ return m(Release, release);
+ });
+ if (old.length === 0) {
+ old = "";
+ } else {
+ old = [
+ m(
+ "",
+ {
+ class: "text-secondary",
+ type: "button",
+ "data-bs-toggle": "collapse",
+ "data-bs-target": "#old",
+ onclick: (e) => {
+ const icon = e.target.querySelector("i") || e.target;
+ const currentRotation =
+ icon.style.transform === "rotate(90deg)"
+ ? "rotate(0deg)"
+ : "rotate(90deg)";
+ icon.style.transform = currentRotation;
+ icon.style.transition = "transform 0.3s ease";
+ },
+ },
+ m("i", { class: "fa-solid fa-ellipsis" }),
+ ),
+ m("div", { class: "collapse", id: "old" }, old),
+ ];
+ }
+
+ return m(".pt-3.border-top", [
+ m(".", [
+ m("h5", [
+ "Releases ",
+ m("span.badge.rounded-pill.text-bg-secondary", releases.length),
+ ]),
+ recent,
+ old,
+ ]),
+ ]);
+ },
+};
diff --git a/extensions/content-manager/src/index.js b/extensions/content-manager/src/index.js
new file mode 100644
index 0000000..42103c8
--- /dev/null
+++ b/extensions/content-manager/src/index.js
@@ -0,0 +1,25 @@
+import m from "mithril";
+
+import "bootstrap/dist/js/bootstrap.bundle.min.js";
+import "@fortawesome/fontawesome-free/css/all.min.css";
+
+import "../scss/index.scss";
+
+
+import Home from "./views/Home";
+import Edit from "./views/Edit";
+import Layout from './views/Layout'
+
+const root = document.getElementById("app");
+m.route(root, "/contents", {
+ "/contents": {
+ render: function () {
+ return m(Layout, m(Home))
+ },
+ },
+ "/contents/:id": {
+ render: function (vnode) {
+ return m(Layout, m(Edit, vnode.attrs));
+ },
+ },
+});
diff --git a/extensions/content-manager/src/models/Author.js b/extensions/content-manager/src/models/Author.js
new file mode 100644
index 0000000..934cc57
--- /dev/null
+++ b/extensions/content-manager/src/models/Author.js
@@ -0,0 +1,34 @@
+import m from "mithril";
+
+const Author = {
+ data: null,
+ _fetch: null,
+
+ load: function (id) {
+ if (this.data) {
+ return Promise.resolve(this.data);
+ }
+
+ if (this._fetch) {
+ return this._fetch;
+ }
+
+ this._fetch = m
+ .request({ method: "GET", url: `api/contents/${id}/author` })
+ .then((result) => {
+ this.data = result;
+ this._fetch = null;
+ })
+ .catch((err) => {
+ this._fetch = null;
+ throw err;
+ });
+ },
+
+ reset: function () {
+ this.data = null;
+ this._fetch = null;
+ },
+};
+
+export default Author;
diff --git a/extensions/content-manager/src/models/Content.js b/extensions/content-manager/src/models/Content.js
new file mode 100644
index 0000000..a3599d6
--- /dev/null
+++ b/extensions/content-manager/src/models/Content.js
@@ -0,0 +1,34 @@
+import m from "mithril";
+
+const Content = {
+ data: null,
+ _fetch: null,
+
+ load: function (id) {
+ if (this.data) {
+ return Promise.resolve(this.data);
+ }
+
+ if (this._fetch) {
+ return this._fetch;
+ }
+
+ this._fetch = m
+ .request({ method: "GET", url: `api/contents/${id}` })
+ .then((result) => {
+ this.data = result;
+ this._fetch = null;
+ })
+ .catch((err) => {
+ this._fetch = null;
+ throw err;
+ });
+ },
+
+ reset: function () {
+ this.data = null;
+ this._fetch = null;
+ },
+};
+
+export default Content;
diff --git a/extensions/content-manager/src/models/Contents.js b/extensions/content-manager/src/models/Contents.js
new file mode 100644
index 0000000..3c51a91
--- /dev/null
+++ b/extensions/content-manager/src/models/Contents.js
@@ -0,0 +1,32 @@
+import m from "mithril";
+
+export default {
+ data: null,
+ _fetch: null,
+
+ load: function (id) {
+ if (this.data) {
+ return Promise.resolve(this.data);
+ }
+
+ if (this._fetch) {
+ return this._fetch;
+ }
+
+ this._fetch = m
+ .request({ method: "GET", url: `api/contents` })
+ .then((result) => {
+ this.data = result;
+ this._fetch = null;
+ })
+ .catch((err) => {
+ this._fetch = null;
+ throw err;
+ });
+ },
+
+ reset: function () {
+ this.data = null;
+ this._fetch = null;
+ },
+};
diff --git a/extensions/content-manager/src/models/Metrics.js b/extensions/content-manager/src/models/Metrics.js
new file mode 100644
index 0000000..75159ac
--- /dev/null
+++ b/extensions/content-manager/src/models/Metrics.js
@@ -0,0 +1,32 @@
+import m from "mithril";
+
+export default {
+ data: null,
+ _fetch: null,
+
+ load: function (id) {
+ if (this.data) {
+ return Promise.resolve(this.data);
+ }
+
+ if (this._fetch) {
+ return this._fetch;
+ }
+
+ this._fetch = m
+ .request({ method: "GET", url: `api/contents/${id}/metrics` })
+ .then((result) => {
+ this.data = result;
+ this._fetch = null;
+ })
+ .catch((err) => {
+ this._fetch = null;
+ throw err;
+ });
+ },
+
+ reset: function () {
+ this.data = null;
+ this._fetch = null;
+ },
+};
diff --git a/extensions/content-manager/src/models/Process.js b/extensions/content-manager/src/models/Process.js
new file mode 100644
index 0000000..49afba1
--- /dev/null
+++ b/extensions/content-manager/src/models/Process.js
@@ -0,0 +1,12 @@
+import m from "mithril";
+
+const Process = {
+ destroy: function (content_id, process_id) {
+ return m.request({
+ method: "DELETE",
+ url: `api/contents/${content_id}/processes/${process_id}`,
+ });
+ },
+};
+
+export default Process;
diff --git a/extensions/content-manager/src/models/Processes.js b/extensions/content-manager/src/models/Processes.js
new file mode 100644
index 0000000..9dcce93
--- /dev/null
+++ b/extensions/content-manager/src/models/Processes.js
@@ -0,0 +1,41 @@
+import m from "mithril";
+
+const Processes = {
+ data: null,
+ _fetch: null,
+
+ load: function (id) {
+ if (this.data) {
+ return Promise.resolve(this.data);
+ }
+
+ if (this._fetch) {
+ return this._fetch;
+ }
+
+ this._fetch = m
+ .request({ method: "GET", url: `api/contents/${id}/processes` })
+ .then((result) => {
+ this.data = result;
+ this._fetch = null;
+ })
+ .catch((err) => {
+ this._fetch = null;
+ throw err;
+ });
+ },
+
+ destroy: function (content_id, process_id) {
+ return m.request({
+ method: "DELETE",
+ url: `api/contents/${content_id}/processes/${process_id}`,
+ });
+ },
+
+ reset: function () {
+ this.data = null;
+ this._fetch = null;
+ },
+};
+
+export default Processes;
diff --git a/extensions/content-manager/src/models/Releases.js b/extensions/content-manager/src/models/Releases.js
new file mode 100644
index 0000000..843a07b
--- /dev/null
+++ b/extensions/content-manager/src/models/Releases.js
@@ -0,0 +1,32 @@
+import m from "mithril";
+
+export default {
+ data: null,
+ _fetch: null,
+
+ load: function (id) {
+ if (this.data) {
+ return Promise.resolve(this.data);
+ }
+
+ if (this._fetch) {
+ return this._fetch;
+ }
+
+ this._fetch = m
+ .request({ method: "GET", url: `api/contents/${id}/releases` })
+ .then((result) => {
+ this.data = result;
+ this._fetch = null;
+ })
+ .catch((err) => {
+ this._fetch = null;
+ throw err;
+ });
+ },
+
+ reset: function () {
+ this.data = null;
+ this._fetch = null;
+ },
+};
diff --git a/extensions/content-manager/src/views/Edit.js b/extensions/content-manager/src/views/Edit.js
new file mode 100644
index 0000000..fed5be1
--- /dev/null
+++ b/extensions/content-manager/src/views/Edit.js
@@ -0,0 +1,84 @@
+import m from "mithril";
+
+import Content from "../models/Content";
+import About from "../components/About";
+import Releases from "../components/Releases";
+import Processes from "../components/Processes";
+import Author from "../components/Author";
+
+import Languages from "../components/Languages";
+
+const Edit = {
+ error: null,
+
+ oninit: function (vnode) {
+ try {
+ Content.load(vnode.attrs.id);
+ } catch (err) {
+ this.error = "Failed to load data.";
+ console.error(err);
+ }
+ },
+
+ onremove: function () {
+ Content.reset();
+ },
+
+ view: function (vnode) {
+ if (this.error) {
+ return m("div", { class: "error" }, this.error);
+ }
+
+ const content = Content.data;
+ if (content === null) {
+ return "";
+ }
+
+ return m(
+ "div",
+ m(".d-flex.flex-row.justify-content-between.align-items-center.my-3", [
+ m("h2.mb-0", content?.title || m("i", "No Name")),
+ m(
+ "a.btn.btn-lg.btn-outline-primary.d-flex.align-items-center.justify-content-center.gap-2",
+ {
+ href: content?.dashboard_url,
+ target: "_blank",
+ },
+ ["Open in Connect", m("i.fa-solid.fa-arrow-up-right-from-square")],
+ ),
+ ]),
+ m(".row", m(".pb-3", m(Languages, content))),
+ m(".row.", [
+ m(".col-8", [
+ m(
+ ".pt-3.pb-3.border-top",
+ m("iframe", {
+ src: content?.content_url,
+ width: "100%",
+ style: { minHeight: "50vh" },
+ frameborder: "0",
+ allowfullscreen: true,
+ class: "border border-bottom-0 border-light rounded p-1",
+ }),
+ ),
+ m(Processes, { id: vnode.attrs.id }),
+ ]),
+ m(".col-4", [
+ m(About, {
+ desc: content?.description,
+ updated: content?.last_deployed_time,
+ created: content?.created_time,
+ }),
+ m(Author, {
+ content_id: content?.guid,
+ }),
+ m(Releases, {
+ content_id: content?.guid,
+ }),
+ ]),
+ ]),
+ );
+ },
+};
+
+export default Edit;
diff --git a/extensions/content-manager/src/views/Home.js b/extensions/content-manager/src/views/Home.js
new file mode 100644
index 0000000..a299eb0
--- /dev/null
+++ b/extensions/content-manager/src/views/Home.js
@@ -0,0 +1,20 @@
+import m from "mithril";
+
+import ContentsComponent from "../components/ContentsComponent";
+
+const Home = {
+ view: function () {
+ return m(
+ "div",
+ m("h1", "Content"),
+ m(
+ "p",
+ { class: "text-secondary" },
+ "Manage your content and their settings here.",
+ ),
+ m(ContentsComponent),
+ );
+ },
+};
+
+export default Home;
diff --git a/extensions/content-manager/src/views/Layout.js b/extensions/content-manager/src/views/Layout.js
new file mode 100644
index 0000000..aec6953
--- /dev/null
+++ b/extensions/content-manager/src/views/Layout.js
@@ -0,0 +1,37 @@
+import m from "mithril";
+
+export default {
+ view: (vnode) => {
+ return m("div", [
+ // Navbar Header
+ m("nav.navbar.navbar-expand-lg.bg-light", [
+ m("div.container-xxl", [
+ m(
+ "a.navbar-brand",
+ {
+ style: { cursor: "pointer" },
+ onclick: () => m.route.set(`/`),
+ },
+ "Publisher Command Center",
+ ),
+ m("ul.navbar-nav.me-auto", [
+ m(
+ "li.nav-item",
+ m(
+ "a.nav-link",
+ {
+ style: { cursor: "pointer" },
+ onclick: () => m.route.set("/contents"),
+ },
+ "Content",
+ ),
+ ),
+ ]),
+ ]),
+ ]),
+
+ // Content Wrapper
+ m("div.container-xxl", vnode.children),
+ ]);
+ },
+};
diff --git a/extensions/content-manager/vite.config.js b/extensions/content-manager/vite.config.js
new file mode 100644
index 0000000..3a89c85
--- /dev/null
+++ b/extensions/content-manager/vite.config.js
@@ -0,0 +1,17 @@
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ esbuild: {
+ jsx: "transform",
+ jsxFactory: "m",
+ jsxFragment: "'['",
+ },
+ preview: {
+ proxy: {
+ "/api": {
+ target: "http://localhost:8000",
+ changeOrigin: true,
+ },
+ },
+ },
+});