Compare commits

..

90 Commits

Author SHA1 Message Date
renovate[bot]
fc76d7e609 fix(deps): update dependency @rolldown/plugin-babel to ^0.2.2 2026-03-21 08:51:10 +00:00
renovate[bot]
1b24bf33ed fix(deps): update dependency com.google.protobuf:protobuf-java to v4.34.1 2026-03-20 18:02:04 +00:00
renovate[bot]
58ff378735 fix(deps): update dependency io.github.hakky54:ayza-for-apache5 to v10.0.4 2026-03-19 13:44:43 +00:00
Jérémie Panzer
1cee04a233 Merge pull request #2088 from Athou/renovate/com.diffplug.spotless-spotless-maven-plugin-3.x
chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v3.4.0
2026-03-19 00:03:44 +01:00
renovate[bot]
ac11a0efb8 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v3.4.0 2026-03-18 21:29:01 +00:00
renovate[bot]
f2ea1e3f7a fix(deps): update dependency io.quarkus.platform:quarkus-bom to v3.32.4 2026-03-18 18:07:29 +00:00
Jérémie Panzer
153970c146 Merge pull request #2087 from Athou/renovate/jsdom-29.x
chore(deps): update dependency jsdom to v29
2026-03-18 13:46:24 +01:00
renovate[bot]
c21287e642 chore(deps): update dependency jsdom to v29 2026-03-18 12:13:28 +00:00
Athou
284942a82e add feature-request to the list of exempted labels 2026-03-18 13:00:44 +01:00
Jérémie Panzer
e796916e73 Merge pull request #2085 from Athou/renovate/com.ibm.icu-icu4j-78.x
fix(deps): update dependency com.ibm.icu:icu4j to v78.3
2026-03-18 12:42:41 +01:00
renovate[bot]
004db1762c fix(deps): update dependency com.ibm.icu:icu4j to v78.3 2026-03-17 22:35:16 +00:00
renovate[bot]
8e05ba8820 chore(deps): update dependency lint-staged to ^16.4.0 (#2083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 20:33:50 +00:00
renovate[bot]
f4f386b5e5 chore(deps): update dependency lint-staged to ^16.3.4 2026-03-17 09:01:31 +00:00
Jérémie Panzer
ecbf8bec23 Merge pull request #2082 from Athou/renovate/debian-13.x
chore(deps): update debian docker tag to v13.4
2026-03-17 07:29:17 +01:00
renovate[bot]
5d8c09ccda chore(deps): update debian docker tag to v13.4 2026-03-17 00:35:37 +00:00
renovate[bot]
47c5c3d8a0 chore(deps): update dependency io.quarkiverse.playwright:quarkus-playwright to v2.3.3 2026-03-16 21:27:52 +00:00
renovate[bot]
3ddce16d5b chore(deps): update graalvm/setup-graalvm digest to 03e8abf 2026-03-16 18:35:24 +00:00
renovate[bot]
acefdc44d9 chore(deps): lock file maintenance 2026-03-16 01:07:15 +00:00
renovate[bot]
d05c5b9d7f chore(deps): update dependency npm to v11.11.1 2026-03-15 04:53:39 +00:00
renovate[bot]
5d1237d1f4 chore(deps): update ncipollo/release-action digest to 339a818 2026-03-15 01:34:06 +00:00
Athou
6bac8631ac output mantine as its own chunk because it is quite large 2026-03-14 20:20:51 +01:00
Athou
2c803327ad upgrade to vite 8 2026-03-13 22:35:20 +01:00
renovate[bot]
451ae5bc51 chore(deps): update dependency lint-staged to ^16.3.3 2026-03-13 05:46:24 +00:00
renovate[bot]
f41ce8c878 chore(deps): update graalvm/setup-graalvm digest to 6e327d2 2026-03-12 19:12:27 +00:00
renovate[bot]
177c54c813 chore(deps): update dependency maven to v3.9.14 2026-03-12 14:46:09 +00:00
Jérémie Panzer
3765e32fd4 Merge pull request #2079 from Athou/renovate/protobuf-monorepo
fix(deps): update dependency com.google.protobuf:protobuf-java to v4.34.0
2026-03-12 08:19:25 +01:00
renovate[bot]
aab7a16d18 fix(deps): update dependency com.google.protobuf:protobuf-java to v4.34.0 2026-03-12 06:26:40 +00:00
Athou
f2463af63c cel update 2026-03-12 07:25:36 +01:00
renovate[bot]
212c2c3b56 fix(deps): update quarkus.version to v3.32.3 2026-03-11 22:48:05 +00:00
renovate[bot]
3ab00b0cdd chore(deps): update actions/download-artifact digest to 3e5f45b 2026-03-11 17:53:34 +00:00
renovate[bot]
fad85e9299 fix(deps): update dependency org.projectlombok:lombok to v1.18.44 2026-03-11 02:48:12 +00:00
renovate[bot]
3cd5e203e2 fix(deps): update mantine monorepo to ^8.3.16 (#2076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 08:40:30 +00:00
Jérémie Panzer
7b081fa870 Merge pull request #2077 from Athou/renovate/biomejs-biome-2.4.x
chore(deps): update dependency @biomejs/biome to v2.4.6
2026-03-09 09:08:37 +01:00
renovate[bot]
5ce22051d4 chore(deps): update dependency @biomejs/biome to v2.4.6 2026-03-09 07:19:20 +00:00
Jérémie Panzer
1e59489fa5 Merge pull request #2078 from Athou/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2026-03-09 08:18:42 +01:00
renovate[bot]
1e691b1255 chore(deps): lock file maintenance 2026-03-09 01:10:36 +00:00
Jérémie Panzer
61e1bef63f Merge pull request #2074 from Athou/renovate/maven-3.9.x
chore(deps): update dependency maven to v3.9.13
2026-03-07 03:45:01 +01:00
renovate[bot]
46dbb78fbf chore(deps): update dependency maven to v3.9.13 2026-03-07 01:16:46 +00:00
renovate[bot]
99890707b3 chore(deps): update dependency lint-staged to ^16.3.2 2026-03-06 21:20:05 +00:00
Jérémie Panzer
f54c841fdc Merge pull request #2073 from Athou/renovate/react-icons-5.x
fix(deps): update dependency react-icons to ^5.6.0
2026-03-06 17:37:19 +01:00
renovate[bot]
e2a6009ee9 fix(deps): update dependency react-icons to ^5.6.0 2026-03-06 10:39:30 +00:00
Jérémie Panzer
a34dd15040 Merge pull request #2072 from Athou/renovate/docker-build-push-action-7.x
chore(deps): update docker/build-push-action action to v7
2026-03-06 07:20:22 +01:00
renovate[bot]
24c934003d chore(deps): update docker/build-push-action action to v7 2026-03-05 21:51:18 +00:00
renovate[bot]
5300e1a245 chore(deps): update dependency @biomejs/biome to v2.4.5 2026-03-05 18:43:33 +00:00
Jérémie Panzer
90381c670b Merge pull request #2071 from Athou/renovate/org.apache.maven.plugins-maven-resources-plugin-3.x
chore(deps): update dependency org.apache.maven.plugins:maven-resources-plugin to v3.5.0
2026-03-05 14:08:00 +01:00
renovate[bot]
23edea93db chore(deps): update dependency org.apache.maven.plugins:maven-resources-plugin to v3.5.0 2026-03-05 12:25:30 +00:00
Jérémie Panzer
a51712e363 Merge pull request #2070 from Athou/renovate/docker-setup-buildx-action-4.x
chore(deps): update docker/setup-buildx-action action to v4
2026-03-05 13:25:03 +01:00
renovate[bot]
4310e979e1 chore(deps): update docker/setup-buildx-action action to v4 2026-03-05 09:47:18 +00:00
renovate[bot]
f690b76d87 fix(deps): update quarkus.version to v3.32.2 2026-03-05 01:25:51 +00:00
Jérémie Panzer
ac29594b67 Merge pull request #2069 from Athou/renovate/docker-setup-qemu-action-4.x
chore(deps): update docker/setup-qemu-action action to v4
2026-03-04 15:29:50 +01:00
renovate[bot]
93f535bb87 chore(deps): update docker/setup-qemu-action action to v4 2026-03-04 13:38:20 +00:00
Jérémie Panzer
076eb3cf42 Merge pull request #2068 from Athou/renovate/docker-login-action-4.x
chore(deps): update docker/login-action action to v4
2026-03-04 14:35:47 +01:00
Athou
7b2e0fffbd reduce timeout for tests to speed up shutdown 2026-03-04 14:19:56 +01:00
renovate[bot]
8eaab0dbc3 chore(deps): update docker/login-action action to v4 2026-03-04 12:59:09 +00:00
Athou
eaa5bc896e stop the application between tests to make sure that there are no active transactions when we truncate the tables 2026-03-04 13:58:06 +01:00
renovate[bot]
42d1db5fc3 chore(deps): update dependency lint-staged to ^16.3.1 2026-03-04 12:09:08 +00:00
Athou
78c017ddaf wait for tasks to complete when shutting down 2026-03-04 12:49:06 +01:00
renovate[bot]
231551d743 chore(deps): update dependency lint-staged to ^16.3.0 (#2067)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 21:56:21 +00:00
Jérémie Panzer
d0984eaba7 Merge pull request #2066 from Athou/renovate/com.diffplug.spotless-spotless-maven-plugin-3.x
chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v3.3.0
2026-03-03 06:05:51 +01:00
renovate[bot]
15854a72d1 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v3.3.0 2026-03-03 01:56:14 +00:00
renovate[bot]
7fd2bf0eda fix(deps): update dependency axios to ^1.13.6 2026-03-02 18:15:22 +00:00
renovate[bot]
6a10a2167e chore(deps): lock file maintenance 2026-03-02 01:25:14 +00:00
Jérémie Panzer
1ba99d255c Merge pull request #2063 from Athou/renovate/npm-11.x
chore(deps): update dependency npm to v11.11.0
2026-03-01 00:43:13 +01:00
renovate[bot]
ff1c2947b6 chore(deps): update dependency npm to v11.11.0 2026-02-28 23:17:20 +00:00
Jérémie Panzer
a690d2e0db Merge pull request #2062 from Athou/renovate/com.puppycrawl.tools-checkstyle-13.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.3.0
2026-03-01 00:16:55 +01:00
renovate[bot]
a3df327396 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.3.0 2026-02-28 20:52:06 +00:00
Jérémie Panzer
ede0016d8e Merge pull request #2060 from Athou/renovate/major-github-artifact-actions
chore(deps): update github artifact actions (major)
2026-02-26 22:57:55 +01:00
renovate[bot]
c19b091795 chore(deps): update github artifact actions 2026-02-26 20:39:27 +00:00
renovate[bot]
9e62c8b9f3 fix(deps): update linguijs monorepo to ^5.9.2 2026-02-26 20:39:23 +00:00
renovate[bot]
9f30dc181c fix(deps): update dependency react-router-dom to ^7.13.1 2026-02-26 18:02:38 +00:00
Jérémie Panzer
37fe44f860 Merge pull request #2059 from Athou/renovate/quarkus.version
fix(deps): update quarkus.version to v3.32.1 (minor)
2026-02-25 20:23:16 +01:00
renovate[bot]
68aaab8467 fix(deps): update quarkus.version to v3.32.1 2026-02-25 16:48:25 +00:00
renovate[bot]
b951ed1fcd chore(deps): update debian:13.3 docker digest to 3615a74 2026-02-24 21:53:17 +00:00
Jérémie Panzer
9bd9dc568a Merge pull request #2058 from Athou/renovate/node-24.x
chore(deps): update node.js to v24.14.0
2026-02-24 21:18:33 +01:00
renovate[bot]
29e4356fee chore(deps): update node.js to v24.14.0 2026-02-24 17:22:57 +00:00
renovate[bot]
0ed31eaa99 chore(deps): update dependency @biomejs/biome to v2.4.4 2026-02-23 21:56:45 +00:00
Jérémie Panzer
1e9869b217 Merge pull request #2057 from Athou/renovate/npm-11.10.x
chore(deps): update dependency npm to v11.10.1
2026-02-23 07:21:35 +01:00
renovate[bot]
78cfc2c827 chore(deps): update dependency npm to v11.10.1 2026-02-23 05:33:44 +00:00
renovate[bot]
a6e5a0d125 chore(deps): lock file maintenance 2026-02-23 00:55:55 +00:00
renovate[bot]
ea13aecd27 chore(deps): update dependency @biomejs/biome to v2.4.3 2026-02-22 21:47:30 +00:00
Athou
d838e8f28f fix occasional flicker 2026-02-22 14:36:25 +01:00
Athou
c9a7b9e17c ensure the indicator is not shown above the app header 2026-02-22 11:47:13 +01:00
Athou
8fe2d0bc0e fix typo 2026-02-21 23:58:49 +01:00
Athou
71e2f1e1e6 kill biome on build to prevent unlink errors on Windows 2026-02-21 23:55:08 +01:00
Athou
1ce9d1b9b2 prevent title line wrapping 2026-02-21 23:39:38 +01:00
Athou
b3d6ae467f reduce code duplication 2026-02-21 23:24:03 +01:00
Athou
da8d720dc4 don't show a pointer on hover 2026-02-21 22:58:49 +01:00
Athou
824c38f8ce show unread count on mobile only 2026-02-21 22:52:32 +01:00
Athou
b0579a70d8 Merge branch 'master' of https://github.com/lpoirothattermann/commafeed into lpoirothattermann-master 2026-02-21 22:28:03 +01:00
Louis POIROT--HATTERMANN
f9fe2d0976 feat: add unread count badge next to title 2026-02-21 18:54:23 +01:00
21 changed files with 1973 additions and 2489 deletions

1
.github/stale.yml vendored
View File

@@ -7,6 +7,7 @@ exemptLabels:
- pinned
- security
- enhancement
- feature-request
- bug
# Label to use when marking an issue as stale
staleLabel: wontfix

View File

@@ -29,7 +29,7 @@ jobs:
# Setup
- name: Set up GraalVM
uses: graalvm/setup-graalvm@54b4f5a65c1a84b2fdfdc2078fe43df32819e4b1 # v1
uses: graalvm/setup-graalvm@03e8abf916fd0e281b2efe7b2da3378bb0a1d085 # v1
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: "graalvm"
@@ -67,14 +67,14 @@ jobs:
# Upload artifacts
- name: Upload cross-platform app
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
with:
name: commafeed-${{ matrix.database }}-jvm
path: commafeed-server/target/commafeed-*.zip
- name: Upload native executable
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
path: commafeed-server/target/commafeed-*-runner*
@@ -104,17 +104,17 @@ jobs:
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Install required packages
run: sudo apt-get install -y rename unzip
# Prepare artifacts
- name: Download artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: commafeed-${{ matrix.database }}-*
path: ./artifacts
@@ -135,7 +135,7 @@ jobs:
# Docker
- name: Login to Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
if: ${{ env.DOCKERHUB_USERNAME != '' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -143,7 +143,7 @@ jobs:
## build but don't push for PRs and renovate
- name: Docker build - native
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.native
@@ -151,7 +151,7 @@ jobs:
platforms: linux/amd64,linux/arm64/v8
- name: Docker build - jvm
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
context: .
file: commafeed-server/src/main/docker/Dockerfile.jvm
@@ -160,7 +160,7 @@ jobs:
## build and push tag
- name: Docker build and push tag - native
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
if: ${{ github.ref_type == 'tag' }}
with:
context: .
@@ -172,7 +172,7 @@ jobs:
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
- name: Docker build and push tag - jvm
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
if: ${{ github.ref_type == 'tag' }}
with:
context: .
@@ -185,7 +185,7 @@ jobs:
## build and push master
- name: Docker build and push master - native
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
if: ${{ github.ref_name == 'master' }}
with:
context: .
@@ -195,7 +195,7 @@ jobs:
tags: athou/commafeed:master-${{ matrix.database }}
- name: Docker build and push master - jvm
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
if: ${{ github.ref_name == 'master' }}
with:
context: .
@@ -220,7 +220,7 @@ jobs:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: commafeed-*
path: ./artifacts
@@ -236,7 +236,7 @@ jobs:
version: ${{ github.ref_name }}
- name: Create GitHub release
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1
with:
name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }}

View File

@@ -1,3 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.2/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
"formatter": {
"indentStyle": "space",
"indentWidth": 4,

File diff suppressed because it is too large Load Diff

View File

@@ -18,19 +18,20 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@fontsource/open-sans": "^5.2.7",
"@lingui/core": "^5.9.1",
"@lingui/react": "^5.9.1",
"@mantine/core": "^8.3.15",
"@mantine/form": "^8.3.15",
"@mantine/hooks": "^8.3.15",
"@mantine/modals": "^8.3.15",
"@mantine/notifications": "^8.3.15",
"@mantine/spotlight": "^8.3.15",
"@lingui/core": "^5.9.3",
"@lingui/react": "^5.9.3",
"@mantine/core": "^8.3.16",
"@mantine/form": "^8.3.16",
"@mantine/hooks": "^8.3.16",
"@mantine/modals": "^8.3.16",
"@mantine/notifications": "^8.3.16",
"@mantine/spotlight": "^8.3.16",
"@monaco-editor/react": "^4.7.0",
"@react-querybuilder/mantine": "^8.14.0",
"@reduxjs/toolkit": "^2.11.2",
"axios": "^1.13.5",
"dayjs": "^1.11.19",
"@rolldown/plugin-babel": "^0.2.2",
"axios": "^1.13.6",
"dayjs": "^1.11.20",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.1",
"monaco-editor": "^0.55.1",
@@ -40,11 +41,11 @@
"react-contexify": "^6.0.0",
"react-dom": "^19.2.4",
"react-draggable": "^4.5.0",
"react-icons": "^5.5.0",
"react-icons": "^5.6.0",
"react-infinite-scroller": "^1.2.6",
"react-querybuilder": "^8.14.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.13.0",
"react-router-dom": "^7.13.1",
"react-swipeable": "^7.0.2",
"style-to-object": "^1.0.14",
"throttle-debounce": "^5.0.2",
@@ -53,10 +54,10 @@
"websocket-heartbeat-js": "^1.1.3"
},
"devDependencies": {
"@biomejs/biome": "^2.4.2",
"@lingui/babel-plugin-lingui-macro": "^5.9.1",
"@lingui/cli": "^5.9.1",
"@lingui/vite-plugin": "^5.9.1",
"@biomejs/biome": "^2.4.7",
"@lingui/babel-plugin-lingui-macro": "^5.9.3",
"@lingui/cli": "^5.9.3",
"@lingui/vite-plugin": "^5.9.3",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
@@ -66,16 +67,15 @@
"@types/react-infinite-scroller": "^1.2.5",
"@types/throttle-debounce": "^5.0.2",
"@types/tinycon": "^0.6.7",
"@vitejs/plugin-react": "^5.1.4",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-react-compiler": "1.0.0",
"husky": "^9.1.7",
"jsdom": "^28.1.0",
"lint-staged": "^16.2.7",
"jsdom": "^29.0.0",
"lint-staged": "^16.4.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite": "^8.0.0",
"vite-plugin-checker": "^0.12.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.18",
"vitest": "^4.1.0",
"yaml": "^2.8.2"
},
"overrides": {

View File

@@ -13,9 +13,9 @@
<properties>
<!-- renovate: datasource=node-version depName=node -->
<node.version>v24.13.1</node.version>
<node.version>v24.14.0</node.version>
<!-- renovate: datasource=npm depName=npm -->
<npm.version>11.10.0</npm.version>
<npm.version>11.11.1</npm.version>
</properties>
<build>
@@ -72,7 +72,7 @@
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.4.0</version>
<version>3.5.0</version>
<executions>
<execution>
<id>copy web interface to resources</id>
@@ -94,4 +94,49 @@
</plugin>
</plugins>
</build>
<profiles>
<!-- This profile is used to kill the Biome process on Windows -->
<!-- npm ci can fail if Biome is running (e.g., in the IDE) because it locks some files -->
<profile>
<id>kill-biome</id>
<activation>
<os>
<family>Windows</family>
</os>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.3</version>
<executions>
<execution>
<id>kill-biome</id>
<phase>initialize</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>taskkill</executable>
<arguments>
<argument>/F</argument>
<argument>/IM</argument>
<argument>biome.exe</argument>
</arguments>
<successCodes>
<successCode>0</successCode>
<!-- taskkill returns 128 if the process is not found, which is fine in this case -->
<successCode>128</successCode>
</successCodes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -1,5 +1,5 @@
import { configureStore } from "@reduxjs/toolkit"
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
import { shallowEqual, type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
import { entriesSlice } from "@/app/entries/slice"
import { redirectSlice } from "@/app/redirect/slice"
import { serverSlice } from "@/app/server/slice"
@@ -41,3 +41,4 @@ export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useShallowEqualAppSelector: TypedUseSelectorHook<RootState> = selector => useSelector(selector, shallowEqual)

View File

@@ -6,7 +6,11 @@ const useStyles = tss.create(() => ({
badge: {
width: "3.2rem",
// for some reason, mantine Badge has "cursor: 'default'"
cursor: "pointer",
cursor: "inherit",
},
indicator: {
// ensure the indicator is not shown above the app header
zIndex: 0,
},
}))
@@ -23,7 +27,15 @@ export function UnreadCount(
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
return (
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
<Indicator disabled={!props.showIndicator} size={4} offset={10} position="middle-start">
<Indicator
disabled={!props.showIndicator}
size={4}
offset={10}
position="middle-start"
classNames={{
indicator: classes.indicator,
}}
>
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
{count}
</Badge>

View File

@@ -8,9 +8,11 @@ import { Constants } from "@/app/constants"
import type { EntrySourceType } from "@/app/entries/slice"
import { loadEntries } from "@/app/entries/thunks"
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "@/app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store"
import { flattenCategoryTree } from "@/app/utils"
import { useAppDispatch, useAppSelector, useShallowEqualAppSelector } from "@/app/store"
import { categoryHasNewEntries, categoryUnreadCount, flattenCategoryTree } from "@/app/utils"
import { FeedEntries } from "@/components/content/FeedEntries"
import { UnreadCount } from "@/components/sidebar/UnreadCount"
import { useMobile } from "@/hooks/useMobile"
import { tss } from "@/tss"
function NoSubscriptionHelp() {
@@ -33,6 +35,12 @@ const useStyles = tss.create(() => ({
sourceWebsiteLink: {
color: "inherit",
textDecoration: "none",
overflow: "hidden",
},
titleText: {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
}))
@@ -48,6 +56,33 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl)
const hasMore = useAppSelector(state => state.entries.hasMore)
const mobile = useMobile()
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
const { unreadCount, hasNewEntries } = useShallowEqualAppSelector(state => {
const root = state.tree.rootCategory
if (!root) return { unreadCount: 0, hasNewEntries: false }
if (props.sourceType === "category") {
const category = id === Constants.categories.all.id ? root : flattenCategoryTree(root).find(c => c.id === id)
return {
unreadCount: categoryUnreadCount(category),
hasNewEntries: categoryHasNewEntries(category),
}
}
if (props.sourceType === "feed") {
const feed = flattenCategoryTree(root)
.flatMap(c => c.feeds)
.find(f => f.id === +id)
return {
unreadCount: feed?.unread ?? 0,
hasNewEntries: !!feed?.hasNewEntries,
}
}
return { unreadCount: 0, hasNewEntries: false }
})
const showUnreadCount = mobile || !sidebarVisible
const dispatch = useAppDispatch()
let title: React.ReactNode = sourceLabel
@@ -89,16 +124,23 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
return (
// add some room at the bottom of the page in order to be able to scroll the current entry at the top of the page when expanding
<Box mb={viewport.height * 0.7}>
<Group gap="xl" className="cf-entries-title">
<Group className="cf-entries-title" wrap="nowrap">
{sourceWebsiteUrl && (
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
<Title order={3}>{title}</Title>
<Title order={3} className={classes.titleText}>
{title}
</Title>
</a>
)}
{!sourceWebsiteUrl && <Title order={3}>{title}</Title>}
{!sourceWebsiteUrl && (
<Title order={3} className={classes.titleText}>
{title}
</Title>
)}
<ActionIcon onClick={titleClicked} variant="subtle" color={theme.primaryColor}>
<TbEdit size={18} />
</ActionIcon>
{showUnreadCount && <UnreadCount unreadCount={unreadCount} showIndicator={hasNewEntries} />}
</Group>
<FeedEntries />

View File

@@ -1,25 +1,17 @@
import { lingui } from "@lingui/vite-plugin"
import react from "@vitejs/plugin-react"
import babel from "@rolldown/plugin-babel"
import react, { reactCompilerPreset } from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import checker from "vite-plugin-checker"
import tsconfigPaths from "vite-tsconfig-paths"
// https://vitejs.dev/config/
export default defineConfig(() => ({
plugins: [
react({
babel: {
plugins: [
// support for lingui macros
// needs to be before the react compiler plugin
"@lingui/babel-plugin-lingui-macro",
// react compiler
["babel-plugin-react-compiler", { target: "19" }],
],
},
react(),
babel({
presets: [reactCompilerPreset()],
plugins: ["@lingui/babel-plugin-lingui-macro"],
}),
lingui(),
tsconfigPaths(),
checker({
typescript: true,
biome: {
@@ -43,22 +35,32 @@ export default defineConfig(() => ({
"/logout": "http://localhost:8083",
},
},
build: {
chunkSizeWarningLimit: 3500,
rollupOptions: {
output: {
manualChunks: id => {
// output mantine as its own chunk because it is quite large
if (id.includes("@mantine")) {
return "mantine"
}
},
},
},
resolve: {
tsconfigPaths: true,
},
legacy: {
// required for websocket-heartbeat-js
inconsistentCjsInterop: true,
},
test: {
environment: "jsdom",
globals: true,
setupFiles: "./src/setupTests.ts",
},
build: {
chunkSizeWarningLimit: 4000,
rolldownOptions: {
output: {
codeSplitting: {
groups: [
// output mantine as its own chunk because it is quite large
{
name: "mantine",
test: "@mantine",
},
],
},
},
},
},
}))

View File

@@ -13,7 +13,7 @@
<packaging>quarkus</packaging>
<properties>
<quarkus.version>3.31.4</quarkus.version>
<quarkus.version>3.32.4</quarkus.version>
<querydsl.version>7.1</querydsl.version>
<rome.version>2.1.0</rome.version>
@@ -29,6 +29,12 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- the quarkus bom declares a dependency on an old version of protobuf, we need to override it for cel-java -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.34.1</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -237,7 +243,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>13.2.0</version>
<version>13.3.0</version>
</dependency>
</dependencies>
<executions>
@@ -266,7 +272,7 @@
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>3.2.1</version>
<version>3.4.0</version>
<?m2e ignore?>
<executions>
<execution>
@@ -302,7 +308,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
<version>1.18.44</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -393,7 +399,7 @@
<dependency>
<groupId>dev.cel</groupId>
<artifactId>cel</artifactId>
<version>0.11.1</version>
<version>0.12.0</version>
</dependency>
<dependency>
<groupId>org.passay</groupId>
@@ -428,7 +434,7 @@
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>78.2</version>
<version>78.3</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cssparser</groupId>
@@ -448,7 +454,7 @@
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>ayza-for-apache5</artifactId>
<version>10.0.3</version>
<version>10.0.4</version>
</dependency>
<dependency>
<groupId>org.brotli</groupId>
@@ -465,7 +471,7 @@
<dependency>
<groupId>io.quarkiverse.playwright</groupId>
<artifactId>quarkus-playwright</artifactId>
<version>2.3.2</version>
<version>2.3.3</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -1,4 +1,4 @@
FROM debian:13.3@sha256:2c91e484d93f0830a7e05a2b9d92a7b102be7cab562198b984a84fdbc7806d91
FROM debian:13.4@sha256:55a15a112b42be10bfc8092fcc40b6748dc236f7ef46a358d9392b339e9d60e8
ARG TARGETARCH
EXPOSE 8082

View File

@@ -10,7 +10,9 @@ import com.commafeed.security.password.PasswordConstraintValidator;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
@RequiredArgsConstructor
public class CommaFeedApplication {
@@ -20,6 +22,8 @@ public class CommaFeedApplication {
private final CommaFeedConfiguration config;
public void start(@Observes StartupEvent ev) {
log.info("starting up...");
PasswordConstraintValidator.setMinimumPasswordLength(config.users().minimumPasswordLength());
feedRefreshEngine.start();
@@ -27,6 +31,8 @@ public class CommaFeedApplication {
}
public void stop(@Observes ShutdownEvent ev) {
log.info("shutting down...");
feedRefreshEngine.stop();
taskScheduler.stop();
}

View File

@@ -92,6 +92,12 @@ public interface CommaFeedConfiguration {
@ConfigDocSection
Websocket websocket();
/**
* Duration to wait for the feed refresh engine and the task scheduler to stop when the application is shutting down.
*/
@WithDefault("2s")
Duration shutdownTimeout();
interface HttpClient {
/**
* User-Agent string that will be used by the http client, leave empty for the default one.
@@ -144,7 +150,7 @@ public interface CommaFeedConfiguration {
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
* resources.
*
* You may want to enable this if you host a public instance of CommaFeed with regisration open.
* You may want to enable this if you host a public instance of CommaFeed with registrations open.
*/
@WithDefault("false")
boolean blockLocalAddresses();

View File

@@ -24,6 +24,7 @@ import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
@@ -41,12 +42,12 @@ public class FeedRefreshEngine {
private final BlockingDeque<Feed> queue;
private final ExecutorService feedProcessingLoopExecutor;
private final ExecutorService refillLoopExecutor;
private final ExecutorService refillExecutor;
private final ThreadPoolExecutor workerExecutor;
private final ThreadPoolExecutor databaseUpdaterExecutor;
private final ThreadPoolExecutor notifierExecutor;
private ExecutorService feedProcessingLoopExecutor;
private ExecutorService refillLoopExecutor;
private ThreadPoolExecutor refillExecutor;
private ThreadPoolExecutor workerExecutor;
private ThreadPoolExecutor databaseUpdaterExecutor;
private ThreadPoolExecutor notifierExecutor;
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) {
@@ -60,6 +61,15 @@ public class FeedRefreshEngine {
this.queue = new LinkedBlockingDeque<>();
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) () -> workerExecutor.getActiveCount());
metrics.register(MetricRegistry.name(getClass(), "updater", "active"),
(Gauge<Integer>) () -> databaseUpdaterExecutor.getActiveCount());
metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge<Integer>) () -> notifierExecutor.getActiveCount());
metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge<Integer>) () -> notifierExecutor.getQueue().size());
}
private void createExecutors() {
this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
this.refillLoopExecutor = Executors.newSingleThreadExecutor();
this.refillExecutor = newDiscardingSingleThreadExecutorService();
@@ -67,15 +77,10 @@ public class FeedRefreshEngine {
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(),
config.pushNotifications().queueCapacity());
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) workerExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge<Integer>) databaseUpdaterExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge<Integer>) notifierExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge<Integer>) () -> notifierExecutor.getQueue().size());
}
public void start() {
createExecutors();
startFeedProcessingLoop();
startRefillLoop();
}
@@ -197,12 +202,14 @@ public class FeedRefreshEngine {
}
public void stop() {
this.feedProcessingLoopExecutor.shutdownNow();
this.refillLoopExecutor.shutdownNow();
this.refillExecutor.shutdownNow();
this.workerExecutor.shutdownNow();
this.databaseUpdaterExecutor.shutdownNow();
this.notifierExecutor.shutdownNow();
MoreExecutors.shutdownAndAwaitTermination(this.feedProcessingLoopExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.refillLoopExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.databaseUpdaterExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout());
queue.clear();
}
/**

View File

@@ -54,10 +54,20 @@ public class DatabaseCleaningService {
int deleted;
long entriesTotal = 0;
do {
if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping cleanup of feeds without subscriptions");
return;
}
List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1));
for (Feed feed : feeds) {
long entriesDeleted;
do {
if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping cleanup of feeds without subscriptions");
return;
}
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
entriesDeletedMeter.mark(entriesDeleted);
entriesTotal += entriesDeleted;
@@ -76,6 +86,11 @@ public class DatabaseCleaningService {
long total = 0;
long deleted;
do {
if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping cleanup of contents without entries");
return;
}
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize));
total += deleted;
log.debug("removed {} contents without entries", total);
@@ -87,6 +102,11 @@ public class DatabaseCleaningService {
log.info("cleaning entries exceeding feed capacity");
long total = 0;
while (true) {
if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping cleanup of entries exceeding feed capacity");
return;
}
List<FeedCapacity> feeds = unitOfWork
.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries));
if (feeds.isEmpty()) {
@@ -97,6 +117,11 @@ public class DatabaseCleaningService {
long remaining = feed.capacity() - maxFeedCapacity;
int deleted;
do {
if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping cleanup of entries exceeding feed capacity");
return;
}
final long rem = remaining;
deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem), keepStarredEntries));
entriesDeletedMeter.mark(deleted);
@@ -114,6 +139,11 @@ public class DatabaseCleaningService {
long total = 0;
long deleted;
do {
if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping cleanup of old entries");
return;
}
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries));
entriesDeletedMeter.mark(deleted);
total += deleted;
@@ -127,6 +157,11 @@ public class DatabaseCleaningService {
long total = 0;
long deleted;
do {
if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping cleanup of old read statuses");
return;
}
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
total += deleted;
log.debug("removed {} old read statuses", total);
@@ -139,6 +174,11 @@ public class DatabaseCleaningService {
long total = 0;
long marked;
do {
if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping marking entries as read");
return;
}
marked = unitOfWork.call(() -> feedEntryStatusDAO.autoMarkAsRead(batchSize));
total += marked;
log.debug("marked {} entries as read", total);

View File

@@ -23,8 +23,8 @@ public abstract class ScheduledTask {
log.error(e.getMessage(), e);
}
};
log.info("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(), getTimeUnit(),
getInitialDelay(), getTimeUnit());
log.debug("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(),
getTimeUnit(), getInitialDelay(), getTimeUnit());
executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit());
}
}

View File

@@ -6,24 +6,32 @@ import java.util.concurrent.ScheduledExecutorService;
import jakarta.inject.Singleton;
import io.quarkus.arc.All;
import com.commafeed.CommaFeedConfiguration;
import com.google.common.util.concurrent.MoreExecutors;
import io.quarkus.arc.All;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class TaskScheduler {
private final List<ScheduledTask> tasks;
private final ScheduledExecutorService executor;
private final CommaFeedConfiguration config;
public TaskScheduler(@All List<ScheduledTask> tasks) {
private ScheduledExecutorService executor;
public TaskScheduler(@All List<ScheduledTask> tasks, CommaFeedConfiguration config) {
this.tasks = tasks;
this.executor = Executors.newScheduledThreadPool(tasks.size());
this.config = config;
}
public void start() {
tasks.forEach(task -> task.register(executor));
this.executor = Executors.newScheduledThreadPool(tasks.size());
this.tasks.forEach(task -> task.register(executor));
}
public void stop() {
executor.shutdownNow();
MoreExecutors.shutdownAndAwaitTermination(executor, config.shutdownTimeout());
}
}

View File

@@ -59,6 +59,7 @@ quarkus.native.additional-build-args=-H:PageSize=65536
# test profile overrides
%test.quarkus.log.category."org.mockserver".level=WARN
%test.quarkus.log.category."liquibase".level=WARN
%test.commafeed.shutdown-timeout=100ms
%test.commafeed.users.create-demo-account=true
%test.commafeed.users.allow-registrations=true
%test.commafeed.password-recovery-enabled=true

View File

@@ -6,6 +6,8 @@ import jakarta.persistence.EntityManager;
import org.hibernate.Session;
import org.kohsuke.MetaInfServices;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
@@ -17,12 +19,17 @@ public class DatabaseReset implements QuarkusTestBeforeEachCallback {
@Override
public void beforeEach(QuarkusTestMethodContext context) {
CDI.current()
.select(EntityManager.class)
.get()
.unwrap(Session.class)
.getSessionFactory()
.getSchemaManager()
.truncateMappedObjects();
// stop the application to make sure that there are no active transactions when we truncate the tables
getBean(CommaFeedApplication.class).stop(new ShutdownEvent());
// truncate all tables so that we have a clean slate for the next test
getBean(EntityManager.class).unwrap(Session.class).getSessionFactory().getSchemaManager().truncateMappedObjects();
// restart the application
getBean(CommaFeedApplication.class).start(new StartupEvent());
}
private static <T> T getBean(Class<T> clazz) {
return CDI.current().select(clazz).get();
}
}