# LinkLog 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 backend/ FastAPI application, services, database, and tests frontend/ Jinja templates and browser-side assets webextension/ Firefox Manifest V3 extension Logo.svg Source logo artwork used by the web and extension interfaces Dockerfile Backend container image docker-compose.yml App with traefik reverse proxy hooks REQUIREMENTS.md Product requirements VIBE/ Conversation and prompt logs ``` ## Requirements For local development: - macOS, Linux, or Windows with Python 3.11+ - Firefox for installing the extension - Docker Desktop and Docker Compose v2 for the container workflow 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 From the repository root: ```sh python3.11 -m venv .venv . .venv/bin/activate python -m pip install --upgrade pip python -m pip install -r backend/requirements.txt ``` On Windows PowerShell, activate the environment with: ```powershell .venv\Scripts\Activate.ps1 ``` The SQLite database is created automatically at `backend/data/linklog.db` when the application starts or when the auth router is imported. The starter accounts are: | Username | Password | Role | | --- | --- | --- | | `alice` | `secret123` | administrator | | `bob` | `secret123` | standard user | These credentials are for development only. Change the authentication and seeding design before deploying publicly. The database schema is versioned with SQLite `PRAGMA user_version`. Application startup applies all pending migrations in order, so updating the application does not require deleting an existing database. New schema changes should be added as a new numbered migration in `backend/app/database.py`; existing migration entries must remain unchanged. ## Run The Backend Activate the virtual environment, then run uvicorn from the repository root: ```sh . .venv/bin/activate PYTHONPATH=. uvicorn backend.app.main:app --reload --host 127.0.0.1 --port 8000 ``` Open these URLs: - Public feed: - User feed: - Profile settings: - Labels: - About: - Admin page: - Web login: - Health check: - OpenAPI documentation: The browser extension defaults to `http://localhost:8000`. The supplied `Logo.svg` is bundled as `frontend/static/logo.svg` for web pages and `webextension/logo.svg` for the Firefox popup and settings page. The visible LinkLog brand text uses the Google Foundry `Asset` font when available, with local fallbacks in the Firefox extension. ## Regenerate Logo Assets `LinkLog.svg` is the source logo. Install ImageMagick, then regenerate the web logo, extension logo, and Firefox toolbar icons with: ```sh 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. ## Releases Releases run in Gitea Actions when a `v*` tag is pushed. The Docker release version comes from `frontend/version.json`; the Firefox plugin version comes from `webextension/manifest.json`. CI also requires both to match `LINKLOG_VERSION`'s default in `backend/app/core/config.py`. The signed XPI is produced manually and must be checked into `XPI/signed/LinkLog-.xpi` before creating the tag. The workflow validates the embedded manifest, publishes the XPI and `webextension/updates.json` as Gitea release assets, and publishes Docker images to `git.kolkman.org/olaf/link-log:` and `:latest`. The extension's `update_url` points at the stable raw repository URL `https://git.kolkman.org/olaf/Link-Log/raw/branch/main/webextension/updates.json`. Update `webextension/updates.json` with each signed XPI version and commit it together with the XPI. The release page provides a direct install link at `https://git.kolkman.org/olaf/Link-Log/releases/download/v/LinkLog-.xpi`. The workflow requires Gitea Actions secrets named `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and `GITEA_TOKEN`. The token needs permission to create releases and upload release assets; the registry credentials need permission to push the image. Every push to `main` also runs `.gitea/workflows/development.yml` and publishes the current Docker image as `git.kolkman.org/olaf/link-log:development`. The workflow uses the same `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` secrets and can also be started manually from Gitea Actions. 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. Users can change their password from the profile page. The current password is required, new passwords must contain at least 8 characters, and the endpoint is `PUT /api/user/password`. On the profile page, the authenticated username is displayed as read-only. Users can upload a PNG, JPEG, GIF, or WebP avatar up to 2 MB; uploaded files are stored in the persistent data volume and served by the application. Bio and email fields remain empty until the user provides values. Mastodon settings default to the `mastodon.social` instance and the `From my #LinkLog: ` post prefix. ## Run Tests ```sh . .venv/bin/activate PYTHONPATH=. pytest backend/tests -q ``` To ensure warnings are clean as well: ```sh PYTHONPATH=. PYTHONWARNINGS=error pytest backend/tests -q ``` ## Install The Firefox Extension For Development The extension is an unpacked Firefox extension. No build step is required. The toolbar uses square PNG icons generated from the bundled dark-background logo; `logo.svg` remains available for the popup and settings branding. The manifest includes stable Firefox extension metadata and references the packaged PNG icons for toolbar and add-on installation. 1. Start the backend locally. 2. Open Firefox and visit `about:debugging#/runtime/this-firefox`. 3. Select **Load Temporary Add-on**. 4. Choose `webextension/manifest.json`. 5. Open the LinkLog extension options and enter: - Backend URL: `http://localhost:8000` - Username: `alice` - Password: `secret123` 6. Save the settings and login. 7. Open a webpage, select the LinkLog toolbar button, review the title and URL, add a comment, and submit it. When the extension settings page has a valid session, it shows ` logged in at ` and a **Sign out** button instead of the login form. Signing out revokes the token and returns the form. Temporary extensions are removed when Firefox restarts. Reload the extension from `about:debugging` after changing its files. ## Docker Create the Docker environment file before starting the stack: ```sh cp .env.example .env ``` Edit `.env` and replace `LINKLOG_SECRET_KEY` with a long random value. Docker Compose automatically reads `.env` from the repository root. The committed `.env.example` contains safe defaults and placeholders; the real `.env` is ignored by Git. The main configurable values are: | Variable | Purpose | Default | | --- | --- | --- | | `LINKLOG_SECRET_KEY` | token signing/security secret | required in Docker | | `LINKLOG_DATABASE_PATH` | SQLite file path inside the container | `/app/backend/data/linklog.db` | | `LINKLOG_TOKEN_EXPIRY_DAYS` | access-token lifetime | `30` | | `LINKLOG_TRACKING_PARAMS` | comma-separated tracking parameters (stripped from logged URLs) | built-in list | | `TRAEFIK_HOST` | hostname routed by Traefik | `localhost` | | `APP_PORT` | direct host port for FastAPI | `8000` | The full set of supported variables is listed in `.env.example`. Application variables are passed into the container by Compose; Docker and Traefik variables are used by Compose itself. Build and start the application and local Traefik proxy: ```sh docker compose up --build ``` The services are available at: - LinkLog through Traefik: - Direct application port: The Compose configuration routes the hostname `localhost` through Traefik. The SQLite database is stored in the named Docker volume `linklog_data`, mounted at `/app/backend/data`. The application runs as a non-root user and reports container health through `/health`; Traefik waits for the application health check before starting. Stop the stack without deleting its database: ```sh docker compose down ``` Stop the stack and delete the named database volume: ```sh docker compose down -v ``` For a real deployment, replace the `localhost` router rule, configure TLS, protect the Traefik dashboard, avoid exposing the direct application port, and provide production secrets and authentication. The included Compose file is a local/prototype deployment scaffold, not a production security configuration. ## Mastodon Configuration After logging in, open and save the Mastodon settings: - **Instance**: hostname or URL such as `mastodon.social` or `https://mastodon.social` - **Access token**: a Mastodon API token with permission to create statuses - **Post prefix**: text placed immediately before the link; defaults to `From my #LinkLog: ` Enable the plugin from the admin API or the admin page. New links are saved first and then posted to the configured instance at `/api/v1/statuses`. A Mastodon network failure does not undo the saved link. ## Useful API Calls Login: ```sh curl -X POST http://localhost:8000/api/auth/login \\ -H 'Content-Type: application/json' \\ -d '{"username":"alice","password":"secret123"}' ``` Submit a link using the returned access token: ```sh curl -X POST http://localhost:8000/api/links \\ -H 'Content-Type: application/json' \\ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\ -d '{"title":"Example","url":"https://example.com","comment":"Worth reading"}' ``` The public feed is available at: ```text GET http://localhost:8000/api/public/feed ``` When a request includes a valid bearer token, entries owned by that authenticated user include edit permission and show an inline **Edit** action in the feed. The update endpoint is `PUT /api/links/{link_id}` and rejects edits from other users. Feed items also expose `is_owner`; it is `true` only for entries owned by the authenticated user and `false` for anonymous viewers or other users. Links support zero to ten tags. Tags are trimmed, deduplicated case-insensitively, and retain their original casing for display. The Firefox capture popup shows existing server tags as checkboxes and accepts new comma-separated tags. The home page displays tags and provides a case-insensitive tag filter; editing a link replaces its complete tag set. The inline link editor also allows multiple existing tags to be selected and new tags to be entered. New tag values receive a leading `#` automatically, and the interface prevents saving more than ten tags. The installation seeds these available tags: `#Internet`, `#Cybersecurity`, `#Fediverse`, `#Food`, `#Photography`, `#Music`, and `#AI`. Users can create, edit, and delete their own labels from `/labels`. Administrators can delete any label from `/admin`; system-seeded labels have no user owner. Admin plugin requests must include the administrator's token: ```sh curl http://localhost:8000/api/admin/plugins \\ -H 'Authorization: Bearer YOUR_ADMIN_ACCESS_TOKEN' ``` Administrators can manage user privileges from the admin page. The `Administrator` checkbox sends `PUT /api/admin/users/{user_id}` with `{"is_admin": true}` or `{"is_admin": false}`. The API rejects any change that would leave the system without an administrator. ## Troubleshooting - **`ModuleNotFoundError`**: activate `.venv` and rerun `python -m pip install -r backend/requirements.txt`. - **Jinja2 import error in Docker**: rebuild the image with `docker compose up --build`; the runtime dependency is declared in `backend/requirements.txt`. - **Extension cannot login**: confirm the backend is running, use the exact backend origin without a trailing API path, and check the browser console for blocked requests. - **Port 8000 is occupied**: run uvicorn with another port and update the extension backend URL, for example `--port 8001`. - **Port 80 or 8080 is occupied**: change the host-side ports in `docker-compose.yml`. - **Stale development data**: stop the backend and remove `backend/data/linklog.db`, or run `docker compose down -v` for the container volume. - **Schema migration issue**: inspect the database version with `sqlite3 backend/data/linklog.db 'PRAGMA user_version;'` and restart the application to apply pending migrations.