mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-03-26 19:03:08 +00:00
Compare commits
5 commits
33311c51c8
...
9de222620c
| Author | SHA1 | Date | |
|---|---|---|---|
| 9de222620c | |||
| c49feb726f | |||
| 3e14fe7326 | |||
| c8e90fccbf | |||
| 3b1f8aa177 |
32 changed files with 526 additions and 415 deletions
|
|
@ -18,7 +18,7 @@
|
|||
- MSVC toolchain for Rust
|
||||
|
||||
**Linux:**
|
||||
- podman-compose
|
||||
- podman compose
|
||||
|
||||
### Quick Setup
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ cp compose/.env.production.example compose/.env.production
|
|||
|
||||
# 4. Deploy
|
||||
cd compose
|
||||
podman-compose --env-file .env.production -f production.yml up -d
|
||||
podman compose --env-file .env.production -f production.yml up -d
|
||||
|
||||
# 5. Access at http://localhost:4321
|
||||
```
|
||||
|
|
@ -57,7 +57,7 @@ cp compose/.env.demo.example compose/.env.demo
|
|||
|
||||
# 2. Deploy
|
||||
cd compose
|
||||
podman-compose --env-file .env.demo -f demo.yml up -d
|
||||
podman compose --env-file .env.demo -f demo.yml up -d
|
||||
|
||||
# 3. Access at http://localhost:4322
|
||||
```
|
||||
|
|
@ -105,8 +105,8 @@ To reset the demo to a clean state:
|
|||
./scripts/demo-reset.sh
|
||||
|
||||
# Or manually:
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml down -v
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml down -v
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
|
@ -141,7 +141,7 @@ For local development without containers:
|
|||
|
||||
```bash
|
||||
# 1. Start only the database
|
||||
podman-compose -f compose/dev.yml up -d
|
||||
podman compose -f compose/dev.yml up -d
|
||||
|
||||
# 2. Configure backend environment
|
||||
cp backend/.env.example backend/.env
|
||||
|
|
@ -159,10 +159,10 @@ npm run dev
|
|||
|
||||
```bash
|
||||
# View all logs
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml logs -f
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml logs -f
|
||||
|
||||
# View specific service
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml logs -f backend
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml logs -f backend
|
||||
|
||||
# Check health
|
||||
curl http://localhost:3001/health
|
||||
|
|
@ -173,10 +173,10 @@ curl http://localhost:3001/health
|
|||
### Database connection issues
|
||||
```bash
|
||||
# Check if postgres is running
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml ps
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml ps
|
||||
|
||||
# View postgres logs
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml logs postgres
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml logs postgres
|
||||
```
|
||||
|
||||
### Migration failures
|
||||
|
|
@ -194,6 +194,6 @@ SELECT * FROM _sqlx_migrations;
|
|||
### Reset everything
|
||||
```bash
|
||||
# Nuclear option - removes all data and volumes
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml down -v
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml down -v
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
```
|
||||
|
|
|
|||
314
README.md
314
README.md
|
|
@ -1,157 +1,157 @@
|
|||
# Likwid - Modular Governance Platform
|
||||
|
||||
## Composable Governance Infrastructure
|
||||
|
||||
A modular toolkit for deliberation, voting, delegation, moderation, and plugins — configure what you need per community.
|
||||
|
||||
Likwid is an open-source platform for participatory governance. Assemble decision-making workflows from modular building blocks: deliberation, voting methods, delegation, moderation, and plugins.
|
||||
|
||||
> *"We are citizens of the 21st century, but we rely on institutions designed in the 19th century, through means designed in the 13th century. The problem is not democracy, it's the interface."*
|
||||
|
||||
## Philosophy
|
||||
|
||||
Likwid implements a set of principles for **modular governance infrastructure**:
|
||||
|
||||
- **Information must be understandable**, not just available
|
||||
- **Listening matters more than speaking** — structured deliberation over flame wars
|
||||
- **Voting should express nuance** — from simple approval to Schulze and quadratic methods
|
||||
- **Delegation should be fluid** — trust networks that adapt in real-time
|
||||
- **Governance should be composable** — workflows assembled from modules, not imposed
|
||||
|
||||
## Features
|
||||
|
||||
### Deliberative Democracy
|
||||
|
||||
- **Inform → Discuss → Decide** workflow for proposals
|
||||
- Resource libraries for informed participation
|
||||
- Optional facilitator role on proposals
|
||||
- "Read before discuss" requirements
|
||||
- Comment reactions for quality signals (agree/disagree/insightful/constructive/off-topic)
|
||||
|
||||
### Advanced Voting Methods
|
||||
|
||||
- **Approval Voting** — vote for multiple options
|
||||
- **Ranked Choice** — order preferences
|
||||
- **Schulze Method** — Condorcet-consistent pairwise comparison
|
||||
- **STAR Voting** — score + automatic runoff
|
||||
- **Quadratic Voting** — express intensity of preference
|
||||
|
||||
### Liquid Delegation
|
||||
|
||||
- Delegate your vote globally or within a community
|
||||
- Real-time transparency: see how delegates vote
|
||||
- Revoke delegation instantly
|
||||
- Transitive delegation chains
|
||||
- Delegation analytics and trust networks
|
||||
|
||||
### Modular Plugin System
|
||||
|
||||
- WASM-based sandboxed plugins
|
||||
- Per-community plugin configuration
|
||||
- Hook-based architecture (actions/filters)
|
||||
- Built-in and third-party plugins
|
||||
- Admin policy for signed/unsigned plugins
|
||||
|
||||
### Governance Infrastructure
|
||||
|
||||
- Multi-community platform support
|
||||
- Granular admin controls (platform mode, registration, moderation)
|
||||
- Tamper-evident public moderation ledger
|
||||
- Role-based access (admin, moderator, facilitator, member)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
| --- | --- |
|
||||
| **Backend** | Rust (Axum 0.8, Tokio, SQLx) |
|
||||
| **Frontend** | Astro + TypeScript |
|
||||
| **Database** | PostgreSQL 16 |
|
||||
| **Plugins** | WebAssembly (wasmtime) |
|
||||
| **Containers** | Podman (rootless) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**Windows:**
|
||||
|
||||
- Windows 10/11 with WSL2
|
||||
- Podman Desktop (WSL2 backend)
|
||||
- Rust (rustup, MSVC toolchain)
|
||||
- Node.js LTS
|
||||
|
||||
**Linux:**
|
||||
|
||||
- Podman + podman-compose
|
||||
- Rust (rustup)
|
||||
- Node.js LTS
|
||||
|
||||
### Development
|
||||
|
||||
```powershell
|
||||
# 1. Clone and configure
|
||||
git clone https://codeberg.org/likwid/likwid.git
|
||||
cd likwid
|
||||
$env:JWT_SECRET="dev_secret_change_me"
|
||||
|
||||
# 2. Start everything (database + backend + frontend)
|
||||
.\scripts\dev-start.ps1
|
||||
|
||||
# 3. Stop everything
|
||||
.\scripts\dev-stop.ps1
|
||||
```
|
||||
|
||||
The platform will be available at:
|
||||
|
||||
- **Frontend**: <http://localhost:4321>
|
||||
- **Backend API**: <http://localhost:3000>
|
||||
- **Setup Wizard**: <http://localhost:4321/setup> (first run)
|
||||
|
||||
### First Run
|
||||
|
||||
1. Navigate to `/register` to create the first user (automatically becomes admin)
|
||||
2. Complete platform setup at `/setup`
|
||||
3. Configure instance settings at `/admin/settings`
|
||||
4. Create your first community
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
likwid/
|
||||
├── backend/ # Rust backend
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # REST endpoints
|
||||
│ │ ├── auth/ # JWT authentication
|
||||
│ │ ├── models/ # Database models
|
||||
│ │ └── plugins/ # Plugin system (WASM + builtins)
|
||||
│ └── migrations/ # SQL migrations
|
||||
├── frontend/ # Astro frontend
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # Routes
|
||||
│ │ ├── layouts/ # Page layouts
|
||||
│ │ └── components/ # UI components
|
||||
├── compose/ # Podman compose files
|
||||
├── scripts/ # Dev scripts (cross-platform)
|
||||
└── docu_dev/ # Design documents
|
||||
```
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Be considerate** — Your work affects others
|
||||
2. **Be respectful** — Assume good intentions
|
||||
3. **Be collaborative** — Work transparently
|
||||
4. **Be pragmatic** — Results over debates
|
||||
5. **Find a third way** — Seek solutions that satisfy everyone
|
||||
|
||||
## License
|
||||
|
||||
EUPL-1.2
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Inspired by:
|
||||
|
||||
- [Pol.is](https://pol.is/) — Opinion mapping
|
||||
- [Decidim](https://decidim.org/) — Participatory democracy
|
||||
- [LiquidFeedback](https://liquidfeedback.org/) — Liquid democracy
|
||||
- [Equal Vote Coalition](https://www.equal.vote/) — STAR Voting
|
||||
# Likwid - Modular Governance Platform
|
||||
|
||||
## Composable Governance Infrastructure
|
||||
|
||||
A modular toolkit for deliberation, voting, delegation, moderation, and plugins — configure what you need per community.
|
||||
|
||||
Likwid is an open-source platform for participatory governance. Assemble decision-making workflows from modular building blocks: deliberation, voting methods, delegation, moderation, and plugins.
|
||||
|
||||
> *"We are citizens of the 21st century, but we rely on institutions designed in the 19th century, through means designed in the 13th century. The problem is not democracy, it's the interface."*
|
||||
|
||||
## Philosophy
|
||||
|
||||
Likwid implements a set of principles for **modular governance infrastructure**:
|
||||
|
||||
- **Information must be understandable**, not just available
|
||||
- **Listening matters more than speaking** — structured deliberation over flame wars
|
||||
- **Voting should express nuance** — from simple approval to Schulze and quadratic methods
|
||||
- **Delegation should be fluid** — trust networks that adapt in real-time
|
||||
- **Governance should be composable** — workflows assembled from modules, not imposed
|
||||
|
||||
## Features
|
||||
|
||||
### Deliberative Democracy
|
||||
|
||||
- **Inform → Discuss → Decide** workflow for proposals
|
||||
- Resource libraries for informed participation
|
||||
- Optional facilitator role on proposals
|
||||
- "Read before discuss" requirements
|
||||
- Comment reactions for quality signals (agree/disagree/insightful/constructive/off-topic)
|
||||
|
||||
### Advanced Voting Methods
|
||||
|
||||
- **Approval Voting** — vote for multiple options
|
||||
- **Ranked Choice** — order preferences
|
||||
- **Schulze Method** — Condorcet-consistent pairwise comparison
|
||||
- **STAR Voting** — score + automatic runoff
|
||||
- **Quadratic Voting** — express intensity of preference
|
||||
|
||||
### Liquid Delegation
|
||||
|
||||
- Delegate your vote globally or within a community
|
||||
- Real-time transparency: see how delegates vote
|
||||
- Revoke delegation instantly
|
||||
- Transitive delegation chains
|
||||
- Delegation analytics and trust networks
|
||||
|
||||
### Modular Plugin System
|
||||
|
||||
- WASM-based sandboxed plugins
|
||||
- Per-community plugin configuration
|
||||
- Hook-based architecture (actions/filters)
|
||||
- Built-in and third-party plugins
|
||||
- Admin policy for signed/unsigned plugins
|
||||
|
||||
### Governance Infrastructure
|
||||
|
||||
- Multi-community platform support
|
||||
- Granular admin controls (platform mode, registration, moderation)
|
||||
- Tamper-evident public moderation ledger
|
||||
- Role-based access (admin, moderator, facilitator, member)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
| --- | --- |
|
||||
| **Backend** | Rust (Axum 0.8, Tokio, SQLx) |
|
||||
| **Frontend** | Astro + TypeScript |
|
||||
| **Database** | PostgreSQL 16 |
|
||||
| **Plugins** | WebAssembly (wasmtime) |
|
||||
| **Containers** | Podman (rootless) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**Windows:**
|
||||
|
||||
- Windows 10/11 with WSL2
|
||||
- Podman Desktop (WSL2 backend)
|
||||
- Rust (rustup, MSVC toolchain)
|
||||
- Node.js LTS
|
||||
|
||||
**Linux:**
|
||||
|
||||
- Podman + podman compose
|
||||
- Rust (rustup)
|
||||
- Node.js LTS
|
||||
|
||||
### Development
|
||||
|
||||
```powershell
|
||||
# 1. Clone and configure
|
||||
git clone https://codeberg.org/likwid/likwid.git
|
||||
cd likwid
|
||||
$env:JWT_SECRET="dev_secret_change_me"
|
||||
|
||||
# 2. Start everything (database + backend + frontend)
|
||||
.\scripts\dev-start.ps1
|
||||
|
||||
# 3. Stop everything
|
||||
.\scripts\dev-stop.ps1
|
||||
```
|
||||
|
||||
The platform will be available at:
|
||||
|
||||
- **Frontend**: <http://localhost:4321>
|
||||
- **Backend API**: <http://localhost:3000>
|
||||
- **Setup Wizard**: <http://localhost:4321/setup> (first run)
|
||||
|
||||
### First Run
|
||||
|
||||
1. Navigate to `/register` to create the first user (automatically becomes admin)
|
||||
2. Complete platform setup at `/setup`
|
||||
3. Configure instance settings at `/admin/settings`
|
||||
4. Create your first community
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
likwid/
|
||||
├── backend/ # Rust backend
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # REST endpoints
|
||||
│ │ ├── auth/ # JWT authentication
|
||||
│ │ ├── models/ # Database models
|
||||
│ │ └── plugins/ # Plugin system (WASM + builtins)
|
||||
│ └── migrations/ # SQL migrations
|
||||
├── frontend/ # Astro frontend
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # Routes
|
||||
│ │ ├── layouts/ # Page layouts
|
||||
│ │ └── components/ # UI components
|
||||
├── compose/ # Podman compose files
|
||||
├── scripts/ # Dev scripts (cross-platform)
|
||||
└── docu_dev/ # Design documents
|
||||
```
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Be considerate** — Your work affects others
|
||||
2. **Be respectful** — Assume good intentions
|
||||
3. **Be collaborative** — Work transparently
|
||||
4. **Be pragmatic** — Results over debates
|
||||
5. **Find a third way** — Seek solutions that satisfy everyone
|
||||
|
||||
## License
|
||||
|
||||
EUPL-1.2
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Inspired by:
|
||||
|
||||
- [Pol.is](https://pol.is/) — Opinion mapping
|
||||
- [Decidim](https://decidim.org/) — Participatory democracy
|
||||
- [LiquidFeedback](https://liquidfeedback.org/) — Liquid democracy
|
||||
- [Equal Vote Coalition](https://www.equal.vote/) — STAR Voting
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ sqlx migrate run --source migrations_demo
|
|||
```bash
|
||||
# Uses separate database on port 5433, backend on 3001, frontend on 4322
|
||||
cp compose/.env.demo.example compose/.env.demo
|
||||
podman-compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
podman compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
```
|
||||
|
||||
### For real users (Production)
|
||||
|
|
@ -76,7 +76,7 @@ cp compose/.env.production.example compose/.env.production
|
|||
# Edit with secure passwords and your domain
|
||||
|
||||
# 2. Deploy
|
||||
podman-compose --env-file compose/.env.production -f compose/production.yml up -d
|
||||
podman compose --env-file compose/.env.production -f compose/production.yml up -d
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, setup_completed, instance_name, platform_mode,\n registration_enabled, registration_mode,\n default_community_visibility, allow_private_communities,\n default_plugin_policy, default_moderation_mode\n FROM instance_settings LIMIT 1",
|
||||
"query": "SELECT id, setup_completed, instance_name, platform_mode,\n theme_id, registration_enabled, registration_mode,\n default_community_visibility, allow_private_communities,\n default_plugin_policy, default_moderation_mode\n FROM instance_settings LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -25,31 +25,36 @@
|
|||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "theme_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "registration_enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "registration_mode",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "default_community_visibility",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "allow_private_communities",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "default_plugin_policy",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "default_moderation_mode",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
|
|
@ -67,8 +72,9 @@
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "18c0fb05da45a3eea514f660bc4ac4d6aca71442645666a9c08db8f2a564ff6c"
|
||||
"hash": "0620f314de8df0c7990ef63fda55f2ff646d5159c59d8288e4ffdfdb07dc159f"
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT setup_completed, instance_name, platform_mode,\n registration_enabled, registration_mode,\n single_community_id\n FROM instance_settings\n LIMIT 1",
|
||||
"query": "SELECT setup_completed, instance_name, theme_id, platform_mode,\n registration_enabled, registration_mode,\n single_community_id\n FROM instance_settings\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -15,21 +15,26 @@
|
|||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "platform_mode",
|
||||
"name": "theme_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "platform_mode",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "registration_enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "registration_mode",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "single_community_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
|
|
@ -43,8 +48,9 @@
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "200e864fa5778cf58d36d49f94a4006f7d104eb84e6f166b795df0f222ee93d8"
|
||||
"hash": "593dc329afc129680dd505221df649aa8cb544841fa78a5fe740adfbb4439502"
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE instance_settings SET\n setup_completed = true,\n setup_completed_at = NOW(),\n setup_completed_by = $1,\n instance_name = $2,\n platform_mode = $3,\n single_community_id = $4\n RETURNING id, setup_completed, instance_name, platform_mode,\n registration_enabled, registration_mode,\n default_community_visibility, allow_private_communities,\n default_plugin_policy, default_moderation_mode",
|
||||
"query": "UPDATE instance_settings SET\n setup_completed = true,\n setup_completed_at = NOW(),\n setup_completed_by = $1,\n instance_name = $2,\n platform_mode = $3,\n single_community_id = $4\n RETURNING id, setup_completed, instance_name, platform_mode,\n theme_id,\n registration_enabled, registration_mode,\n default_community_visibility, allow_private_communities,\n default_plugin_policy, default_moderation_mode",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -25,31 +25,36 @@
|
|||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "theme_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "registration_enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "registration_mode",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "default_community_visibility",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "allow_private_communities",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "default_plugin_policy",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "default_moderation_mode",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
|
|
@ -72,8 +77,9 @@
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b9586185e84644f0bd936d7bf5e9bec6ebeaba77ab354d0b7096d9334656497f"
|
||||
"hash": "8dd178663df95d64d72c776e3b8bda63851d2ad0e13a6d80b327610078ecbaeb"
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE instance_settings SET\n instance_name = COALESCE($1, instance_name),\n platform_mode = COALESCE($2, platform_mode),\n registration_enabled = COALESCE($3, registration_enabled),\n registration_mode = COALESCE($4, registration_mode)\n RETURNING id, setup_completed, instance_name, platform_mode,\n registration_enabled, registration_mode,\n default_community_visibility, allow_private_communities,\n default_plugin_policy, default_moderation_mode",
|
||||
"query": "UPDATE instance_settings SET\n instance_name = COALESCE($1, instance_name),\n theme_id = COALESCE($2, theme_id),\n platform_mode = COALESCE($3, platform_mode),\n registration_enabled = COALESCE($4, registration_enabled),\n registration_mode = COALESCE($5, registration_mode)\n RETURNING id, setup_completed, instance_name, platform_mode,\n theme_id, registration_enabled, registration_mode,\n default_community_visibility, allow_private_communities,\n default_plugin_policy, default_moderation_mode",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -25,37 +25,43 @@
|
|||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "theme_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "registration_enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "registration_mode",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "default_community_visibility",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "allow_private_communities",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "default_plugin_policy",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "default_moderation_mode",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
|
|
@ -72,8 +78,9 @@
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c35608b0d7569f739dda24b3da59b7b500ff26f5e79433b3f7e3625d91177d26"
|
||||
"hash": "a903d88370faa52169ffd4ec6a54a789ee4a6173fe84aca0ef8dedaa46b1f93c"
|
||||
}
|
||||
2
backend/migrations/20260215190000_instance_theme.sql
Normal file
2
backend/migrations/20260215190000_instance_theme.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE instance_settings
|
||||
ADD COLUMN IF NOT EXISTS theme_id VARCHAR(50) NOT NULL DEFAULT 'neutral';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE instance_settings
|
||||
SET theme_id = 'breeze-dark'
|
||||
WHERE theme_id = 'neutral';
|
||||
|
|
@ -40,7 +40,7 @@ async fn get_demo_status(State(state): State<DemoState>) -> impl IntoResponse {
|
|||
vec![
|
||||
"Cannot delete communities",
|
||||
"Cannot delete users",
|
||||
"Cannot modify instance settings",
|
||||
"Cannot modify instance settings (except instance theme)",
|
||||
"Data resets periodically"
|
||||
]
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ pub struct SetupStatus {
|
|||
pub struct PublicInstanceSettings {
|
||||
pub setup_completed: bool,
|
||||
pub instance_name: String,
|
||||
pub theme_id: String,
|
||||
pub platform_mode: String,
|
||||
pub registration_enabled: bool,
|
||||
pub registration_mode: String,
|
||||
|
|
@ -42,6 +43,7 @@ pub struct InstanceSettings {
|
|||
pub id: Uuid,
|
||||
pub setup_completed: bool,
|
||||
pub instance_name: String,
|
||||
pub theme_id: String,
|
||||
pub platform_mode: String,
|
||||
pub registration_enabled: bool,
|
||||
pub registration_mode: String,
|
||||
|
|
@ -64,6 +66,8 @@ pub struct UpdateInstanceRequest {
|
|||
#[serde(default)]
|
||||
pub instance_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub theme_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub platform_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub registration_enabled: Option<bool>,
|
||||
|
|
@ -71,6 +75,32 @@ pub struct UpdateInstanceRequest {
|
|||
pub registration_mode: Option<String>,
|
||||
}
|
||||
|
||||
const KNOWN_THEME_IDS: [&str; 4] = ["neutral", "breeze-light", "breeze-dark", "opensuse"];
|
||||
|
||||
fn validate_theme_id(theme_id: &str) -> Result<(), (StatusCode, String)> {
|
||||
if theme_id.trim().is_empty() {
|
||||
return Err((StatusCode::BAD_REQUEST, "Theme cannot be empty".to_string()));
|
||||
}
|
||||
if theme_id.len() > 50 {
|
||||
return Err((StatusCode::BAD_REQUEST, "Theme is too long".to_string()));
|
||||
}
|
||||
if !theme_id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||
{
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Theme contains invalid characters".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !KNOWN_THEME_IDS.iter().any(|t| t == &theme_id) {
|
||||
return Err((StatusCode::BAD_REQUEST, "Unknown theme".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CommunitySettings {
|
||||
pub community_id: Uuid,
|
||||
|
|
@ -121,7 +151,7 @@ async fn get_public_settings(
|
|||
State(pool): State<PgPool>,
|
||||
) -> Result<Json<PublicInstanceSettings>, String> {
|
||||
let row = sqlx::query!(
|
||||
r#"SELECT setup_completed, instance_name, platform_mode,
|
||||
r#"SELECT setup_completed, instance_name, theme_id, platform_mode,
|
||||
registration_enabled, registration_mode,
|
||||
single_community_id
|
||||
FROM instance_settings
|
||||
|
|
@ -135,6 +165,7 @@ async fn get_public_settings(
|
|||
return Ok(Json(PublicInstanceSettings {
|
||||
setup_completed: false,
|
||||
instance_name: "Likwid".to_string(),
|
||||
theme_id: "neutral".to_string(),
|
||||
platform_mode: "open".to_string(),
|
||||
registration_enabled: true,
|
||||
registration_mode: "open".to_string(),
|
||||
|
|
@ -161,6 +192,7 @@ async fn get_public_settings(
|
|||
Ok(Json(PublicInstanceSettings {
|
||||
setup_completed: r.setup_completed,
|
||||
instance_name: r.instance_name,
|
||||
theme_id: r.theme_id,
|
||||
platform_mode: r.platform_mode,
|
||||
registration_enabled: r.registration_enabled,
|
||||
registration_mode: r.registration_mode,
|
||||
|
|
@ -224,6 +256,7 @@ async fn complete_setup(
|
|||
platform_mode = $3,
|
||||
single_community_id = $4
|
||||
RETURNING id, setup_completed, instance_name, platform_mode,
|
||||
theme_id,
|
||||
registration_enabled, registration_mode,
|
||||
default_community_visibility, allow_private_communities,
|
||||
default_plugin_policy, default_moderation_mode"#,
|
||||
|
|
@ -240,6 +273,7 @@ async fn complete_setup(
|
|||
id: settings.id,
|
||||
setup_completed: settings.setup_completed,
|
||||
instance_name: settings.instance_name,
|
||||
theme_id: settings.theme_id,
|
||||
platform_mode: settings.platform_mode,
|
||||
registration_enabled: settings.registration_enabled,
|
||||
registration_mode: settings.registration_mode,
|
||||
|
|
@ -260,7 +294,7 @@ async fn get_instance_settings(
|
|||
|
||||
let s = sqlx::query!(
|
||||
r#"SELECT id, setup_completed, instance_name, platform_mode,
|
||||
registration_enabled, registration_mode,
|
||||
theme_id, registration_enabled, registration_mode,
|
||||
default_community_visibility, allow_private_communities,
|
||||
default_plugin_policy, default_moderation_mode
|
||||
FROM instance_settings LIMIT 1"#
|
||||
|
|
@ -273,6 +307,7 @@ async fn get_instance_settings(
|
|||
id: s.id,
|
||||
setup_completed: s.setup_completed,
|
||||
instance_name: s.instance_name,
|
||||
theme_id: s.theme_id,
|
||||
platform_mode: s.platform_mode,
|
||||
registration_enabled: s.registration_enabled,
|
||||
registration_mode: s.registration_mode,
|
||||
|
|
@ -293,24 +328,37 @@ async fn update_instance_settings(
|
|||
// Check platform settings permission
|
||||
require_permission(&pool, auth.user_id, perms::PLATFORM_SETTINGS, None).await?;
|
||||
|
||||
if let Some(theme_id) = req.theme_id.as_deref() {
|
||||
validate_theme_id(theme_id)?;
|
||||
}
|
||||
|
||||
if config.is_demo() {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
"Instance settings cannot be modified in demo mode".to_string(),
|
||||
));
|
||||
let allowed = req.theme_id.is_some()
|
||||
&& req.instance_name.is_none()
|
||||
&& req.platform_mode.is_none()
|
||||
&& req.registration_enabled.is_none()
|
||||
&& req.registration_mode.is_none();
|
||||
if !allowed {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
"Only theme updates are allowed in demo mode".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let s = sqlx::query!(
|
||||
r#"UPDATE instance_settings SET
|
||||
instance_name = COALESCE($1, instance_name),
|
||||
platform_mode = COALESCE($2, platform_mode),
|
||||
registration_enabled = COALESCE($3, registration_enabled),
|
||||
registration_mode = COALESCE($4, registration_mode)
|
||||
theme_id = COALESCE($2, theme_id),
|
||||
platform_mode = COALESCE($3, platform_mode),
|
||||
registration_enabled = COALESCE($4, registration_enabled),
|
||||
registration_mode = COALESCE($5, registration_mode)
|
||||
RETURNING id, setup_completed, instance_name, platform_mode,
|
||||
registration_enabled, registration_mode,
|
||||
theme_id, registration_enabled, registration_mode,
|
||||
default_community_visibility, allow_private_communities,
|
||||
default_plugin_policy, default_moderation_mode"#,
|
||||
req.instance_name,
|
||||
req.theme_id,
|
||||
req.platform_mode,
|
||||
req.registration_enabled,
|
||||
req.registration_mode
|
||||
|
|
@ -323,6 +371,7 @@ async fn update_instance_settings(
|
|||
id: s.id,
|
||||
setup_completed: s.setup_completed,
|
||||
instance_name: s.instance_name,
|
||||
theme_id: s.theme_id,
|
||||
platform_mode: s.platform_mode,
|
||||
registration_enabled: s.registration_enabled,
|
||||
registration_mode: s.registration_mode,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Demo deployment - includes demo users, seed data, and restricted actions
|
||||
# Usage: podman-compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
# Reset: podman-compose --env-file compose/.env.demo -f compose/demo.yml down -v; podman-compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
# Usage: podman compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
# Reset: podman compose --env-file compose/.env.demo -f compose/demo.yml down -v; podman compose --env-file compose/.env.demo -f compose/demo.yml up -d
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
|
|
|||
|
|
@ -1,61 +1,61 @@
|
|||
# Production deployment - clean instance without demo data
|
||||
# Usage: podman-compose -f compose/production.yml up -d
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: likwid-prod-db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-likwid}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-likwid_prod}
|
||||
volumes:
|
||||
- likwid_prod_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-likwid}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
INCLUDE_DEMO_SEED: "false"
|
||||
container_name: likwid-prod-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${BACKEND_PORT:-3000}:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://${POSTGRES_USER:-likwid}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-likwid_prod}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SERVER_HOST: 0.0.0.0
|
||||
SERVER_PORT: 3000
|
||||
DEMO_MODE: "false"
|
||||
RUST_LOG: info
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
API_BASE: ${API_BASE:-http://localhost:3000}
|
||||
container_name: likwid-prod-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-4321}:4321"
|
||||
environment:
|
||||
INTERNAL_API_BASE: http://backend:3000
|
||||
API_BASE: ${API_BASE:-http://localhost:3000}
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
likwid_prod_data:
|
||||
# Production deployment - clean instance without demo data
|
||||
# Usage: podman compose -f compose/production.yml up -d
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: likwid-prod-db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-likwid}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-likwid_prod}
|
||||
volumes:
|
||||
- likwid_prod_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-likwid}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
INCLUDE_DEMO_SEED: "false"
|
||||
container_name: likwid-prod-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${BACKEND_PORT:-3000}:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://${POSTGRES_USER:-likwid}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-likwid_prod}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SERVER_HOST: 0.0.0.0
|
||||
SERVER_PORT: 3000
|
||||
DEMO_MODE: "false"
|
||||
RUST_LOG: info
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
API_BASE: ${API_BASE:-http://localhost:3000}
|
||||
container_name: likwid-prod-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-4321}:4321"
|
||||
environment:
|
||||
INTERNAL_API_BASE: http://backend:3000
|
||||
API_BASE: ${API_BASE:-http://localhost:3000}
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
likwid_prod_data:
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ find "$BACKUP_DIR" -name "*.dump.gz" -mtime +$RETENTION_DAYS -delete
|
|||
### Containerized Backup
|
||||
|
||||
```bash
|
||||
# If using podman-compose
|
||||
# If using podman compose
|
||||
podman exec likwid-prod-db pg_dump -U likwid likwid_prod > backup.sql
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ Managed via the Admin panel or API:
|
|||
- **Instance Name** - Display name for your Likwid instance
|
||||
- **Instance Description** - Brief description
|
||||
- **Registration** - Open, invite-only, or closed
|
||||
- **Email Verification** - Required or optional
|
||||
- **Approval workflows** - Registration and community creation can be open, invite-only, or require admin approval
|
||||
|
||||
### Features
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ Required settings:
|
|||
|
||||
```bash
|
||||
cd compose
|
||||
podman-compose --env-file .env.production -f production.yml up -d
|
||||
podman compose --env-file .env.production -f production.yml up -d
|
||||
```
|
||||
|
||||
### 4. Access
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Likwid is a modular governance platform for distributed organizations. This guid
|
|||
- Email address
|
||||
- Display name (shown to others)
|
||||
- Password
|
||||
4. Verify your email if required by the instance
|
||||
4. If registration requires approval, your account will be pending until an admin approves it
|
||||
|
||||
## Exploring Without an Account
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ interface Props {
|
|||
}
|
||||
|
||||
import { DEFAULT_THEME, themes as themeRegistry } from '../lib/themes';
|
||||
import { API_BASE as apiBase } from '../lib/api';
|
||||
import { API_BASE as apiBase, SERVER_API_BASE } from '../lib/api';
|
||||
import VotingIcons from '../components/icons/VotingIcons.astro';
|
||||
import DesignSystemStyles from '../components/ui/DesignSystemStyles.astro';
|
||||
|
||||
|
|
@ -22,11 +22,24 @@ const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_S
|
|||
Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]),
|
||||
);
|
||||
|
||||
const defaultTheme = DEFAULT_THEME;
|
||||
const settingsApiBase = SERVER_API_BASE || 'http://127.0.0.1:3000';
|
||||
|
||||
let defaultTheme = DEFAULT_THEME;
|
||||
try {
|
||||
const res = await fetch(`${settingsApiBase}/api/settings/public`);
|
||||
if (res.ok) {
|
||||
const settings = await res.json();
|
||||
if (settings && typeof settings.theme_id === 'string' && themeRegistry[settings.theme_id]) {
|
||||
defaultTheme = settings.theme_id;
|
||||
}
|
||||
}
|
||||
} catch (_e) {}
|
||||
|
||||
const initialTheme = themeRegistry[defaultTheme] || themeRegistry[DEFAULT_THEME];
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="neutral" data-theme-mode="dark">
|
||||
<html lang="en" data-theme={defaultTheme} data-theme-mode={initialTheme.isDark ? 'dark' : 'light'}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
|
@ -36,9 +49,9 @@ const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_S
|
|||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>{title} | Likwid</title>
|
||||
<script is:inline define:vars={{ themes, defaultTheme }}>
|
||||
<script is:inline define:vars={{ themes, defaultTheme, publicDemoSite }}>
|
||||
(function() {
|
||||
const saved = localStorage.getItem('likwid-theme') || defaultTheme;
|
||||
const saved = publicDemoSite ? defaultTheme : (localStorage.getItem('likwid-theme') || defaultTheme);
|
||||
const theme = themes[saved] || themes[defaultTheme];
|
||||
const root = document.documentElement;
|
||||
const c = theme.colors;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ interface Props {
|
|||
}
|
||||
|
||||
import { DEFAULT_THEME, themes as themeRegistry } from '../lib/themes';
|
||||
import { SERVER_API_BASE } from '../lib/api';
|
||||
import DesignSystemStyles from '../components/ui/DesignSystemStyles.astro';
|
||||
|
||||
function isEnabled(v: string | undefined): boolean {
|
||||
|
|
@ -21,11 +22,24 @@ const themes = Object.fromEntries(
|
|||
Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]),
|
||||
);
|
||||
|
||||
const defaultTheme = DEFAULT_THEME;
|
||||
const settingsApiBase = SERVER_API_BASE || 'http://127.0.0.1:3000';
|
||||
|
||||
let defaultTheme = DEFAULT_THEME;
|
||||
try {
|
||||
const res = await fetch(`${settingsApiBase}/api/settings/public`);
|
||||
if (res.ok) {
|
||||
const settings = await res.json();
|
||||
if (settings && typeof settings.theme_id === 'string' && themeRegistry[settings.theme_id]) {
|
||||
defaultTheme = settings.theme_id;
|
||||
}
|
||||
}
|
||||
} catch (_e) {}
|
||||
|
||||
const initialTheme = themeRegistry[defaultTheme] || themeRegistry[DEFAULT_THEME];
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="neutral" data-theme-mode="dark">
|
||||
<html lang="en" data-theme={defaultTheme} data-theme-mode={initialTheme.isDark ? 'dark' : 'light'}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
|
@ -35,9 +49,9 @@ const defaultTheme = DEFAULT_THEME;
|
|||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>{title} | Likwid</title>
|
||||
<script is:inline define:vars={{ themes, defaultTheme }}>
|
||||
<script is:inline define:vars={{ themes, defaultTheme, publicDemoSite }}>
|
||||
(function() {
|
||||
const saved = localStorage.getItem('likwid-theme') || defaultTheme;
|
||||
const saved = publicDemoSite ? defaultTheme : (localStorage.getItem('likwid-theme') || defaultTheme);
|
||||
const theme = themes[saved] || themes[defaultTheme];
|
||||
const root = document.documentElement;
|
||||
const c = theme.colors;
|
||||
|
|
@ -103,11 +117,6 @@ const defaultTheme = DEFAULT_THEME;
|
|||
<a href="/docs">Documentation</a>
|
||||
</div>
|
||||
<div class="nav-actions">
|
||||
<select id="theme-select" class="theme-select" aria-label="Theme">
|
||||
{Object.values(themeRegistry).map((t) => (
|
||||
<option value={t.id}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<a href="/demo" class="ui-btn ui-btn-primary">Explore Demo</a>
|
||||
{!publicDemoSite ? <a href="/login" class="ui-btn ui-btn-secondary">Sign In</a> : null}
|
||||
</div>
|
||||
|
|
@ -154,10 +163,9 @@ const defaultTheme = DEFAULT_THEME;
|
|||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script is:inline define:vars={{ themes, defaultTheme }}>
|
||||
<script is:inline define:vars={{ themes, defaultTheme, publicDemoSite }}>
|
||||
const nav = document.getElementById('public-nav');
|
||||
const toggle = document.getElementById('public-nav-toggle');
|
||||
const themeSelect = document.getElementById('theme-select');
|
||||
|
||||
function openNav() {
|
||||
if (!nav || !toggle) return;
|
||||
|
|
@ -239,58 +247,6 @@ const defaultTheme = DEFAULT_THEME;
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applySelectedTheme() {
|
||||
if (!(themeSelect instanceof HTMLSelectElement)) return;
|
||||
const selected = themeSelect.value || defaultTheme;
|
||||
const theme = themes[selected] || themes[defaultTheme];
|
||||
const root = document.documentElement;
|
||||
const c = theme.colors;
|
||||
root.style.setProperty('--color-bg', c.bg);
|
||||
root.style.setProperty('--color-bg-alt', c.bgAlt);
|
||||
root.style.setProperty('--color-surface', c.surface);
|
||||
root.style.setProperty('--color-surface-hover', c.surfaceHover);
|
||||
root.style.setProperty('--color-border', c.border);
|
||||
root.style.setProperty('--color-border-hover', c.borderHover);
|
||||
root.style.setProperty('--color-text', c.text);
|
||||
root.style.setProperty('--color-text-muted', c.textMuted);
|
||||
root.style.setProperty('--color-text-inverse', c.textInverse);
|
||||
root.style.setProperty('--color-primary', c.primary);
|
||||
root.style.setProperty('--color-primary-hover', c.primaryHover);
|
||||
root.style.setProperty('--color-primary-muted', c.primaryMuted);
|
||||
root.style.setProperty('--color-secondary', c.secondary);
|
||||
root.style.setProperty('--color-secondary-hover', c.secondaryHover);
|
||||
root.style.setProperty('--color-info', c.info);
|
||||
root.style.setProperty('--color-info-hover', c.infoHover);
|
||||
root.style.setProperty('--color-info-muted', c.infoMuted);
|
||||
root.style.setProperty('--color-neutral', c.neutral);
|
||||
root.style.setProperty('--color-neutral-hover', c.neutralHover);
|
||||
root.style.setProperty('--color-neutral-muted', c.neutralMuted);
|
||||
root.style.setProperty('--color-success', c.success);
|
||||
root.style.setProperty('--color-success-muted', c.successMuted);
|
||||
root.style.setProperty('--color-success-hover', c.successHover);
|
||||
root.style.setProperty('--color-warning', c.warning);
|
||||
root.style.setProperty('--color-warning-muted', c.warningMuted);
|
||||
root.style.setProperty('--color-error', c.error);
|
||||
root.style.setProperty('--color-error-muted', c.errorMuted);
|
||||
root.style.setProperty('--color-error-hover', c.errorHover);
|
||||
root.style.setProperty('--color-link', c.link);
|
||||
root.style.setProperty('--color-link-visited', c.linkVisited);
|
||||
root.style.setProperty('--color-overlay', c.overlay);
|
||||
root.style.setProperty('--color-field-bg', c.fieldBg);
|
||||
root.style.setProperty('--color-on-primary', c.onPrimary);
|
||||
root.setAttribute('data-theme', selected);
|
||||
root.setAttribute('data-theme-mode', theme.isDark ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
if (themeSelect instanceof HTMLSelectElement) {
|
||||
const saved = localStorage.getItem('likwid-theme') || defaultTheme;
|
||||
themeSelect.value = themes[saved] ? saved : defaultTheme;
|
||||
themeSelect.addEventListener('change', () => {
|
||||
localStorage.setItem('likwid-theme', themeSelect.value);
|
||||
applySelectedTheme();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -449,20 +405,6 @@ const defaultTheme = DEFAULT_THEME;
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.theme-select {
|
||||
width: auto;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.theme-select:hover {
|
||||
border-color: var(--color-border-hover);
|
||||
}
|
||||
|
||||
.public-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,16 @@ import { API_BASE as apiBase } from '../../lib/api';
|
|||
<label for="instance_name">Platform Name</label>
|
||||
<input type="text" id="instance_name" name="instance_name" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="theme_id">Theme</label>
|
||||
<select id="theme_id" name="theme_id">
|
||||
<option value="neutral">Neutral Dark</option>
|
||||
<option value="breeze-light">Breeze Light</option>
|
||||
<option value="breeze-dark">Breeze Dark</option>
|
||||
<option value="opensuse">openSUSE</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Platform Mode -->
|
||||
|
|
@ -182,6 +192,8 @@ import { API_BASE as apiBase } from '../../lib/api';
|
|||
const saveBtn = document.getElementById('save-btn');
|
||||
const saveStatus = document.getElementById('save-status');
|
||||
|
||||
let initialSettings = null;
|
||||
|
||||
if (!form || !loadingEl || !errorEl || !saveStatus) return;
|
||||
|
||||
async function loadSettings() {
|
||||
|
|
@ -213,9 +225,11 @@ import { API_BASE as apiBase } from '../../lib/api';
|
|||
}
|
||||
|
||||
const settings = await res.json();
|
||||
initialSettings = settings;
|
||||
|
||||
// Populate form
|
||||
(document.getElementById('instance_name')).value = settings.instance_name;
|
||||
(document.getElementById('theme_id')).value = settings.theme_id || 'neutral';
|
||||
(document.getElementById('platform_mode')).value = settings.platform_mode;
|
||||
(document.getElementById('registration_enabled')).checked = settings.registration_enabled;
|
||||
(document.getElementById('registration_mode')).value = settings.registration_mode;
|
||||
|
|
@ -240,14 +254,39 @@ import { API_BASE as apiBase } from '../../lib/api';
|
|||
|
||||
if (saveBtn) saveBtn.disabled = true;
|
||||
saveStatus.textContent = 'Saving...';
|
||||
saveStatus.style.color = '';
|
||||
|
||||
const data = {
|
||||
const current = {
|
||||
instance_name: (document.getElementById('instance_name')).value,
|
||||
theme_id: (document.getElementById('theme_id')).value,
|
||||
platform_mode: (document.getElementById('platform_mode')).value,
|
||||
registration_enabled: (document.getElementById('registration_enabled')).checked,
|
||||
registration_mode: (document.getElementById('registration_mode')).value
|
||||
registration_mode: (document.getElementById('registration_mode')).value,
|
||||
};
|
||||
|
||||
const data = {};
|
||||
if (!initialSettings || current.instance_name !== initialSettings.instance_name) {
|
||||
data.instance_name = current.instance_name;
|
||||
}
|
||||
if (!initialSettings || current.theme_id !== initialSettings.theme_id) {
|
||||
data.theme_id = current.theme_id;
|
||||
}
|
||||
if (!initialSettings || current.platform_mode !== initialSettings.platform_mode) {
|
||||
data.platform_mode = current.platform_mode;
|
||||
}
|
||||
if (!initialSettings || current.registration_enabled !== initialSettings.registration_enabled) {
|
||||
data.registration_enabled = current.registration_enabled;
|
||||
}
|
||||
if (!initialSettings || current.registration_mode !== initialSettings.registration_mode) {
|
||||
data.registration_mode = current.registration_mode;
|
||||
}
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
saveStatus.textContent = 'No changes to save.';
|
||||
if (saveBtn) saveBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/settings/instance`, {
|
||||
method: 'PATCH',
|
||||
|
|
@ -262,8 +301,17 @@ import { API_BASE as apiBase } from '../../lib/api';
|
|||
throw new Error(await res.text());
|
||||
}
|
||||
|
||||
const updated = await res.json();
|
||||
initialSettings = updated;
|
||||
|
||||
saveStatus.textContent = 'Saved!';
|
||||
setTimeout(() => { saveStatus.textContent = ''; }, 3000);
|
||||
|
||||
if (data.theme_id) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 150);
|
||||
}
|
||||
} catch (err) {
|
||||
saveStatus.textContent = 'Error: ' + err.message;
|
||||
saveStatus.style.color = '#c62828';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import { API_BASE as apiBase } from '../lib/api';
|
||||
|
||||
function isEnabled(v: string | undefined): boolean {
|
||||
if (!v) return false;
|
||||
const n = v.trim().toLowerCase();
|
||||
return n === '1' || n === 'true' || n === 'yes' || n === 'on';
|
||||
}
|
||||
|
||||
const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
|
||||
---
|
||||
|
||||
<Layout title="Settings">
|
||||
|
|
@ -13,7 +21,7 @@ import { API_BASE as apiBase } from '../lib/api';
|
|||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ apiBase }}>
|
||||
<script define:vars={{ apiBase, publicDemoSite }}>
|
||||
import { getAllThemes, loadSavedTheme, saveTheme } from '../lib/themes';
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
|
|
@ -42,36 +50,50 @@ import { API_BASE as apiBase } from '../lib/api';
|
|||
|
||||
const user = await res.json();
|
||||
|
||||
const currentTheme = loadSavedTheme();
|
||||
const themeOptions = getAllThemes()
|
||||
.map((t) => {
|
||||
const selected = t.id === currentTheme ? 'selected' : '';
|
||||
return `<option value="${t.id}" ${selected}>${t.name}</option>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="form-section ui-card ui-card-pad-lg ui-form">
|
||||
<h2>Appearance</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="theme-select">Theme</label>
|
||||
<select id="theme-select" class="theme-select">
|
||||
${themeOptions}
|
||||
</select>
|
||||
<p class="hint">Choose a visual theme for the interface</p>
|
||||
let appearanceHtml = '';
|
||||
if (publicDemoSite) {
|
||||
appearanceHtml = `
|
||||
<div class="form-section ui-card ui-card-pad-lg ui-form">
|
||||
<h2>Appearance</h2>
|
||||
<p class="hint">On the public demo, appearance is managed by the instance administrator.</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
const currentTheme = loadSavedTheme();
|
||||
const themeOptions = getAllThemes()
|
||||
.map((t) => {
|
||||
const selected = t.id === currentTheme ? 'selected' : '';
|
||||
return `<option value="${t.id}" ${selected}>${t.name}</option>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
<div id="theme-preview" class="theme-preview">
|
||||
<div class="preview-colors">
|
||||
<span class="preview-swatch preview-bg" title="Background"></span>
|
||||
<span class="preview-swatch preview-surface" title="Surface"></span>
|
||||
<span class="preview-swatch preview-primary" title="Primary"></span>
|
||||
<span class="preview-swatch preview-success" title="Success"></span>
|
||||
<span class="preview-swatch preview-error" title="Error"></span>
|
||||
appearanceHtml = `
|
||||
<div class="form-section ui-card ui-card-pad-lg ui-form">
|
||||
<h2>Appearance</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="theme-select">Theme</label>
|
||||
<select id="theme-select" class="theme-select">
|
||||
${themeOptions}
|
||||
</select>
|
||||
<p class="hint">Choose a visual theme for the interface</p>
|
||||
</div>
|
||||
|
||||
<div id="theme-preview" class="theme-preview">
|
||||
<div class="preview-colors">
|
||||
<span class="preview-swatch preview-bg" title="Background"></span>
|
||||
<span class="preview-swatch preview-surface" title="Surface"></span>
|
||||
<span class="preview-swatch preview-primary" title="Primary"></span>
|
||||
<span class="preview-swatch preview-success" title="Success"></span>
|
||||
<span class="preview-swatch preview-error" title="Error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
${appearanceHtml}
|
||||
|
||||
<form id="profile-form" class="settings-form">
|
||||
<div class="form-section ui-card ui-card-pad-lg ui-form">
|
||||
|
|
@ -99,7 +121,9 @@ import { API_BASE as apiBase } from '../lib/api';
|
|||
</div>
|
||||
`;
|
||||
|
||||
setupThemeSwitcher();
|
||||
if (!publicDemoSite) {
|
||||
setupThemeSwitcher();
|
||||
}
|
||||
|
||||
document.getElementById('profile-form')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -148,6 +172,8 @@ import { API_BASE as apiBase } from '../lib/api';
|
|||
}
|
||||
|
||||
function setupThemeSwitcher() {
|
||||
if (publicDemoSite) return;
|
||||
|
||||
function updatePreview() {
|
||||
const previewBg = document.querySelector('.preview-bg');
|
||||
const previewSurface = document.querySelector('.preview-surface');
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ if (Test-Path $envFile) {
|
|||
}
|
||||
|
||||
Write-Host "`n[1/3] Stopping demo containers and removing volumes..." -ForegroundColor Yellow
|
||||
podman-compose @composeArgs down --remove-orphans -v
|
||||
podman compose @composeArgs down --remove-orphans -v
|
||||
|
||||
Write-Host "`n[2/3] Starting fresh demo instance..." -ForegroundColor Yellow
|
||||
podman-compose @composeArgs up -d
|
||||
podman compose @composeArgs up -d
|
||||
|
||||
Write-Host "`n[3/3] Waiting for services to be ready..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 10
|
||||
|
|
@ -63,4 +63,4 @@ while ($retry -lt $maxRetries) {
|
|||
|
||||
Write-Host "`nWarning: Backend health check timed out. Check logs with:" -ForegroundColor Yellow
|
||||
$composeArgsText = ($composeArgs -join ' ')
|
||||
Write-Host " podman-compose $composeArgsText logs backend"
|
||||
Write-Host " podman compose $composeArgsText logs backend"
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ if [ "$1" != "--force" ] && [ "$1" != "-f" ]; then
|
|||
fi
|
||||
|
||||
echo -e "\n[1/3] Stopping demo containers and removing volumes..."
|
||||
podman-compose "${COMPOSE_ARGS[@]}" down --remove-orphans -v || docker-compose "${COMPOSE_ARGS[@]}" down --remove-orphans -v
|
||||
podman compose "${COMPOSE_ARGS[@]}" down --remove-orphans -v || podman-compose "${COMPOSE_ARGS[@]}" down --remove-orphans -v || docker-compose "${COMPOSE_ARGS[@]}" down --remove-orphans -v
|
||||
|
||||
echo -e "\n[2/3] Starting fresh demo instance..."
|
||||
podman-compose "${COMPOSE_ARGS[@]}" up -d || docker-compose "${COMPOSE_ARGS[@]}" up -d
|
||||
podman compose "${COMPOSE_ARGS[@]}" up -d || podman-compose "${COMPOSE_ARGS[@]}" up -d || docker-compose "${COMPOSE_ARGS[@]}" up -d
|
||||
|
||||
echo -e "\n[3/3] Waiting for services to be ready..."
|
||||
sleep 5
|
||||
|
|
@ -54,5 +54,5 @@ while [ $retry -lt $max_retries ]; do
|
|||
done
|
||||
|
||||
echo -e "\nWarning: Backend health check timed out. Check logs with:"
|
||||
echo " podman-compose ${COMPOSE_ARGS[*]} logs backend"
|
||||
echo " podman compose ${COMPOSE_ARGS[*]} logs backend"
|
||||
echo " docker-compose ${COMPOSE_ARGS[*]} logs backend"
|
||||
|
|
|
|||
|
|
@ -27,19 +27,19 @@ function Invoke-Compose {
|
|||
[Parameter(Mandatory=$true)][string[]]$Args
|
||||
)
|
||||
|
||||
$podmanCompose = Get-Command podman-compose -ErrorAction SilentlyContinue
|
||||
if ($podmanCompose) {
|
||||
$null = & podman-compose @Args
|
||||
return $LASTEXITCODE
|
||||
}
|
||||
|
||||
$podman = Get-Command podman -ErrorAction SilentlyContinue
|
||||
if ($podman) {
|
||||
$null = & podman compose @Args
|
||||
return $LASTEXITCODE
|
||||
}
|
||||
|
||||
throw 'Neither podman-compose nor podman was found in PATH.'
|
||||
$podmanCompose = Get-Command podman-compose -ErrorAction SilentlyContinue
|
||||
if ($podmanCompose) {
|
||||
$null = & podman-compose @Args
|
||||
return $LASTEXITCODE
|
||||
}
|
||||
|
||||
throw 'Neither podman nor podman-compose was found in PATH.'
|
||||
}
|
||||
|
||||
function Get-CommandLine {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fi
|
|||
|
||||
# Start PostgreSQL
|
||||
echo "Starting PostgreSQL..."
|
||||
podman-compose -f "$ROOT_DIR/compose/dev.yml" up -d 2>/dev/null || true
|
||||
podman compose -f "$ROOT_DIR/compose/dev.yml" up -d 2>/dev/null || podman-compose -f "$ROOT_DIR/compose/dev.yml" up -d 2>/dev/null || true
|
||||
|
||||
# Wait for PostgreSQL
|
||||
echo "Waiting for PostgreSQL..."
|
||||
|
|
|
|||
|
|
@ -19,19 +19,19 @@ function Invoke-Compose {
|
|||
[Parameter(Mandatory=$true)][string[]]$Args
|
||||
)
|
||||
|
||||
$podmanCompose = Get-Command podman-compose -ErrorAction SilentlyContinue
|
||||
if ($podmanCompose) {
|
||||
$null = & podman-compose @Args
|
||||
return $LASTEXITCODE
|
||||
}
|
||||
|
||||
$podman = Get-Command podman -ErrorAction SilentlyContinue
|
||||
if ($podman) {
|
||||
$null = & podman compose @Args
|
||||
return $LASTEXITCODE
|
||||
}
|
||||
|
||||
throw 'Neither podman-compose nor podman was found in PATH.'
|
||||
$podmanCompose = Get-Command podman-compose -ErrorAction SilentlyContinue
|
||||
if ($podmanCompose) {
|
||||
$null = & podman-compose @Args
|
||||
return $LASTEXITCODE
|
||||
}
|
||||
|
||||
throw 'Neither podman nor podman-compose was found in PATH.'
|
||||
}
|
||||
|
||||
function Get-CommandLine {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ pkill -f "astro dev" 2>/dev/null || true
|
|||
# Stop PostgreSQL if requested
|
||||
if [ "$STOP_DB" = "--all" ] || [ "$STOP_DB" = "-a" ]; then
|
||||
echo "Stopping PostgreSQL..."
|
||||
podman-compose -f "$ROOT_DIR/compose/dev.yml" down 2>/dev/null || true
|
||||
podman compose -f "$ROOT_DIR/compose/dev.yml" down 2>/dev/null || podman-compose -f "$ROOT_DIR/compose/dev.yml" down 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
podman-compose -f compose/dev.yml up
|
||||
podman compose -f compose/dev.yml up
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
podman-compose -f compose/dev.yml up
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
#!/bin/sh
|
||||
podman-compose -f compose/dev.yml up
|
||||
podman compose -f compose/dev.yml up || podman-compose -f compose/dev.yml up
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Write-Host "=== Likwid Post-Reboot Setup ===" -ForegroundColor Cyan
|
|||
|
||||
# Step 1: Verify WSL2 is working
|
||||
Write-Host "`n[1/4] Checking WSL2 status..." -ForegroundColor Yellow
|
||||
$wslStatus = wsl --status 2>&1
|
||||
wsl --status 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "WSL2 is not ready. Please ensure virtualization is enabled in BIOS." -ForegroundColor Red
|
||||
Write-Host "Run 'wsl --install --no-distribution' as administrator if needed." -ForegroundColor Red
|
||||
|
|
@ -35,7 +35,7 @@ wsl --set-default openSUSE-Tumbleweed
|
|||
# Step 4: Configure Podman in WSL2
|
||||
Write-Host "`n[4/4] Configuring Podman in openSUSE Tumbleweed..." -ForegroundColor Yellow
|
||||
wsl -d openSUSE-Tumbleweed -e bash -c "
|
||||
echo 'Installing Podman and podman-compose...'
|
||||
echo 'Installing Podman and compose support (use: podman compose)'
|
||||
sudo zypper refresh
|
||||
sudo zypper install -y podman podman-compose
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ wsl -d openSUSE-Tumbleweed -e bash -c "
|
|||
|
||||
echo 'Verifying installation...'
|
||||
podman --version
|
||||
podman-compose --version
|
||||
podman compose --help >/dev/null 2>&1 || true
|
||||
"
|
||||
|
||||
Write-Host "`n=== Setup Complete ===" -ForegroundColor Cyan
|
||||
|
|
|
|||
|
|
@ -28,4 +28,4 @@ if (Test-Path $demoMigration) {
|
|||
Write-Host "`n=== Production Preparation Complete ===" -ForegroundColor Green
|
||||
Write-Host "`nNext steps:"
|
||||
Write-Host " 1. Configure compose/.env.production"
|
||||
Write-Host " 2. Run: podman-compose -f compose/production.yml up -d"
|
||||
Write-Host " 2. Run: podman compose -f compose/production.yml up -d"
|
||||
|
|
|
|||
Loading…
Reference in a new issue