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 - pinned
- security - security
- enhancement - enhancement
- feature-request
- bug - bug
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: wontfix staleLabel: wontfix

View File

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

View File

@@ -1,3 +1,3 @@
wrapperVersion=3.3.4 wrapperVersion=3.3.4
distributionType=only-script 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": { "formatter": {
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 4, "indentWidth": 4,

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -13,9 +13,9 @@
<properties> <properties>
<!-- renovate: datasource=node-version depName=node --> <!-- 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 --> <!-- renovate: datasource=npm depName=npm -->
<npm.version>11.10.0</npm.version> <npm.version>11.11.1</npm.version>
</properties> </properties>
<build> <build>
@@ -72,7 +72,7 @@
</plugin> </plugin>
<plugin> <plugin>
<artifactId>maven-resources-plugin</artifactId> <artifactId>maven-resources-plugin</artifactId>
<version>3.4.0</version> <version>3.5.0</version>
<executions> <executions>
<execution> <execution>
<id>copy web interface to resources</id> <id>copy web interface to resources</id>
@@ -94,4 +94,49 @@
</plugin> </plugin>
</plugins> </plugins>
</build> </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> </project>

View File

@@ -1,5 +1,5 @@
import { configureStore } from "@reduxjs/toolkit" 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 { entriesSlice } from "@/app/entries/slice"
import { redirectSlice } from "@/app/redirect/slice" import { redirectSlice } from "@/app/redirect/slice"
import { serverSlice } from "@/app/server/slice" import { serverSlice } from "@/app/server/slice"
@@ -41,3 +41,4 @@ export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector 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: { badge: {
width: "3.2rem", width: "3.2rem",
// for some reason, mantine Badge has "cursor: 'default'" // 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 const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
return ( return (
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}> <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> <Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
{count} {count}
</Badge> </Badge>

View File

@@ -8,9 +8,11 @@ import { Constants } from "@/app/constants"
import type { EntrySourceType } from "@/app/entries/slice" import type { EntrySourceType } from "@/app/entries/slice"
import { loadEntries } from "@/app/entries/thunks" import { loadEntries } from "@/app/entries/thunks"
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "@/app/redirect/thunks" import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "@/app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store" import { useAppDispatch, useAppSelector, useShallowEqualAppSelector } from "@/app/store"
import { flattenCategoryTree } from "@/app/utils" import { categoryHasNewEntries, categoryUnreadCount, flattenCategoryTree } from "@/app/utils"
import { FeedEntries } from "@/components/content/FeedEntries" import { FeedEntries } from "@/components/content/FeedEntries"
import { UnreadCount } from "@/components/sidebar/UnreadCount"
import { useMobile } from "@/hooks/useMobile"
import { tss } from "@/tss" import { tss } from "@/tss"
function NoSubscriptionHelp() { function NoSubscriptionHelp() {
@@ -33,6 +35,12 @@ const useStyles = tss.create(() => ({
sourceWebsiteLink: { sourceWebsiteLink: {
color: "inherit", color: "inherit",
textDecoration: "none", 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 sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl) const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl)
const hasMore = useAppSelector(state => state.entries.hasMore) 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() const dispatch = useAppDispatch()
let title: React.ReactNode = sourceLabel let title: React.ReactNode = sourceLabel
@@ -89,16 +124,23 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
return ( 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 // 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}> <Box mb={viewport.height * 0.7}>
<Group gap="xl" className="cf-entries-title"> <Group className="cf-entries-title" wrap="nowrap">
{sourceWebsiteUrl && ( {sourceWebsiteUrl && (
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}> <a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
<Title order={3}>{title}</Title> <Title order={3} className={classes.titleText}>
{title}
</Title>
</a> </a>
)} )}
{!sourceWebsiteUrl && <Title order={3}>{title}</Title>} {!sourceWebsiteUrl && (
<Title order={3} className={classes.titleText}>
{title}
</Title>
)}
<ActionIcon onClick={titleClicked} variant="subtle" color={theme.primaryColor}> <ActionIcon onClick={titleClicked} variant="subtle" color={theme.primaryColor}>
<TbEdit size={18} /> <TbEdit size={18} />
</ActionIcon> </ActionIcon>
{showUnreadCount && <UnreadCount unreadCount={unreadCount} showIndicator={hasNewEntries} />}
</Group> </Group>
<FeedEntries /> <FeedEntries />

View File

@@ -1,25 +1,17 @@
import { lingui } from "@lingui/vite-plugin" 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 { defineConfig } from "vite"
import checker from "vite-plugin-checker" import checker from "vite-plugin-checker"
import tsconfigPaths from "vite-tsconfig-paths"
// https://vitejs.dev/config/
export default defineConfig(() => ({ export default defineConfig(() => ({
plugins: [ plugins: [
react({ react(),
babel: { babel({
plugins: [ presets: [reactCompilerPreset()],
// support for lingui macros plugins: ["@lingui/babel-plugin-lingui-macro"],
// needs to be before the react compiler plugin
"@lingui/babel-plugin-lingui-macro",
// react compiler
["babel-plugin-react-compiler", { target: "19" }],
],
},
}), }),
lingui(), lingui(),
tsconfigPaths(),
checker({ checker({
typescript: true, typescript: true,
biome: { biome: {
@@ -43,22 +35,32 @@ export default defineConfig(() => ({
"/logout": "http://localhost:8083", "/logout": "http://localhost:8083",
}, },
}, },
build: { resolve: {
chunkSizeWarningLimit: 3500, tsconfigPaths: true,
rollupOptions: { },
output: { legacy: {
manualChunks: id => { // required for websocket-heartbeat-js
// output mantine as its own chunk because it is quite large inconsistentCjsInterop: true,
if (id.includes("@mantine")) {
return "mantine"
}
},
},
},
}, },
test: { test: {
environment: "jsdom", environment: "jsdom",
globals: true, globals: true,
setupFiles: "./src/setupTests.ts", 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> <packaging>quarkus</packaging>
<properties> <properties>
<quarkus.version>3.31.4</quarkus.version> <quarkus.version>3.32.4</quarkus.version>
<querydsl.version>7.1</querydsl.version> <querydsl.version>7.1</querydsl.version>
<rome.version>2.1.0</rome.version> <rome.version>2.1.0</rome.version>
@@ -29,6 +29,12 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </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> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -237,7 +243,7 @@
<dependency> <dependency>
<groupId>com.puppycrawl.tools</groupId> <groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId> <artifactId>checkstyle</artifactId>
<version>13.2.0</version> <version>13.3.0</version>
</dependency> </dependency>
</dependencies> </dependencies>
<executions> <executions>
@@ -266,7 +272,7 @@
<plugin> <plugin>
<groupId>com.diffplug.spotless</groupId> <groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId> <artifactId>spotless-maven-plugin</artifactId>
<version>3.2.1</version> <version>3.4.0</version>
<?m2e ignore?> <?m2e ignore?>
<executions> <executions>
<execution> <execution>
@@ -302,7 +308,7 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.42</version> <version>1.18.44</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -393,7 +399,7 @@
<dependency> <dependency>
<groupId>dev.cel</groupId> <groupId>dev.cel</groupId>
<artifactId>cel</artifactId> <artifactId>cel</artifactId>
<version>0.11.1</version> <version>0.12.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.passay</groupId> <groupId>org.passay</groupId>
@@ -428,7 +434,7 @@
<dependency> <dependency>
<groupId>com.ibm.icu</groupId> <groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId> <artifactId>icu4j</artifactId>
<version>78.2</version> <version>78.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.sourceforge.cssparser</groupId> <groupId>net.sourceforge.cssparser</groupId>
@@ -448,7 +454,7 @@
<dependency> <dependency>
<groupId>io.github.hakky54</groupId> <groupId>io.github.hakky54</groupId>
<artifactId>ayza-for-apache5</artifactId> <artifactId>ayza-for-apache5</artifactId>
<version>10.0.3</version> <version>10.0.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.brotli</groupId> <groupId>org.brotli</groupId>
@@ -465,7 +471,7 @@
<dependency> <dependency>
<groupId>io.quarkiverse.playwright</groupId> <groupId>io.quarkiverse.playwright</groupId>
<artifactId>quarkus-playwright</artifactId> <artifactId>quarkus-playwright</artifactId>
<version>2.3.2</version> <version>2.3.3</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

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

View File

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

View File

@@ -92,6 +92,12 @@ public interface CommaFeedConfiguration {
@ConfigDocSection @ConfigDocSection
Websocket websocket(); 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 { interface HttpClient {
/** /**
* User-Agent string that will be used by the http client, leave empty for the default one. * 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 * Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
* resources. * 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") @WithDefault("false")
boolean blockLocalAddresses(); 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.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -41,12 +42,12 @@ public class FeedRefreshEngine {
private final BlockingDeque<Feed> queue; private final BlockingDeque<Feed> queue;
private final ExecutorService feedProcessingLoopExecutor; private ExecutorService feedProcessingLoopExecutor;
private final ExecutorService refillLoopExecutor; private ExecutorService refillLoopExecutor;
private final ExecutorService refillExecutor; private ThreadPoolExecutor refillExecutor;
private final ThreadPoolExecutor workerExecutor; private ThreadPoolExecutor workerExecutor;
private final ThreadPoolExecutor databaseUpdaterExecutor; private ThreadPoolExecutor databaseUpdaterExecutor;
private final ThreadPoolExecutor notifierExecutor; private ThreadPoolExecutor notifierExecutor;
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater, public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) { FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) {
@@ -60,6 +61,15 @@ public class FeedRefreshEngine {
this.queue = new LinkedBlockingDeque<>(); 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.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
this.refillLoopExecutor = Executors.newSingleThreadExecutor(); this.refillLoopExecutor = Executors.newSingleThreadExecutor();
this.refillExecutor = newDiscardingSingleThreadExecutorService(); this.refillExecutor = newDiscardingSingleThreadExecutorService();
@@ -67,15 +77,10 @@ public class FeedRefreshEngine {
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads()); this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(), this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(),
config.pushNotifications().queueCapacity()); 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() { public void start() {
createExecutors();
startFeedProcessingLoop(); startFeedProcessingLoop();
startRefillLoop(); startRefillLoop();
} }
@@ -197,12 +202,14 @@ public class FeedRefreshEngine {
} }
public void stop() { public void stop() {
this.feedProcessingLoopExecutor.shutdownNow(); MoreExecutors.shutdownAndAwaitTermination(this.feedProcessingLoopExecutor, config.shutdownTimeout());
this.refillLoopExecutor.shutdownNow(); MoreExecutors.shutdownAndAwaitTermination(this.refillLoopExecutor, config.shutdownTimeout());
this.refillExecutor.shutdownNow(); MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout());
this.workerExecutor.shutdownNow(); MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout());
this.databaseUpdaterExecutor.shutdownNow(); MoreExecutors.shutdownAndAwaitTermination(this.databaseUpdaterExecutor, config.shutdownTimeout());
this.notifierExecutor.shutdownNow(); MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout());
queue.clear();
} }
/** /**

View File

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

View File

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

View File

@@ -6,24 +6,32 @@ import java.util.concurrent.ScheduledExecutorService;
import jakarta.inject.Singleton; 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 @Singleton
public class TaskScheduler { public class TaskScheduler {
private final List<ScheduledTask> tasks; 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.tasks = tasks;
this.executor = Executors.newScheduledThreadPool(tasks.size()); this.config = config;
} }
public void start() { 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() { 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 profile overrides
%test.quarkus.log.category."org.mockserver".level=WARN %test.quarkus.log.category."org.mockserver".level=WARN
%test.quarkus.log.category."liquibase".level=WARN %test.quarkus.log.category."liquibase".level=WARN
%test.commafeed.shutdown-timeout=100ms
%test.commafeed.users.create-demo-account=true %test.commafeed.users.create-demo-account=true
%test.commafeed.users.allow-registrations=true %test.commafeed.users.allow-registrations=true
%test.commafeed.password-recovery-enabled=true %test.commafeed.password-recovery-enabled=true

View File

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