diff --git a/README.md b/README.md
index 4da348f..c8b55dd 100644
--- a/README.md
+++ b/README.md
@@ -146,6 +146,33 @@ python run.py
- Python 3.8 or higher
- pip (Python package manager)
+#### Using Virtual Environments (Recommended for Arch/Fedora/Ubuntu 23.04+)
+
+Modern Linux distributions enforce [PEP 668](https://peps.python.org/pep-0668/), which prevents system-wide pip installs. Use a virtual environment instead:
+
+```bash
+# Clone the repository
+git clone https://github.com/gamosoft/notediscovery.git
+cd notediscovery
+
+# Create a virtual environment
+python -m venv venv
+
+# Activate it (choose your shell):
+source venv/bin/activate # Bash/Zsh (most Linux distros)
+source venv/bin/activate.fish # Fish (CachyOS, etc.)
+source venv/bin/activate.csh # Csh/Tcsh
+.\venv\Scripts\activate # Windows PowerShell
+
+# Install dependencies and run
+pip install -r requirements.txt
+python run.py
+```
+
+> โ ๏ธ **Warning**
+>
+> *You'll need to activate the virtual environment (source venv/bin/activate) each time you open a new terminal before running the app*
+
### Advanced Docker Setup
The image includes bundled config, themes, plugins, and locales. To customize, you must:
diff --git a/backend/main.py b/backend/main.py
index 2d057e7..aaa547e 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -67,10 +67,34 @@ if 'AUTHENTICATION_ENABLED' in os.environ:
else:
print(f"๐ Authentication {'ENABLED' if config.get('authentication', {}).get('enabled', False) else 'DISABLED'} (from config.yaml)")
-# Allow password hash to be set via environment variable (useful for demos)
-if 'AUTHENTICATION_PASSWORD_HASH' in os.environ:
+# Password configuration priority:
+# 1. AUTHENTICATION_PASSWORD env var (plain text, hashed at startup)
+# 2. AUTHENTICATION_PASSWORD_HASH env var (pre-hashed)
+# 3. authentication.password in config.yaml (plain text, hashed at startup)
+# 4. authentication.password_hash in config.yaml (pre-hashed)
+# Default password is "admin" if nothing is configured
+if 'AUTHENTICATION_PASSWORD' in os.environ:
+ # Plain text password from env var - hash it
+ plain_password = os.getenv('AUTHENTICATION_PASSWORD')
+ config['authentication']['password_hash'] = bcrypt.hashpw(
+ plain_password.encode('utf-8'),
+ bcrypt.gensalt()
+ ).decode('utf-8')
+ print("๐ Password loaded from AUTHENTICATION_PASSWORD env var (hashed at startup)")
+elif 'AUTHENTICATION_PASSWORD_HASH' in os.environ:
+ # Pre-hashed password from env var
config['authentication']['password_hash'] = os.getenv('AUTHENTICATION_PASSWORD_HASH')
print("๐ Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var")
+elif config.get('authentication', {}).get('password'):
+ # Plain text password from config.yaml - hash it
+ plain_password = config['authentication']['password']
+ config['authentication']['password_hash'] = bcrypt.hashpw(
+ plain_password.encode('utf-8'),
+ bcrypt.gensalt()
+ ).decode('utf-8')
+ # Clear the plain text password from config for security
+ del config['authentication']['password']
+ print("๐ Password loaded from config.yaml (hashed at startup)")
# Allow secret key to be set via environment variable (for session security)
if 'AUTHENTICATION_SECRET_KEY' in os.environ:
diff --git a/config.yaml b/config.yaml
index 9dbd2f5..5a3653a 100644
--- a/config.yaml
+++ b/config.yaml
@@ -35,9 +35,16 @@ authentication:
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
secret_key: "change_this_to_a_random_secret_key_in_production"
- # Password hash - Generate with: python generate_password.py
+ # Password configuration - choose ONE of the following options:
+ #
+ # Option 1: Plain text password (recommended for ease of use)
+ # The password is hashed automatically at startup
# โ ๏ธ Default password is "admin" - CHANGE THIS for production!
- password_hash: "$2b$12$t/6PGExFzdpU2PUta0iVY.eDQwvu63kH.c/d4bEnnHaQ5CspH1yrG" # Default: "admin"
+ password: "admin"
+
+ # Option 2: Pre-hashed password (for advanced users)
+ # Generate with: python generate_password.py
+ # password_hash: "$2b$12$..."
# Session expiry in seconds (default: 7 days)
session_max_age: 604800
diff --git a/docs/index.html b/docs/index.html
index 16d1081..6b02042 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -353,7 +353,7 @@
animation: fadeInScale 0.6s ease-out forwards;
}
- /* Stagger animation delays for feature cards */
+ /* Stagger animation delays for feature cards (5 rows ร 3 cols = 15 cards) */
.feature.animated:nth-child(1) { animation-delay: 0s; }
.feature.animated:nth-child(2) { animation-delay: 0.1s; }
.feature.animated:nth-child(3) { animation-delay: 0.2s; }
@@ -367,6 +367,8 @@
.feature.animated:nth-child(11) { animation-delay: 0.25s; }
.feature.animated:nth-child(12) { animation-delay: 0.35s; }
.feature.animated:nth-child(13) { animation-delay: 0.2s; }
+ .feature.animated:nth-child(14) { animation-delay: 0.3s; }
+ .feature.animated:nth-child(15) { animation-delay: 0.4s; }
.screenshot-section {
margin: 4rem 0 3rem 0;
@@ -832,12 +834,6 @@
No subscriptions, no hidden fees. Free and open source forever.
-
-
-
-
+
+
+
+
@@ -946,38 +948,6 @@
-
-
๐
-
- Dark Mode
- Easy on the eyes
-
-
-
-
-
๐ป
-
- Self-Hosted
- You own your data
-
-
-
-
-
๐
-
- Wikilinks
- [[Link notes]] Obsidian-style
-
-
-
-
-
๐ธ๏ธ
-
- Graph View
- Visualize note connections
-
-
-
๐ผ๏ธ
@@ -1011,10 +981,10 @@
-
โ๏ธ
+
๐
- Properties Panel
- View frontmatter metadata at a glance
+ Wikilinks
+ [[Link notes]] Obsidian-style
diff --git a/documentation/AUTHENTICATION.md b/documentation/AUTHENTICATION.md
index 0f7adb1..7d5c6b7 100644
--- a/documentation/AUTHENTICATION.md
+++ b/documentation/AUTHENTICATION.md
@@ -1,179 +1,165 @@
# ๐ NoteDiscovery Authentication Guide
-## โ ๏ธ **IMPORTANT: Default Password Warning**
+## โ ๏ธ Default Password Warning
-> **The default `config.yaml` includes authentication disabled by default, with password: `admin`**
->
-> ๐ด **CHANGE THIS if you're exposing NoteDiscovery to a network!**
->
-> The default configuration is provided for **quick testing only**. Follow the setup guide below to set your own secure password and secret key.
+> **Default password is `admin`** โ CHANGE THIS before exposing to any network!
---
## Overview
-NoteDiscovery includes a simple, secure authentication system for single-user deployments. When enabled, users must log in with a password before accessing the application.
+NoteDiscovery includes simple password protection for single-user deployments. When enabled, users must log in before accessing notes.
-## โจ Features
-
-- โ
**Single User** - Perfect for personal/self-hosted use
-- โ
**Secure** - Passwords hashed with bcrypt
-- โ
**Session-based** - Stay logged in for 7 days (configurable)
+- โ
Single user / self-hosted use
+- โ
Passwords hashed with bcrypt
+- โ
Session-based (7 days default, configurable)
---
-## ๐ Quick Setup
+## Quick Test (Local Only)
-**Default Configuration:**
-- Authentication is **disabled by default**
-- Default password is `admin`
-- Default secret key is insecure
+For local testing, authentication is **disabled by default**. To test with auth:
-**โ ๏ธ IMPORTANT:** For production or network-exposed deployments, **change both the password and secret key immediately**.
-
----
-
-### ๐งช **Quick Test (Use Default Password)**
-
-For **local testing only**, you can use the default configuration:
-
-1. Start NoteDiscovery (Docker or locally)
-2. Navigate to `http://localhost:8000`
+1. Set `authentication.enabled: true` in `config.yaml`
+2. Restart the app
3. Log in with password: `admin`
-**โ ๏ธ Only use this for local testing on your own machine!**
+โ ๏ธ **Don't use the default password on any network!**
---
-### ๐ **Production Setup (Change Password & Secret Key)**
+## Production Setup
For any deployment exposed to a network, follow these steps:
-### Step 1: Generate a Password Hash
+### Step 1: Generate a Secret Key
-Choose your environment:
-
-**Docker Users:**
+The secret key encrypts session cookies. Generate a random one:
```bash
-# Docker Compose with the GHCR compose file
-docker-compose -f docker-compose.ghcr.yml exec notediscovery python generate_password.py
-
-# Or with docker run
-docker exec -it notediscovery python generate_password.py
-```
-
-**Local Users:**
-
-```bash
-# Install bcrypt if not already installed
-pip install bcrypt
-
-# Run the password generator
-python generate_password.py
-```
-
-The script will:
-1. Prompt you for your password (input is hidden)
-2. Ask you to confirm it
-3. Generate a bcrypt hash
-4. Display the hash with instructions
-
-**Copy the hash** - you'll need it for Step 3.
-
-### Step 2: Generate a Secret Key
-
-Generate a random secret key for session encryption:
-
-**Docker Users:**
-```bash
-docker-compose exec notediscovery python -c "import secrets; print(secrets.token_hex(32))"
-
-# Or with docker run
+# Docker
docker exec -it notediscovery python -c "import secrets; print(secrets.token_hex(32))"
-```
-**Local Users:**
-```bash
+# Local
python -c "import secrets; print(secrets.token_hex(32))"
```
-**Copy the key** - you'll need it for Step 3.
-
-### Step 3: Update `config.yaml`
-
-Edit your `config.yaml` and update the authentication section:
-
-```yaml
-authentication:
- # Enable authentication
- enabled: true
-
- # Session secret key (paste the output from Step 2)
- secret_key: "your_generated_secret_key_here"
-
- # Password hash (paste the output from Step 1)
- password_hash: "$2b$12$..."
-
- # Session expiry in seconds (7 days by default)
- session_max_age: 604800
-```
-
-### Step 4: Restart the Application
-
-```bash
-# If running locally
-uvicorn backend.main:app --reload
-
-# If using Docker Compose
-docker-compose restart
-
-# Or with docker run
-docker restart notediscovery
-```
-
-### Step 5: Test Login
-
-Navigate to `http://localhost:8000` and you'll be redirected to the login page.
-
-Enter the password you chose in Step 2.
+**Save this key** โ you'll need it in Step 2.
---
-## ๐ Security Considerations
+### Step 2: Configure Authentication
+
+Choose **one** of these options:
+
+#### Option A: Plain Text Password (Recommended)
+
+The easiest approach. Your password is automatically hashed at startup.
+
+**Via Environment Variables (Docker):**
+```bash
+docker run -d \
+ -e AUTHENTICATION_ENABLED=true \
+ -e AUTHENTICATION_PASSWORD=your_secure_password \
+ -e AUTHENTICATION_SECRET_KEY=your_generated_secret_key \
+ ...
+```
+
+**Via config.yaml:**
+```yaml
+authentication:
+ enabled: true
+ password: "your_secure_password"
+ secret_key: "your_generated_secret_key"
+```
+
+---
+
+#### Option B: Pre-Hashed Password (Advanced)
+
+For users who prefer to hash passwords themselves.
+
+**Generate a hash:**
+```bash
+# Docker
+docker exec -it notediscovery python generate_password.py
+
+# Local
+python generate_password.py
+```
+
+**Then configure:**
+```yaml
+authentication:
+ enabled: true
+ password_hash: "$2b$12$..." # paste your hash here
+ secret_key: "your_generated_secret_key"
+```
+
+---
+
+### Step 3: Restart & Test
+
+```bash
+# Docker Compose
+docker-compose restart
+
+# Docker run
+docker restart notediscovery
+
+# Local
+python run.py
+```
+
+Navigate to `http://localhost:8000` โ you'll be redirected to the login page.
+
+---
+
+## Configuration Priority
+
+If multiple sources are configured, this priority applies (first wins):
+
+| Priority | Source | Type |
+|----------|--------|------|
+| 1st | `AUTHENTICATION_PASSWORD` env var | Plain text |
+| 2nd | `AUTHENTICATION_PASSWORD_HASH` env var | Pre-hashed |
+| 3rd | `password` in config.yaml | Plain text |
+| 4th | `password_hash` in config.yaml | Pre-hashed |
+
+**Example:** If you set `AUTHENTICATION_PASSWORD` as an env var, it overrides anything in config.yaml.
+
+---
+
+## Security Considerations
### โ
What This Protects
- Unauthorized access to your notes
-- Viewing, creating, editing, and deleting notes
- All API endpoints
+- Viewing, creating, editing, deleting notes
### โ ๏ธ What This Doesn't Protect
-This is a **simple authentication system** designed for **self-hosted, single-user** deployments. It is **NOT** suitable for:
+This is a **simple single-user** system. NOT suitable for:
- โ Multi-user environments
-- โ Public internet exposure without HTTPS
-- โ Production SaaS applications
+- โ Public internet without HTTPS
- โ Compliance requirements (HIPAA, GDPR, etc.)
### ๐ก๏ธ Best Practices
-1. **Use HTTPS** - Always run behind a reverse proxy (Traefik, nginx, Caddy) with SSL/TLS
-2. **Strong Password** - Use at least 12 characters with mixed case, numbers, and symbols
-3. **Unique Secret Key** - Never reuse secret keys across applications
-4. **Keep Config Secure** - Don't commit `config.yaml` with real credentials to version control
-5. **VPN/Private Network** - Keep NoteDiscovery on a private network or behind a VPN
+1. **Use HTTPS** โ Run behind a reverse proxy (Traefik, nginx, Caddy)
+2. **Strong password** โ At least 12 characters, mixed case, numbers, symbols
+3. **Unique secret key** โ Never reuse across applications
+4. **Keep config secure** โ Don't commit credentials to version control
---
-## ๐ซ Disabling Authentication
-
-To disable authentication and allow open access:
+## Disabling Authentication
```yaml
authentication:
enabled: false
```
-Restart the application, and authentication will be bypassed.
\ No newline at end of file
+Restart the app to apply.
diff --git a/documentation/ENVIRONMENT_VARIABLES.md b/documentation/ENVIRONMENT_VARIABLES.md
index 4b32ef2..5a0c268 100644
--- a/documentation/ENVIRONMENT_VARIABLES.md
+++ b/documentation/ENVIRONMENT_VARIABLES.md
@@ -17,9 +17,22 @@ NoteDiscovery supports environment variables to override configuration settings,
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `AUTHENTICATION_ENABLED` | boolean | `config.yaml` | Enable/disable authentication |
-| `AUTHENTICATION_PASSWORD_HASH` | string | `config.yaml` | Bcrypt password hash |
+| `AUTHENTICATION_PASSWORD` | string | `admin` | Plain text password (hashed automatically at startup) |
+| `AUTHENTICATION_PASSWORD_HASH` | string | - | Pre-hashed bcrypt password (for advanced users) |
| `AUTHENTICATION_SECRET_KEY` | string | `config.yaml` | Session secret key (for session security) |
+> **Password Priority:** `AUTHENTICATION_PASSWORD` takes precedence over `AUTHENTICATION_PASSWORD_HASH`. If both are set, the plain text password is used.
+
+#### Example: Setting password via environment variable
+
+```bash
+# Docker
+docker run -e AUTHENTICATION_ENABLED=true -e AUTHENTICATION_PASSWORD=mysecretpassword ...
+
+# Docker Compose (in .env file or docker-compose.yml)
+AUTHENTICATION_PASSWORD=mysecretpassword
+```
+
### Demo Mode
| Variable | Type | Default | Description |
diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md
index 2f1a20a..1a8287d 100644
--- a/documentation/FEATURES.md
+++ b/documentation/FEATURES.md
@@ -318,12 +318,32 @@ Full immersive distraction-free writing experience:
- **Easy exit** - Press `Esc`, click exit button, or use shortcut again
- **State preserved** - Returns to your previous view mode on exit
+## ๐ฑ Progressive Web App (PWA)
+
+NoteDiscovery can be installed as a standalone app on your device:
+
+- **Install as app** - Add to home screen on mobile, or install via browser on desktop
+- **Standalone mode** - Runs without browser chrome for a native app feel
+
+### How to Install
+- **Desktop (Chrome/Edge)**: Click the install icon in the address bar, or Menu โ "Install NoteDiscovery"
+- **Android**: Chrome Menu โ "Add to Home Screen"
+- **iOS**: Safari Share โ "Add to Home Screen"
+
+## ๐ Internationalization
+
+- **Multiple languages** - English, Spanish, German, French built-in
+- **Easy to add** - Drop JSON files in `locales/` folder
+- **Instant switch** - Change language in Settings without reload
+- **Community translations** - Contributions welcome!
+
## ๐ Performance
- **Instant loading** - No lag, no loading spinners
- **Efficient caching** - Smart local storage
- **Minimal resources** - Runs on modest hardware
- **No bloat** - Focused on what matters
+- **Lightweight** - No heavy frameworks
---
diff --git a/frontend/app.js b/frontend/app.js
index 8595b8b..83f482d 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -800,9 +800,17 @@ function noteApp() {
// Links [text](url)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1]($2)');
- // Lists
- html = html.replace(/^(\s*)([-*+])\s/gm, '$1$2 ');
- html = html.replace(/^(\s*)(\d+\.)\s/gm, '$1$2 ');
+ // Lists - use [ \t] instead of \s to avoid matching newlines
+ // Capture rest of line to handle empty bullets properly (issue #142)
+ html = html.replace(/^(\s*)([-*+])[ \t](.*)$/gm, (match, indent, bullet, rest) => {
+ // If rest is empty/whitespace-only, add zero-width space to prevent line collapse
+ const content = rest.trim() === '' ? '\u200B' : rest;
+ return `${indent}${bullet} ${content}`;
+ });
+ html = html.replace(/^(\s*)(\d+\.)[ \t](.*)$/gm, (match, indent, bullet, rest) => {
+ const content = rest.trim() === '' ? '\u200B' : rest;
+ return `${indent}${bullet} ${content}`;
+ });
// Blockquotes
html = html.replace(/^(>.*)$/gm, '$1');