From 5d60bd801cf3fc903d60d9ac8ec018a897238f77 Mon Sep 17 00:00:00 2001 From: Olaf Date: Tue, 25 Aug 2026 09:07:29 +0200 Subject: [PATCH] Versioning (0.1.0) --- .env.example | 1 + .gitignore | 1 + Makefile | 16 +++++++++++++++- README.md | 11 +++++++++++ VIBE/CHAT_LOG.md | 18 ++++++++++++++++++ VIBE/PROMPTS.md | 3 +++ VIBE/README.md | 6 ++++-- XPI/unsigned/LinkLog-0.1.0.xpi | Bin 0 -> 24119 bytes backend/app/core/config.py | 1 + backend/app/main.py | 3 ++- backend/tests/test_api.py | 1 + frontend/static/logo.svg | 2 ++ webextension/icon-16.png | Bin 950 -> 950 bytes webextension/icon-32.png | Bin 1588 -> 1588 bytes webextension/icon-48.png | Bin 2486 -> 2486 bytes webextension/icon-96.png | Bin 4938 -> 4938 bytes webextension/logo.svg | 2 ++ 17 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 XPI/unsigned/LinkLog-0.1.0.xpi diff --git a/.env.example b/.env.example index dc4532b..ce2bb45 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ APP_HEALTHCHECK_TIMEOUT=5s APP_HEALTHCHECK_START_PERIOD=10s APP_HEALTHCHECK_RETRIES=3 LINKLOG_APP_NAME=LinkLog +LINKLOG_VERSION=0.1.0 LINKLOG_SECRET_KEY=replace-with-a-long-random-secret LINKLOG_TOKEN_EXPIRY_DAYS=30 # Optional comma-separated override. Leave empty to use the built-in list. diff --git a/.gitignore b/.gitignore index fdc1eef..192c4a8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ __pycache__/ *.log backend/data/avatars/ LinkLog.afdesign~lock~ + diff --git a/Makefile b/Makefile index e88d081..f213b4d 100644 --- a/Makefile +++ b/Makefile @@ -9,13 +9,27 @@ WEB_LOGO := frontend/static/logo.svg EXTENSION_LOGO := webextension/logo.svg ICON_FILES := webextension/icon-16.png webextension/icon-32.png webextension/icon-48.png webextension/icon-96.png GENERATED_LOGOS := $(WEB_LOGO) $(EXTENSION_LOGO) $(ICON_FILES) +EXTENSION_VERSION := $(shell sed -n 's/^[[:space:]]*"version":[[:space:]]*"\([^"]*\)".*/\1/p' webextension/manifest.json | head -n 1) +XPI_FILE := LinkLog-$(EXTENSION_VERSION).xpi +XPI_UNSIGNED_DIR := XPI/unsigned +XPI_SIGNED_DIR := XPI/signed +XPI_OUTPUT := $(XPI_UNSIGNED_DIR)/$(XPI_FILE) +EXTENSION_FILES := manifest.json logo.svg icon-16.png icon-32.png icon-48.png icon-96.png options.css options.html options.js popup.css popup.html popup.js -.PHONY: all logos check-tools clean-generated +.PHONY: all logos xpi check-tools clean-generated all: logos + logos: check-tools $(GENERATED_LOGOS) +xpi: logos + @test -n "$(EXTENSION_VERSION)" || { echo "Error: extension version is missing from webextension/manifest.json" >&2; exit 1; } + @mkdir -p $(XPI_UNSIGNED_DIR) $(XPI_SIGNED_DIR) + @rm -f $(XPI_OUTPUT) + @cd webextension && zip -q -9 "../$(XPI_OUTPUT)" $(EXTENSION_FILES) + @echo "Created $(XPI_OUTPUT)" + check-tools: @command -v $(MAGICK) >/dev/null 2>&1 || { echo "Error: ImageMagick '$(MAGICK)' is required. Install ImageMagick and retry." >&2; exit 1; } diff --git a/README.md b/README.md index 4b6d0d0..04f7092 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ LinkLog is a Firefox extension and Python web service for saving links with a title, comment, timestamp, and tracking parameters removed. The service stores links in SQLite and can publish them through plugins, including Mastodon. +For full transparency: The author used vibe coding to create this software. + ## Project Layout ```text @@ -25,6 +27,7 @@ For local development: The backend currently uses FastAPI, uvicorn, SQLite, and Pydantic. `httpx2` is included for the Starlette-compatible test client. Jinja2 is included for server-rendered HTML templates. +The backend version is `0.1.0` and is exposed through the FastAPI/OpenAPI metadata. It can be overridden with `LINKLOG_VERSION`. LinkLog is licensed under the GNU General Public License, version 3 or any later version. See [LICENSE](LICENSE). ## Local Installation @@ -93,6 +96,14 @@ make logos The generated files are `frontend/static/logo.svg`, `webextension/logo.svg`, and `webextension/icon-16.png`, `icon-32.png`, `icon-48.png`, and `icon-96.png`. Override the ImageMagick executable with `make MAGICK=magick logos` when needed. +Create an installable Firefox XPI bundle with: + +```sh +make xpi +``` + +This creates `XPI/unsigned/LinkLog-0.1.0.xpi` from the `webextension/` package and excludes macOS metadata and minified artifacts. The version is read from `webextension/manifest.json`. The `XPI/signed/` directory is reserved for signed release bundles. + Appending a username to the root URL, such as `/alice`, opens that user's public feed and profile information. Configuration APIs require a bearer token returned by the login endpoint. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account; the development `alice` account is seeded as an administrator, while `bob` is a standard user. diff --git a/VIBE/CHAT_LOG.md b/VIBE/CHAT_LOG.md index 9fcf775..d1d50a8 100644 --- a/VIBE/CHAT_LOG.md +++ b/VIBE/CHAT_LOG.md @@ -493,3 +493,21 @@ Add a copyright statement to source files refer to GNU license ### Assistant outcome Added `Copyright © 2026 Olaf Kolkman` and `SPDX-License-Identifier: GPL-3.0-or-later` headers to all 47 non-minified application, test, frontend, extension, build, deployment, and source-logo files. Generated/runtime assets were left untouched. Python and JavaScript compilation, Compose validation, whitespace checks, and the strict backend suite passed with 15 tests. + +### User +Add the version to the name of the resulting xpi + +### Assistant outcome +Updated the Makefile to read the extension version from `webextension/manifest.json` and produce a versioned archive such as `LinkLog-0.1.0.xpi`. Updated `.gitignore` and README documentation. The generated XPI contains 12 expected extension files and passes `unzip -tq` integrity validation. + +### User +Set the version number of the backend to 0.1.0 + +### Assistant outcome +Added backend setting `version = 0.1.0`, wired it into FastAPI and OpenAPI metadata, and exposed `LINKLOG_VERSION` as an environment override. Added regression coverage and documented the setting. Compose validation and the strict backend suite passed with 15 tests. + +### User +Add the creation of an xpi bundle for the webextention to the Makefile + +### Assistant outcome +Added a portable `xpi` Makefile target that regenerates logo assets and packages the Firefox extension into `LinkLog.xpi`, excluding metadata and minified artifacts. Added README instructions, ignored the generated XPI, and verified the archive contents and integrity with `unzip -tq`. diff --git a/VIBE/PROMPTS.md b/VIBE/PROMPTS.md index 856116c..01fc9c4 100644 --- a/VIBE/PROMPTS.md +++ b/VIBE/PROMPTS.md @@ -85,6 +85,9 @@ 81. in the footer make the link the same color as the mother text, when clicked use a tint from the Catpuccin theme 82. Add a GNU License file 83. Add a copyright statement to source files refer to GNU license +84. Add the version to the name of the resulting xpi +85. Set the version number of the backend to 0.1.0 +84. Add the creation of an xpi bundle for the webextention to the Makefile ## Future entries diff --git a/VIBE/README.md b/VIBE/README.md index eb628bb..5c9cbbf 100644 --- a/VIBE/README.md +++ b/VIBE/README.md @@ -1,9 +1,11 @@ # VIBE +This code has mostly been vibe coded. For full transparency we maintain a log of prompts and results. + This folder contains an append-only record of the visible Link Log development conversation. -- `CHAT_LOG.md` records user requests and assistant responses or outcomes in chronological order. -- `PROMPTS.md` records user prompts separately for quick reference. +- [`CHAT_LOG.md`](./CHAT_LOG.md) records user requests and assistant responses or outcomes in chronological order. +- [`PROMPTS.md`](PROMPTS.md) records user prompts separately for quick reference. Only the user-visible conversation is recorded. System, developer, environment, and private tool instructions are intentionally excluded. diff --git a/XPI/unsigned/LinkLog-0.1.0.xpi b/XPI/unsigned/LinkLog-0.1.0.xpi new file mode 100644 index 0000000000000000000000000000000000000000..1ca1c294c17f0703599df0277a358d1bc058369e GIT binary patch literal 24119 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 AQUCw| literal 0 HcmV?d00001 diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 269ba28..187780f 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -13,6 +13,7 @@ DB_PATH = BASE_DIR / 'data' / 'linklog.db' @dataclass class Settings: app_name: str = os.getenv('LINKLOG_APP_NAME', 'LinkLog') + version: str = os.getenv('LINKLOG_VERSION', '0.1.0') database_url: str = os.getenv('LINKLOG_DATABASE_URL', f'sqlite:///{DB_PATH}') secret_key: str = os.getenv('LINKLOG_SECRET_KEY', 'dev-secret-key-change-me') token_expiry_days: int = int(os.getenv('LINKLOG_TOKEN_EXPIRY_DAYS', '30')) diff --git a/backend/app/main.py b/backend/app/main.py index b8ecd7e..2ded283 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -12,10 +12,11 @@ from backend.app.api.auth import router as auth_router from backend.app.api.links import router as links_router from backend.app.api.public import router as public_router from backend.app.api.user_config import router as user_config_router +from backend.app.core.config import settings from backend.app.database import AVATARS_DIR from backend.app.services.link_service import list_public_links -app = FastAPI(title='LinkLog API') +app = FastAPI(title='LinkLog API', version=settings.version) app.mount('/static', StaticFiles(directory='frontend/static'), name='static') app.mount('/media', StaticFiles(directory=AVATARS_DIR), name='media') app.include_router(auth_router, prefix='/api/auth') diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 1a1c518..0da1620 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -22,6 +22,7 @@ def login_headers(username='alice'): def test_login_returns_token(): + assert app.version == '0.1.0' response = client.post('/api/auth/login', json={ 'username': 'alice', 'password': 'secret123', diff --git a/frontend/static/logo.svg b/frontend/static/logo.svg index ee95617..f87aede 100644 --- a/frontend/static/logo.svg +++ b/frontend/static/logo.svg @@ -1,4 +1,6 @@ + + diff --git a/webextension/icon-16.png b/webextension/icon-16.png index 5f895f25fb84ccd9c7090272dfb31caa1db757a0..b86cdecf5c51ad6b148d32c1d44e8be5d0ab01bc 100644 GIT binary patch delta 95 zcmdnSzKwkW6Ne-_gQC9Aaqr0-OtK=TAqM7F1_oBfM%qBqz+msGnk|zpu}GYmeuQIk W7Lx)(hcQHl?bmn7o98h}FaiLZpc%6O delta 95 zcmdnSzKwkW6NiL|u64)_xk-~bm}EsvLW~ToOiZmzEwv2{tPBif)lX$lw!|WlQ&n_* Tau$;Uh7RleV2;i6m?RhhfcptHiB=)8JPdP=f~ChLX(O)Z&uF+{wF`xkb>8k5j!J Kx%nn@G!p=&G%dvd delta 165 zcmdnOvxR4aHFLd$h#uq4pHm()Ffgc=xJHzuB$lLFB^RXvDF!10BQsqC3tc0V5F-OC z6EiCVLu~^CD+7aGdBbcJ4Y~O#nQ4`{HT-evybjc$0k@$fGdH!kBr$jLE@o~KbmLct Mui)5xlR26R0Do97v;Y7A diff --git a/webextension/icon-48.png b/webextension/icon-48.png index 95cdfb4ad1714cf4ebea1974dc50b6f954cd528d..d16b25e93482a298fb8e2c5a8b0e71d41da1c922 100644 GIT binary patch delta 92 zcmdlcyiIsQJ%=PagQ|uibI`5Z6Il2@cvV<@WdUM;`7$OUO(}r R0*3mNyG|8smSvpD0RXED8&m)Q delta 92 zcmdlcyiIsQJ%@yd9&`BjujePW%ZiwU7#UcZm{}PZYa19?85l@4C^${rfhnGScgDYo SFBLG<+nm^`xLKBQCI;nT#I R3K;5VwryBK+6D$z1_qS@q7@T&V2X3_u01mG Rr2>Zfc!?hA&9aQTLI8iU7tR0x diff --git a/webextension/logo.svg b/webextension/logo.svg index ee95617..f87aede 100644 --- a/webextension/logo.svg +++ b/webextension/logo.svg @@ -1,4 +1,6 @@ + +