### Description
This PR introduces support for dark background colors on post labels.
Specifically, the following changes have been made:
1. **Luminance & Color Contrast**: Added `isDark` and `contrastingTextColor` getters in `ColorViewModel`. The color contrast detection has been improved to use the standard sRGB relative luminance formula (per WCAG 2.x standard) with a threshold of `0.179` to determine if a background is dark, ensuring much more accurate contrast adjustments than a simple YIQ calculation.
2. **Text Color Adjustment**: Sets the text color of the `PostLabel` dynamically to `#ffffff` if the background is dark, and inherits the default color if the background is light.
3. **Indicator Dot Styling**: Adjusts the background color of the small decorative dot inside `PostLabel` using a lighter variant (`lighten(0.4)`) on dark labels, and a darker variant (`darken(0.2)`) on light labels to ensure visual clarity.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#165
### Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
Reviewed-on: #298
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This PR configures and registers MCP (Model Context Protocol) servers to enhance AI-assisted development workflow, corrects a typo in the PR template, and documents the setup instructions in `README.md`.
Specifically, the following changes are introduced:
- **MCP Configuration**: Created `.agents/mcp_config.json` to register MCP servers for `gitea`, `svelte`, and `cratesio` domains.
- **Documentation**: Updated `README.md` with step-by-step instructions to set up the Go environment, install `cratesio-mcp`, and configure the Gitea access token.
- **Template Correction**: Corrected a typo in `.gitea/PULL_REQUEST_TEMPLATE.yaml`.
- **Git Ignore**: Added `.antigravitycli` directory to `.gitignore` to prevent tracking of local CLI configurations.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#296.
### Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
Reviewed-on: #297
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This change migrates the frontend environment from Node.js/pnpm to Bun to improve build performance and runtime efficiency. The migration includes updating CI/CD workflows, Docker configurations, and the SvelteKit adapter. Additionally, the PR introduces stricter linting rules via eslint-plugin-simple-import-sort and a global restriction on relative imports to enforce the use of path aliases ($lib/...), ensuring a cleaner and more maintainable import structure across the project. Backend environment variables were also renamed for consistency.
### Package Changes
Package | Action | Version
-- | -- | --
svelte-adapter-bun | Added | 1.0.1
eslint-plugin-simple-import-sort | Added | 13.0.0
@types/node | Added | 22.19.17
eslint-plugin-import | Removed | 2.32.0
@sveltejs/adapter-node | Removed | 5.5.4
svelte | Updated | 5.55.1 -> 5.55.5
@sentry/sveltekit | Updated | 10.47.0 -> 10.51.0
@sveltejs/kit | Updated | 2.55.0 -> 2.58.0
tailwindcss | Updated | 4.2.2 -> 4.2.4
typescript-eslint | Updated | 8.58.0 -> 8.59.1
### Screenshots
_No response_
### Reference
Resolves#290.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #294
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #292
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#288.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #289
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #287
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#284 Cont.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#282.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #286
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#284 Cont.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#282.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #285
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Description
This pull request refactors our Sentry configuration to improve error tracking accuracy and support multi-project deployments. We've added explicit release versioning and environment tracking to both the client and server hooks. Additionally, sourcemap uploading has been decoupled from the default SvelteKit Sentry wrapper; we are now directly utilizing `@sentry/vite-plugin` to iterate over an array of projects. This ensures that sourcemaps are correctly uploaded to both the `blog-frontend` and `blog-beta-frontend` Sentry projects during the build process.
#### Key Changes
* **`frontend/src/hooks.client.ts` & `frontend/src/hooks.server.ts`**: Updated `Sentry.init()` to include explicit `release` (`App.__VERSION__`) and `environment` (`process.env.NODE_ENV || 'development'`) tracking.
* **`frontend/vite.config.ts`**:
* Removed inline `sourceMapsUploadOptions` from `sentrySvelteKit()`.
* Imported and configured `@sentry/vite-plugin` to map over an array of `sentryProjects` (`blog-frontend`, `blog-beta-frontend`), ensuring sourcemaps and release versions are tied to multiple target projects during Vite's build phase.
#### Testing/Review Notes
* **Build Verification:** Run a production build (`pnpm build`) and verify that the build output logs confirm sourcemap uploads to both `blog-frontend` and `blog-beta-frontend` Sentry projects.
* **Tag Verification:** Trigger a manual error in your local development environment and check the Sentry dashboard to confirm that the `environment` (e.g., "development") and `release` tags are correctly attached to the event payload.
* **Regression Check:** Ensure that client-side and server-side routing continues to function without Sentry initialization errors.
### Package Changes
* **Added:** `@sentry/vite-plugin` (`^5.2.0`) in `package.json` to handle dedicated Vite build integrations.
* **Updated:** Relevant `@sentry/*` build dependencies (e.g., `babel-plugin-component-annotate`, `bundler-plugin-core`, `rollup-plugin`) bumped to `5.2.0` in `pnpm-lock.yaml` to align with the new Vite plugin addition.
### Screenshots
_No response_
### Reference
Resolves#282.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #284
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #281
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#275.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #280
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#274.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #279
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#273.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #278
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#271.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #277
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots

### Reference
Resolves#272.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #276
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#269.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #270
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #268
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Refer to #265.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#261.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #267
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Summary
This resolves the issue where Sentry logs and source maps were failing to upload during the deployment pipeline (BLOG-261). The core problem was that the frontend build environment lacked the necessary authentication credentials to communicate with the Sentry API. I've updated the deployment workflow to pass the `SENTRY_AUTH_TOKEN` from our CI secrets into the Docker build context, and updated the frontend Dockerfile to securely mount and utilize this token during the build phase. A minor descriptive adjustment was also made to our pre-commit config.
#### Key Changes
* **`.gitea/workflows/deployment.yaml`**: Injected the `SENTRY_AUTH_TOKEN` into the build container's secrets configuration.
* **`frontend/Dockerfile`**: Configured secure secret mounting (`--mount=type=secret`) to read `SENTRY_AUTH_TOKEN` and expose it as an environment variable specifically during the `pnpm run build` execution.
* **`.pre-commit-config.yaml`**: Renamed the `frontend-lint` hook from "frontend lint" to "frontend lint & check" to better reflect its underlying script behavior.
#### Testing/Review Notes
* Trigger a deployment build in the CI/CD pipeline to test the workflow changes.
* Check the build logs for the frontend container; verify that the Sentry plugin successfully detects the token and uploads the sourcemaps/releases without throwing an authentication error.
* Ensure no token leakage occurs in the standard CI output logs or the final compiled Docker image layers.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#261.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #265
Co-authored-by: squid <squid@squidspirit.com>
Co-committed-by: squid <squid@squidspirit.com>
### Description
#### Summary
This PR addresses the ONNX runtime build failures in the backend container. The core issue originates from Alpine Linux's use of `musl` libc, which causes compatibility conflicts with the pre-compiled C/C++ binaries required by the ONNX ecosystem. By migrating both the builder and runner stages to Debian-based slim images (which utilize standard `glibc`), we provide a compatible compilation environment and resolve the build breakage. Furthermore, this update adds the required environment configuration for the Qdrant vector database.
#### Key Changes
* **`backend/Dockerfile`:**
* Switched the base builder image from `rust:1-alpine` to `rust:1-slim`.
* Replaced the `apk` package manager logic with `apt-get` to provision `build-essential`, `libssl-dev`, and `pkg-config` for the Rust build process.
* Updated the runner stage image from `alpine:latest` to `debian:trixie-slim` to ensure runtime compatibility with the compiled `glibc` binary.
* Added `ENV QDRANT_URL=http://127.0.0.1:6334` to expose the vector database endpoint to the server.
#### Testing/Review Notes
* Run a clean build of the backend image locally (`docker build -t backend:test ./backend`) and confirm that the cargo build process bypasses the previous ONNX compilation errors.
* Start the resulting container and verify that the server boots successfully without missing shared library errors (`.so`).
* Ensure your local or CI test environments can reach the specified `QDRANT_URL` or override it as necessary during runtime.
### Package Changes
* **System Packages (Dockerfile):**
* *Removed:* `build-base`, `openssl-dev`, `openssl-libs-static` (Alpine).
* *Added:* `build-essential`, `libssl-dev`, `pkg-config` (Debian).
* **Application Dependencies:** No changes to `Cargo.toml` or `Cargo.lock`.
### Screenshots
_No response_
### Reference
Resolves#262.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #264
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #260
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Description
This PR introduces a significant update to core frontend dependencies, including Svelte, Vite, Tailwind CSS, and Sentry, to keep the tooling up to date. To comply with the updated linting rules and SvelteKit routing standards, all static and interpolated anchor links have been migrated to use the `$app/paths` `resolve` function. Additionally, component reactivity has been modernized to properly leverage Svelte 5 paradigms, replacing static `$state` assignments with lazy-evaluated functions and transitioning direct parameter reads to `$derived` data properties.
#### Key Changes
* **Routing & Path Resolution:** Imported `resolve` from `$app/paths` across navigation, layout, and dashboard components (e.g., `Footer.svelte`, `Navbar.svelte`, `PostPreview.svelte`) to wrap `href` attributes, ensuring correct base path routing. Added `/* eslint-disable svelte/no-navigation-without-resolve */` where necessary.
* **Reactivity Enhancements:** * Migrated `$state` initializations in dialog components (`EditLabelDialog.svelte`, `EditPostDialog.svelte`, `FilteringDialog.svelte`) to use functional wrappers (e.g., `$state((() => defaultValues)())`) to ensure proper evaluation.
* Updated components and pages to utilize `$derived(data.id)` instead of extracting directly from `params`, accompanied by corresponding `id` returns in `+page.server.ts` load functions.
* Refactored initial data population in root page components to use getter functions (`getInitialData()`) before passing them to container stores.
* **UI/Layout Updates:** * Changed container max-widths from `max-w-screen-xl` to `max-w-7xl` in `Footer.svelte`, `Navbar.svelte`, and `NotFoundPage.svelte`.
* Added `rel="external"` attributes to all outbound links in the footer.
* Replaced hardcoded policy link constants with dynamic parameterized routes.
* **Store Interfaces:** Formatted implementation signatures for improved readability in `postsListedStore.ts`.
#### Testing/Review Notes
* Run `pnpm install` before testing to synchronize the massive updates in `pnpm-lock.yaml`.
* Navigate through the application (especially the dashboard and policy links in the footer) to verify that `$app/paths` `resolve` accurately handles all internal routing.
* Test the `EditPostDialog` and `EditLabelDialog` components to verify that default form values initialize and persist correctly under the new functional `$state` declarations.
* Confirm external links in the footer open correctly without triggering router intercepts.
### Package Changes
* **Updated:** * `svelte` (`5.39.13` -> `5.55.1`)
* `@sveltejs/kit` (`2.22.0` -> `2.55.0`)
* `vite` (`7.0.4` -> `7.3.1`)
* `tailwindcss` (`4.0.0` -> `4.2.2`)
* `eslint` (`9.18.0` -> `9.39.4`)
* `@sentry/sveltekit` (`10.1.0` -> `10.47.0`)
* `typescript-eslint` (`8.20.0` -> `8.58.0`)
* Various other `@sveltejs/*`, `@tailwindcss/*`, and minor tooling dependencies bumped to their latest compatible versions.
### Screenshots
_No response_
### Reference
Resolves#253.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #259
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Summary
This PR introduces a Redis-backed caching layer for search query embeddings. By caching the vector embeddings of search strings, we eliminate redundant embedding generation for repeated queries, significantly improving search latency and reducing CPU/compute overhead. Additionally, the Redis connection management has been refactored to use `deadpool-redis`, establishing a shared connection pool that now serves both the newly implemented embedding cache and the existing Actix session store.
#### Key Changes
* **Search Caching Logic:** Introduced the `SearchQueryEmbeddingCache` trait and implemented `SearchQueryEmbeddingCacheRedisImpl`. Cache keys are uniquely bound to the specific model revision to prevent vector dimension mismatches if the underlying embedding model is updated.
* **Repository Integration:** Modified `SearchRepositoryImpl` to check the Redis cache before calling the text embedding model. Cache misses trigger embedding generation and automatically populate the cache.
* **Redis Pooling:** Abstracted Redis configuration into a dedicated `RedisConfiguration` struct utilizing `deadpool_redis::Pool`.
* **Session Store Refactor:** Updated `actix-session` to use `RedisSessionStore::builder_pooled()`, migrating it to the shared Redis connection pool instead of managing its own standalone connection.
* **Configuration & Env:** Added `REDIS_SESSION_PREFIX` and `REDIS_SEARCH_QUERY_EMBEDDING_PREFIX` to `.env.example` for namespace isolation.
* **Frontend Copy:** Updated the search placeholder text in `frontend/src/lib/strings.ts` to clarify that the search uses vector comparison.
#### Testing/Review Notes
* Ensure a local Redis instance is running and accessible via `REDIS_URL`.
* Execute a semantic search query. Verify via `redis-cli` that a new cache key is generated matching the pattern configured by the prefix and model revision (e.g., `search:query_embedding:<model>:<query>`).
* Execute the same search query a second time. Verify that the search executes noticeably faster and bypasses the local embedding generation phase.
* Verify that user authentication and session states remain fully functional to ensure the `actix-session` pool refactor introduced no regressions.
### Package Changes
* **Added:** `deadpool-redis` (`0.22.1`), `deadpool` (`0.12.3`), `deadpool-runtime` (`0.1.4`)
* **Added:** `redis` (`0.32.7` with features: `aio`, `connection-manager`, `tokio-comp`)
* **Added:** `serde_json` (`1.0.145`) to `backend/Cargo.toml` and `backend/feature/search/Cargo.toml`
* **Updated:** `actix-session` (`0.11.0`) - Added the `redis-pool` feature.
### Screenshots
_No response_
### Reference
Resolves#246.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #257
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
### Summary
This PR replaces the legacy string-matching search logic with an AI-powered semantic vector search (Retrieval-Augmented Generation) for blog posts. By integrating Qdrant as the vector database and FastEmbed for generating local text embeddings, the application now allows users to perform context-aware semantic searches or ask natural language questions to find relevant post content. The backend text processing leverages markdown splitting and tokenization to chunk post content efficiently. The frontend has been updated to provide UI hints regarding this new AI search capability, utilizing a newly added Tooltip component.
### Key Changes
* **Backend - New Search Module:** Created a new `search` workspace feature containing `SearchRepository`, `SearchVectorDbService`, and `SearchService`.
* **Backend - Post Integration:** Integrated `SearchService` into `CreatePostUseCase`, `UpdatePostUseCase`, and `GetAllPostInfoUseCase`. Post creations and updates now automatically trigger vector document indexing. Retrieving posts with a keyword query now delegates to the vector search to return contextually relevant post IDs.
* **Backend - Embeddings & Vector DB:** Added dependency injection and configurations for Qdrant (`QdrantConfiguration`) and local embeddings (`EmbeddingConfiguration`), utilizing the `BAAI/bge-large-zh-v1.5` model for optimized Chinese text embeddings.
* **Backend - Environment & Build:** Added required Qdrant and embedding environment variables to `.env.example`. Updated the `Dockerfile` to utilize parallel compilation (`cargo build --release -j2`) for faster build times.
* **Frontend - UI Components:** Implemented a new `Tooltip` component architecture using `bits-ui`.
* **Frontend - User Experience:** Updated the `FilteringDialog` to display a tooltip hint explaining the new RAG-based semantic search to users. Updated `strings.ts` with relevant placeholders and instructions.
### Testing/Review Notes
* Ensure a local Qdrant instance is running and accessible at the URL defined in your `.env` (default is `http://127.0.0.1:6334`).
* On initial backend startup, verify that the `BAAI/bge-large-zh-v1.5` embedding model downloads successfully to your designated `EMBEDDING_CACHE_DIR`.
* Create or edit an existing post in the system. Check your Qdrant dashboard/API to verify that the post's content chunks are successfully vectorized and upserted into the `blog_post` collection.
* Perform a search query using natural language in the frontend to confirm semantic retrieval returns the expected posts.
* Verify that the new tooltip renders correctly when hovering over the info icon in the search dialog.
### Package Changes
* **Backend (`Cargo.toml` / `Cargo.lock`):**
* **Added:** `fastembed` (5.13.0), `qdrant-client` (1.17.0), `text-splitter` (0.29.3, with `markdown` and `tokenizers` features), `tokenizers` (0.22.2).
* **Updated:** `tokio` (from 1.50.0 to 1.51.0).
* **Frontend (`package.json` / `pnpm-lock.yaml`):**
* **Updated:** `bits-ui` (from ^2.11.5 to ^2.16.5), `@internationalized/date` (from ^3.10.0 to ^3.12.0).
### Screenshots
_No response_
### Reference
Resolves#237.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #256
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
### Summary
This pull request refactors the static `ErrorPage` component into a dynamic, reusable `NotFoundPage`. By accepting a `title` prop, the component can now handle various "not found" or empty states across the application rather than just standard HTTP 404 errors. Notably, this implementation is now utilized in the `PostOverallPage` to gracefully handle scenarios where no posts are returned (e.g., empty database or zero search filter results). Minor structural UI adjustments and centralized string constants were also added to support these updates.
### Key Changes
* **`frontend/src/lib/common/framework/ui/NotFoundPage.svelte`**:
* Renamed from `ErrorPage.svelte`.
* Added a `title` prop to replace hardcoded '404' strings.
* Updated flexbox utility classes (`flex-wrap`, `shrink-0`) to improve layout responsiveness on smaller viewports.
* **`frontend/src/lib/post/framework/ui/PostOverallPage.svelte`**:
* Introduced an `isPostsEmpty` derived state.
* Added conditional rendering to display the `NotFoundPage` with an `EMPTY_POSTS` string when no articles are available.
* **`frontend/src/lib/strings.ts`**:
* Added new constants: `EMPTY_POSTS` ('查無文章'), `NOT_FOUND_CODE` ('404'), and `POST` ('文章') to centralize copy.
* **`frontend/src/routes/+error.svelte` & `frontend/src/routes/dashboard/+layout.svelte`**:
* Replaced legacy `ErrorPage` imports with `NotFoundPage`.
* Passed `Strings.NOT_FOUND_CODE` as the required title prop to maintain previous 404 behavior.
* **`frontend/src/lib/post/framework/ui/FilteringButton.svelte`**:
* Changed the wrapper element from a `<button>` to a `<div>`.
###Testing/Review Notes
* Navigate to an invalid URL to ensure the global `+error.svelte` routing still correctly renders the "404" page.
* Visit the main posts page (`/post`). Apply a filter (keyword or label) that is guaranteed to return zero results. Verify that the new "Empty Posts" (查無文章) view is rendered correctly and the layout does not break.
* Verify that unauthenticated access to the dashboard still triggers the standard 404 view via the layout wrapper.
### Package Changes
_No response_
### Screenshots
<video src="attachments/93d87bc9-48d0-4d50-bb29-f335ecfa0cdb" title="螢幕錄影 2026-04-02 14.58.18.mov" controls></video>
### Reference
Resolves#236.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #255
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This pull request introduces dynamic markdown-based pages for site policies and refactors the global Footer component to improve maintainability and responsive design. The hardcoded static links and UI sections within the Footer were extracted into centralized configuration classes (`Links` and `Strings`) and modular Svelte snippets. Furthermore, we established a new dynamic route `/terms/[name]` capable of serving static markdown files, specifically introducing the Privacy Policy and AI Usage Policy to the platform.
### Key Changes
* **`frontend/src/lib/common/framework/ui/Footer.svelte`:** Upgraded the script block to TypeScript. Refactored the markup using Svelte `#snippet` blocks (`desktopLayout`, `mobileLayout`, `icons`, `policies`, etc.) for cleaner component structure. Integrated centralized strings and links.
* **`frontend/src/lib/links.ts`:** Created a new centralized abstraction for external application URLs (YouTube, Email, RSS, Source Code).
* **`frontend/src/lib/strings.ts`:** Enforced `string` types on existing constants and added new centralized UI strings (`PRIVICY_POLICY`, `AI_USAGE_POLICY`, `TOC`).
* **`frontend/src/lib/post/framework/ui/PostContentPage.svelte`:** Replaced hardcoded "Table of Contents" text with the newly centralized `Strings.TOC` property.
* **`frontend/src/routes/terms/[name]/+page.ts` & `+page.svelte`:** Implemented a dynamic route parameter that fetches corresponding markdown files from the static directory and safely renders them using the existing `MarkdownRenderer` component. Throws a 404 if the markdown file is absent.
* **`frontend/static/terms/*.md`:** Added `privacy-policy.md` and `ai-usage-policy.md` assets containing the platform's official policies.
### Testing/Review Notes
* Verify the global Footer renders correctly on both mobile and desktop viewports.
* Click through the external links in the Footer (YouTube, Email, RSS, Git) to ensure they route to the appropriate targets.
* Navigate to `/terms/privacy-policy` and `/terms/ai-usage-policy` to confirm the markdown documents are fetched and rendered properly.
* Manually test an invalid terms route (e.g., `/terms/does-not-exist`) to verify the 404 error handling functions as expected.
* Open a standard post page to ensure the Table of Contents header correctly displays the localized string.
### Package Changes
_No response_
### Screenshots
|Footer|Terms|
|-|-|
|||
### Reference
Resolves#245.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #254
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This PR introduces HTTP caching mechanisms to the image retrieval endpoint to optimize bandwidth consumption and improve client-side load times. By implementing `ETag` generation and validation alongside aggressive `Cache-Control` directives, the server can now negotiate caching with the client. If the client holds a valid cached version of the image, the server will return a `304 Not Modified` response, entirely bypassing the need to re-transmit the binary image payload.
#### Key Changes
* **`backend/feature/image/src/framework/web/get_image_by_id_handler.rs`:**
* Modified `get_image_by_id_handler` to accept `HttpRequest` for header inspection.
* Added conditional logic to evaluate the `If-None-Match` header against a newly generated ETag.
* Injected `ETag` and `Cache-Control` (`public, max-age=31536000, immutable`) headers into both `200 OK` and `304 Not Modified` responses.
* Implemented `is_not_modified` helper function to accurately parse and validate `If-None-Match` candidates, including wildcard (`*`) support.
* Implemented `generate_etag` utilizing the `Fnv64` hashing algorithm for highly performant, non-cryptographic hashing of the image binary data.
#### Testing/Review Notes
1. Initiate a `GET` request to retrieve an image by its ID. Confirm the response returns a `200 OK` status, includes the binary payload, and exposes both `ETag` and `Cache-Control` headers.
2. Copy the returned `ETag` value.
3. Initiate a secondary `GET` request for the same image ID, explicitly passing the copied ETag value within the `If-None-Match` HTTP header.
4. Verify that the server responds with a `304 Not Modified` status code, omitting the image body payload, while still returning the correct caching headers.
### Package Changes
* **Added:** `fnv_rs` (`v0.4.4`) - Introduced for high-performance FNV-1a hashing required during ETag computation.
* **Added:** `paste` (`v1.0.15`) - Transitive dependency resolved via `fnv_rs`.
### Screenshots

### Reference
Resolves#240.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #252
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This PR introduces the ability to delete uploaded images while ensuring data integrity by preventing the deletion of images currently referenced by any posts. It adds a new `post_image` relational table to track these references dynamically. The frontend has been updated to support deleting images from the dashboard, visually disabling the deletion action for in-use images. Additionally, we've extracted image parsing logic into a dedicated service, updated dependencies across the backend, and strengthened our pre-commit hooks to ensure deeper code validation.
#### Key Changes
* **Database Migrations**:
* Created the `post_image` join table to establish a relationship between posts and images and track active references.
* **Backend - Image Module**:
* Added `DeleteImageUseCase` and `delete_image_handler` (`DELETE /image/{id}`) to handle safe deletions.
* Refactored the internal entity `ImageInfo` to `ImageMetaData` and introduced an `is_referred` boolean on API DTOs.
* Updated `GetImageInfoUseCase` and `ListImagesUseCase` to query the `PostRepository` for reference counts, determining if an image is safe to delete.
* **Backend - Post Module**:
* Introduced `ImageExtractor` to automatically parse Markdown content and the `preview_image_url` to extract embedded image IDs.
* Updated `CreatePostUseCase`, `UpdatePostUseCase`, and `PostDbServiceImpl` to synchronize `post_image` relationships upon post creation/updates based on the extracted IDs.
* Refactored label validation logic into a centralized `LabelRelationService`.
* **Frontend**:
* Implemented `ImageDeletedStore` and the corresponding `DeleteImageUseCase`.
* Updated `ImageOverallDashboardPage` to display a delete button, which disables automatically if `isReferred` is true.
* Changed `previewImageUrl` types from `URL` to `string` in post DTOs, ViewModels, and UI components (`OpenGraph`, `StructuredData`) to simplify state hydration/serialization.
* **Tooling & CI**:
* Updated `.pre-commit-config.yaml` to restrict hook executions to their specific directories (`^backend/` and `^frontend/`).
* Added `cargo test` to `backend-check.sh` and `pnpm check` to `frontend-lint.sh`.
#### Testing/Review Notes
* **Database setup**: Run `sqlx migrate run` to apply the new `post_image` table migration.
* **Upload & Reference**: Upload an image via the dashboard. Create a new post referencing the image (either within the Markdown content or as the preview image).
* **Deletion constraint check**: Navigate to the Image Dashboard. Verify the delete button for the referenced image is disabled. Sending a direct `DELETE` request to the API should return a `400 Bad Request` with the `ReferencedImage` error.
* **Successful deletion**: Remove the image reference from the post (or delete the post). Return to the Image Dashboard and verify the image can now be successfully deleted.
* **Tooling check**: Verify that committing changes locally correctly runs `cargo test` and `pnpm check` without failures.
### Package Changes
**Backend (`Cargo.toml` / `Cargo.lock`)**:
* Updated `actix-web` from **4.12.1** to **4.13.0**
* Updated `tokio` from **1.48.0** to **1.50.0**
* Updated `chrono` from **0.4.42** to **0.4.44**
* Updated `anyhow` from **1.0.100** to **1.0.102**
* Updated `env_logger` from **0.11.8** to **0.11.10**
* Updated `futures` from **0.3.31** to **0.3.32**
* Updated `regex` from **1.12.2** to **1.12.3**
* Updated `sentry` from **0.46.0** to **0.47.0**
* Various transitive dependencies updated to reflect the new primary bumps.
### Screenshots
|Is Not Referred|Is Referred|
|-|-|
|||
### Reference
Resolves#243.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #250
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Description/Summary
This pull request streamlines our frontend UI architecture by removing the deprecated `SearchBar.svelte` component, pointing toward a consolidation of search input handling. To maintain a consistent and polished user experience, the keyword input within `FilteringDialog.svelte` has been updated with refined padding and placeholder typography. Additionally, a root `.gitignore` file has been initialized to establish standard exclusions for environment configurations and OS-specific artifacts, preventing accidental commits of sensitive or unnecessary files.
#### Key Changes
* **`.gitignore`:** Added root ignore file to exclude `.env` configurations and macOS `.DS_Store` artifacts from version control.
* **`FilteringDialog.svelte`:** Enhanced the `Input` component by injecting utility classes (`px-4 py-2 text-sm placeholder:text-gray-500`) for improved spatial alignment, sizing, and placeholder legibility.
* **`SearchBar.svelte`:** Deleted the component entirely, including its localized debounce logic, state management, and DOM elements, to reduce technical debt and component duplication.
### Package Changes
_No response_
### Screenshots

### Reference
Resolves#241.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #249
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Make the number sync with real year number.
### Package Changes
_No response_
### Screenshots

### Reference
Resolves #
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #248
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Fix by using `Environment` attribute instead of from `window` api.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolve#238.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #239
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #234
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
**Description**
This PR introduces a comprehensive filtering and search mechanism for the blog post list. It replaces the simple search bar with a robust filtering dialog that supports both keyword search and label selection. To enhance the mobile user experience, the `vaul-svelte` library has been integrated to provide native-feeling drawer interactions. Additionally, the filtering state is now synchronized with URL query parameters, allowing for shareable filtered views.
**Key Changes**
* **Dependencies:**
* Added `vaul-svelte` (v1.0.0-next.7) for handling mobile drawer interactions.
* **UI Components:**
* Implemented a set of generic `Drawer` components (`drawer-content`, `drawer-overlay`, etc.) wrapping `vaul-svelte`.
* Renamed the existing application navigation drawer from `Drawer.svelte` to `NavigationDrawer.svelte` to avoid namespace conflicts.
* Updated `Footer.svelte` to use the semantic `<footer>` tag.
* **Filtering Feature:**
* **FilteringDialog:** Created a responsive form that renders as a `Dialog` on desktop and a `Drawer` on mobile. Includes inputs for keywords and a combobox for label selection.
* **FilteringButton:** Added a floating action button (FAB) that adjusts its position relative to the footer visibility. It displays a "rainbow ring" animation when active filters are applied.
* **PostOverallPage:** Replaced `SearchBar` with `FilteringDialog`. Implemented logic to sync form state with URL search parameters (`keyword`, `label_id`).
* **Data & Logic:**
* Updated `routes/post/+page.server.ts` and `+page.svelte` to parse query parameters and hydrate both post and label stores server-side.
* Added `setData` method to `PostsListedStore`.
* Updated `PostContentPage` to link post tags directly to the filtered list view.
**Testing & Review Notes**
1. **Functionality:**
* Navigate to the post list page. Verify that clicking the new floating search button opens the filtering modal.
* Test filtering by keyword, label, or both. Ensure the list updates correctly.
* Verify that applying a filter updates the URL (e.g., `?keyword=test&label_id=1`). Refreshing the page should persist the filtered state.
2. **Responsiveness:**
* **Desktop:** Ensure the filter opens as a centered Dialog.
* **Mobile:** Ensure the filter opens as a bottom-sheet Drawer.
3. **Regression:**
* Verify that the main navigation menu (`NavigationDrawer`) still functions correctly after the rename.
* Check that the footer layout remains stable.
### Package Changes
```
"vaul-svelte": "1.0.0-next.7",
```
### Screenshots
|Scenario|Mobile|Desktop|
|-|-|-|
|Inactive||<img width="1680" alt="截圖 2025-12-16 01.45.58.png" src="attachments/4b478cda-33d4-4a9a-beb4-9cd4e5795350">|
|Active||<img width="1680" alt="截圖 2025-12-16 01.43.57.png" src="attachments/30a02bbb-7915-484b-95cc-371e951c666d">|
|Configuration||<img width="1680" alt="截圖 2025-12-16 01.43.34.png" src="attachments/17fa73b6-5f87-4781-987b-484db5de7e91">|
### Reference
Resolves#199.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #231
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Summary
This PR introduces a new `script/toolbox` directory containing a Python-based utility service designed to handle background tasks, specifically generating an RSS 2.0 feed (`generate_rss.py`). It includes the necessary Docker infrastructure to build and deploy this toolbox alongside the existing services.
Additionally, this change refactors the location of maintenance shell scripts to `script/pre-commit/` for better organization and optimizes the CI/CD deployment workflow by implementing Docker layer caching and removing redundant frontend build steps.
#### Key Changes
##### Toolbox & RSS
* **New Service:** Added `script/toolbox/` containing a Python 3.14 environment, `Dockerfile`, and `requirements.txt`.
* **RSS Logic:** Implemented `generate_rss.py` to fetch published posts from the PostgreSQL database, scrape site metadata, and generate a `feed.xml` file.
* **Documentation:** Added `README.md` and `.env.example` for the toolbox service.
##### CI/CD & Infrastructure
* **Workflow Update (`deployment.yaml`):**
* Added a new job to build and push the `toolbox` Docker image.
* Enabled `cache-from` and `cache-to` (registry caching) for Frontend, Backend, and Toolbox images to speed up build times.
* **Frontend CI:** Removed the redundant `pnpm run build` step from `frontend-ci.yaml`.
##### Refactoring & UI
* **Script Reorganization:** Moved `backend-check.sh`, `frontend-lint.sh`, and `sqlx-prepare.sh` into `script/pre-commit/` and updated their internal path resolution.
* **Pre-commit:** Updated `.pre-commit-config.yaml` to reflect the new script locations.
* **Frontend UI:** Added an RSS icon link to `Footer.svelte` pointing to the generated feed.
#### Testing & Review Notes
1. **Toolbox Build:** Verify the `toolbox` image builds successfully:
```bash
cd script/toolbox
docker build -t blog-toolbox .
```
2. **RSS Generation:**
* Run the container locally with correct `.env` variables.
* Execute `python generate_rss.py` inside the container.
* Verify the output `feed.xml` validates against RSS 2.0 standards.
3. **Pre-commit Hooks:** Run `pre-commit run --all-files` to ensure the path refactoring hasn't broken local linting/checking.
4. **Deployment:** Monitor the next Gitea Action run to confirm registry caching is functioning correctly.
### Package Changes
```
python-dotenv==1.2.1
psycopg2-binary==2.9.11
requests==2.32.5
```
### Screenshots
> Screenshot at Feeder app on Android
<img src="/attachments/d791ac12-307c-43a7-a146-c423f2600c02" width="200px"/>
### Reference
Resolve#227.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #230
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
## 📝 Summary
This PR implements the ability to search/filter posts by keyword. It introduces a search bar on the frontend and passes the search query through the architectural layers to the backend, where a basic title-based filtering is currently applied.
## 🛠️ Changes
### Backend (`backend/feature/post`)
- **API & Controller**: Updated `PostController` and `PostQueryDto` to accept an optional `keyword` parameter.
- **Use Case**: Updated `GetAllPostInfoUseCase` to propagate the `keyword` to the repository.
- **Repository**:
- Updated `PostRepository` interface and implementation.
- **Logic**: Implemented a temporary in-memory filter that checks if the `post.title` contains the keyword.
- *Note*: Added a TODO to replace this string matching with vector/embedding search in the future.
### Frontend (`frontend/src/lib/post`)
- **UI Components**:
- Created a new component `SearchBar.svelte` with:
- Input debouncing (1000ms).
- Clear button functionality.
- Hover/Focus styling effects.
- Integrated `SearchBar` into `PostOverallPage.svelte`.
- **State Management**: Updated `PostsListedStore` to handle the `keyword` trigger.
- **Gateway/API**: Updated `PostListQueryDto` and `PostRepositoryImpl` to append the `keyword` to the URL search parameters when calling the backend.
## ⚠️ Technical Notes
- **Search Scope**: Currently, the search is **strictly limited to the post title**. The logic is explicitly filtering: `post.title.contains(keyword)`.
- **Performance**:
- The frontend implements a **1-second debounce** on the input to reduce unnecessary API calls.
- The backend filtering happens *after* fetching post info from the DB service (in `map` logic). As indicated in the code comments, this is a provisional solution pending a switch to embedding-based search.
### Package Changes
_No response_
### Screenshots


### Reference
Resolve#198.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #229
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
## Feature: Implement Image Gallery and Detail Views
### Summary
This PR introduces full functionality for listing images and viewing image details. It extends the backend to support retrieving image metadata without binary data and updates the frontend to display a gallery grid and a dedicated detail page. Additionally, it enforces absolute imports in the frontend codebase via updated ESLint rules.
### 🏗️ Backend Changes (`/backend`)
**New Endpoints**
* **`GET /image`**: Retrieves a list of all non-deleted images (metadata only: ID and MIME type).
* **`GET /image/{id}/info`**: Retrieves specific metadata for an image ID without fetching the binary blob.
**Architecture & Refactoring**
* **Domain Entities**: Split the concept of an image into `ImageInfo` (metadata) and `Image` (contains `ImageInfo` + binary data). This allows listing images efficiently without loading heavy binary data from storage.
* **Use Cases**: Added `ListImagesUseCase` and `GetImageInfoUseCase`.
* **Repository**: Updated `ImageRepository` and `ImageDbService` to support listing and metadata retrieval using new SQLx queries.
* **API**: Updated `openapi` documentation to include the new routes.
### 🖥️ Frontend Changes (`/frontend`)
**UI & Components**
* **Image Dashboard (`/dashboard/image`)**:
* Implemented a grid layout gallery.
* Added hover effects showing Image ID and MIME type.
* Added quick actions: **Copy URL** and **View Details**.
* **Image Details (`/dashboard/image/[id]`)**:
* Created a dedicated page to view full-size images.
* Displays a table with ID, MIME Type, and URL (with copy functionality).
* **Utilities**: Added `copyToClipboard.ts` utility with Toast notification integration.
**State Management**
* Added `ImagesListedStore` to handle fetching and state for the gallery.
* Added `ImageLoadedStore` to handle fetching state for the detail view.
* Updated `Container` to inject these new dependencies.
**Developer Experience (DX)**
* **ESLint**: Added `eslint-plugin-import`.
* **Rules**: Enforced absolute imports (`import/no-relative-parent-imports`) to maintain a cleaner project structure (CSS files are exempted).
-----
### 🔍 Technical Details
#### 1\. Optimization: Splitting Metadata vs. Data
To prevent high memory usage and latency when listing images, the backend now distinguishes between fetching *info* and fetching *blobs*.
#### 2\. ESLint Configuration
New rules now prevent `../../` spaghetti imports in TypeScript files.
```javascript
// Invalid
import { foo } from '../../common/foo';
// Valid
import { foo } from '$lib/common/foo';
```
-----
### 🧪 Testing
1. **Gallery View**:
* Navigate to `/dashboard/image`.
* Verify that previously uploaded images appear in the grid.
* Hover over an image and click the "Copy" icon; verify the URL is in your clipboard.
2. **Detail View**:
* Click the "Eye" icon on an image card.
* Verify redirection to `/dashboard/image/[id]`.
* Verify the image loads and metadata matches.
3. **Upload**:
* Upload a new image via the dashboard.
* Verify the list updates automatically upon success.
### Package Changes
"eslint-plugin-import": "^2.32.0",
### Screenshots


### Reference
Resolve#158.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #228
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #225
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Make structured data be able to be passed different type of data.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#202.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #222
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Merge [logo-light.svg](/frontend/static/icon/logo-light.svg) and [logo-dark.svg](/frontend/static/icon/logo-light.svg) as a single [favicon.svg](/frontend/static/favicon.svg)
### Package Changes
_No response_
### Screenshots
|Dark Mode|Light Mode|
|-|-|
|||
### Reference
Resolves#219.
https://gemini.google.com/share/75ba68b088e3
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #221
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #218
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>