██████ ██████ ██████ ████████ █████ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██ ██ ██████ ██ ███████ ██
██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██ ██ ██ ██ ██ ███████
// BOOKMARKS — terminal link vault
A cyberpunk-style minimalist bookmark management website built with Flask and SQLite, featuring drag-and-drop sorting and one-click Docker deployment.
- CRUD operations — Add, edit, and delete bookmarks with custom titles.
- Edit mode — Click
[ EDIT ]in the top-right corner to enter edit mode; the button turns yellow. Only in edit mode are the per-item EDIT and DEL buttons visible (no hover required), and drag-and-drop reordering enabled. Click again to exit. - Add mode — Click
[ ADD ]in the top-right corner to reveal the ADD NEW ENTRY form. A[ CANCEL ]button inside the form exits add mode without submitting. Add mode and edit mode are mutually exclusive. - Delete confirmation — Deleting a bookmark requires a second confirmation via a modal overlay, preventing accidental removal.
- Drag-and-drop sorting — Drag items to instantly save their order using SortableJS (only available in edit mode), with no data lost on refresh.
- Inline editing — Click EDIT to expand the editing panel directly in the list, eliminating the need for page redirects.
- Toast notifications — Lightweight, terminal-style pop-ups appear after every action.
- Zero frontend dependencies — The project only imports SortableJS via CDN, avoiding heavy frontend frameworks.
- SQLite persistence — Data is securely stored in a local file without needing an external database.
- One-click Docker deployment — The multi-stage Alpine build keeps the container image extremely small.
- Comprehensive test coverage — Full workflow testing with pytest covers the homepage, adding, deleting, editing, and reordering.
| Mode | Trigger | Effect |
|---|---|---|
| Normal | Default | Read-only view; EDIT/DEL buttons hidden; drag disabled |
| Edit mode | [ EDIT ] button (top-right) |
Shows EDIT/DEL buttons on every item; enables drag-and-drop reordering |
| Add mode | [ ADD ] button (top-right) |
Reveals the ADD NEW ENTRY form; [ CANCEL ] hides it again |
Edit mode and add mode are mutually exclusive — activating one automatically exits the other.
| Layer | Technology |
|---|---|
| Backend framework | Python 3.12 · Flask |
| Data storage | SQLite (data/bookmarks.db) |
| Template engine | Jinja2 |
| Frontend interaction | Vanilla JS · SortableJS 1.15 |
| UI style | Cyberpunk terminal, pure CSS variables |
| Containerization | Docker (Alpine multi-stage build) |
| Testing framework | pytest · Flask Test Client |
# Pull and run the pre-built image directly
docker run -d \
-p 5000:5000 \
-v $(pwd)/data:/app/data \
--name portal \
ghcr.io/evlos/portal:latestVisit http://localhost:5000 to start using the app.
# Build the image
docker build -t portal .
# Run the container with a mounted volume for persistent data
docker run -d \
-p 5000:5000 \
-v $(pwd)/data:/app/data \
--name portal \
portalVisit http://localhost:5000 to start using the app.
# Clone the repository
git clone <repo-url>
cd portal
# Install dependencies
pip install -r requirements.txt
# Start the server
python app.py| Method | Path | Description |
|---|---|---|
GET |
/ |
Home page, renders the bookmark list |
POST |
/add |
Add a new bookmark (form submission) |
POST |
/edit/<id> |
Edit a bookmark (JSON) |
POST |
/delete/<id> |
Delete a bookmark (returns JSON) |
POST |
/reorder |
Update sorting order (JSON array of IDs) |
portal/
├── app.py # Main Flask app, routing and database logic
├── templates/
│ └── index.html # Single-page frontend, Jinja2 + vanilla JS
├── tests/
│ ├── conftest.py # pytest fixtures, isolated test database
│ └── test_app.py # Complete test cases
├── Dockerfile # Alpine multi-stage build
├── requirements.txt # Python dependencies
└── LICENSE # GPL v3
pip install pytest
pytest tests/ -vThis project is open-sourced under the GNU General Public License v3.0.
