Compare commits

...

38 Commits
6.1.1 ... 6.2.0

Author SHA1 Message Date
Athou
141a863079 release 6.2.0 2026-02-09 19:32:22 +01:00
Athou
6fa8d4be34 keep starred entries (#1581) 2026-02-09 06:59:41 +01:00
renovate[bot]
984e8a44d5 chore(deps): lock file maintenance 2026-02-09 01:33:44 +00:00
renovate[bot]
bdb296bce2 chore(deps): update dependency @types/react to ^19.2.13 2026-02-08 14:02:01 +00:00
Jérémie Panzer
955a9084c3 Merge pull request #2042 from Athou/renovate/npm-11.x
chore(deps): update dependency npm to v11.9.0
2026-02-08 05:23:05 +01:00
renovate[bot]
70f486b0eb chore(deps): update dependency npm to v11.9.0 2026-02-07 21:45:56 +00:00
renovate[bot]
0bc383c6a8 chore(deps): update dependency @types/react to ^19.2.11 2026-02-07 13:12:20 +00:00
renovate[bot]
0bb2b36585 chore(deps): update dependency @biomejs/biome to v2.3.14 2026-02-06 16:48:35 +00:00
Jérémie Panzer
9e3a24753a Merge pull request #2040 from Athou/renovate/com.puppycrawl.tools-checkstyle-13.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.2.0
2026-02-06 02:20:48 +01:00
renovate[bot]
f2c400799e chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.2.0 2026-02-05 21:38:36 +00:00
renovate[bot]
25a8c8a7e3 chore(deps): update dependency @vitejs/plugin-react to ^5.1.3 2026-02-05 10:32:18 +00:00
Jérémie Panzer
8f95d89fc6 Merge pull request #2039 from Athou/renovate/jsdom-28.x
chore(deps): update dependency jsdom to v28
2026-02-05 11:30:07 +01:00
renovate[bot]
39b0cdb9d5 chore(deps): update dependency jsdom to v28 2026-02-05 09:42:39 +00:00
renovate[bot]
42e06b848e fix(deps): update quarkus.version to v3.31.2 2026-02-04 17:56:35 +00:00
renovate[bot]
7c3a13b1c4 fix(deps): update mantine monorepo to ^8.3.14 2026-02-04 12:45:49 +00:00
Jérémie Panzer
151248fce2 Merge pull request #2038 from xmgz/master
Update gl messages.po
2026-02-04 07:13:01 +01:00
ghose
6e8d6fe063 Update gl messages.po
up to date gl translation
2026-02-04 04:09:30 +00:00
renovate[bot]
ca2da5e631 chore(deps): update actions/checkout digest to de0fac2 2026-02-03 16:36:24 +00:00
renovate[bot]
6cd3b70201 chore(deps): update debian:13.3 docker digest to 2c91e48 2026-02-03 14:08:51 +00:00
renovate[bot]
2dcfba75b5 chore(deps): update jaywcjlove/markdown-to-html-cli action to v5.0.4 2026-02-03 09:32:58 +00:00
Jérémie Panzer
44a51b03d3 Merge pull request #2037 from Athou/renovate/org.apache.maven.plugins-maven-compiler-plugin-3.x
chore(deps): update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.15.0
2026-02-02 06:14:39 +01:00
renovate[bot]
6ee9e9831e chore(deps): lock file maintenance 2026-02-02 01:56:05 +00:00
renovate[bot]
68c717cee8 chore(deps): update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.15.0 2026-02-01 21:36:26 +00:00
Jérémie Panzer
b15fc02c34 Merge pull request #2035 from Athou/renovate/com.puppycrawl.tools-checkstyle-13.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.1.0
2026-02-01 07:38:58 +01:00
renovate[bot]
033ebfb497 chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.1.0 2026-01-31 20:23:47 +00:00
renovate[bot]
4cceaa7650 fix(deps): update dependency axios to ^1.13.4 2026-01-30 21:51:52 +00:00
renovate[bot]
5df47f1396 chore(deps): update dependency @types/react to ^19.2.10 2026-01-30 12:33:18 +00:00
renovate[bot]
903f35c01b fix(deps): update react monorepo to ^19.2.4 2026-01-29 20:53:17 +00:00
renovate[bot]
6a34f94277 chore(deps): update dependency @biomejs/biome to v2.3.13 2026-01-29 17:09:33 +00:00
Athou
dcc143eb7d upgrade to quarkus 3.31 2026-01-28 18:04:16 +01:00
renovate[bot]
fb47bf27e8 fix(deps): update dependency axios to ^1.13.3 2026-01-28 16:39:42 +00:00
renovate[bot]
dcf969ff2e chore(deps): update docker/login-action digest to c94ce9f 2026-01-28 13:35:35 +00:00
renovate[bot]
32c1318355 chore(deps): update dependency com.diffplug.spotless:spotless-maven-plugin to v3.2.1 2026-01-27 22:36:42 +00:00
Jérémie Panzer
8ca6b89da4 Merge pull request #2033 from Athou/renovate/patch-react-router-monorepo
fix(deps): update dependency react-router-dom to ^7.13.0
2026-01-27 07:21:39 +01:00
renovate[bot]
b46c3a15f3 fix(deps): update dependency react-router-dom to ^7.13.0 2026-01-27 04:49:15 +00:00
renovate[bot]
cbc5e014f7 chore(deps): update dependency vite-tsconfig-paths to ^6.0.5 (#2032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 04:48:45 +00:00
renovate[bot]
8925b248e4 fix(deps): update linguijs monorepo to ^5.9.0 2026-01-26 22:16:14 +00:00
renovate[bot]
cc6aa2bbc5 chore(deps): update dependency @biomejs/biome to v2.3.12 2026-01-26 17:45:44 +00:00
16 changed files with 807 additions and 818 deletions

View File

@@ -23,7 +23,7 @@ jobs:
steps:
# Checkout
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
@@ -48,19 +48,19 @@ jobs:
run: mkdir -p target/pages/documentation/custom-css
- name: Convert readme file to html
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
uses: jaywcjlove/markdown-to-html-cli@cff9330af4ca8048b197a76d9eb1db189c2a7cee # v5.0.4
with:
source: README.md
output: target/pages/index.html
- name: Convert config documentation to html
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
uses: jaywcjlove/markdown-to-html-cli@cff9330af4ca8048b197a76d9eb1db189c2a7cee # v5.0.4
with:
source: commafeed-server/target/quarkus-generated-doc/config/commafeed-server.md
output: target/pages/documentation/index.html
- name: Convert custom css documentation to html
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
uses: jaywcjlove/markdown-to-html-cli@cff9330af4ca8048b197a76d9eb1db189c2a7cee # v5.0.4
with:
source: documentation/CUSTOMCSS.md
output: target/pages/documentation/custom-css/index.html
@@ -98,7 +98,7 @@ jobs:
steps:
# Checkout
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
@@ -135,7 +135,7 @@ jobs:
# Docker
- name: Login to Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
if: ${{ env.DOCKERHUB_USERNAME != '' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -215,7 +215,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
@@ -249,7 +249,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0

View File

@@ -1,5 +1,9 @@
# Changelog
## [6.2.0]
- Starred entries are no longer deleted after a certain amount of time, they are now kept indefinitely. The new `commafeed.database.cleanup.keep-starred-entries` setting can be disabled to restore the previous behavior if you want to keep deleting starred entries during normal entries cleanup (#1581)
## [6.1.1]
- Fix old starred entries not loading if they were marked as read (#2031)

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -17,31 +17,31 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@fontsource/open-sans": "^5.2.7",
"@lingui/core": "^5.8.0",
"@lingui/react": "^5.8.0",
"@mantine/core": "^8.3.13",
"@mantine/form": "^8.3.13",
"@mantine/hooks": "^8.3.13",
"@mantine/modals": "^8.3.13",
"@mantine/notifications": "^8.3.13",
"@mantine/spotlight": "^8.3.13",
"@lingui/core": "^5.9.0",
"@lingui/react": "^5.9.0",
"@mantine/core": "^8.3.14",
"@mantine/form": "^8.3.14",
"@mantine/hooks": "^8.3.14",
"@mantine/modals": "^8.3.14",
"@mantine/notifications": "^8.3.14",
"@mantine/spotlight": "^8.3.14",
"@monaco-editor/react": "^4.7.0",
"@reduxjs/toolkit": "^2.11.2",
"axios": "^1.13.2",
"axios": "^1.13.4",
"dayjs": "^1.11.19",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.1",
"monaco-editor": "^0.55.1",
"mousetrap": "^1.6.5",
"react": "^19.2.3",
"react": "^19.2.4",
"react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0",
"react-dom": "^19.2.3",
"react-dom": "^19.2.4",
"react-draggable": "^4.5.0",
"react-icons": "^5.5.0",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^9.2.0",
"react-router-dom": "^7.12.0",
"react-router-dom": "^7.13.0",
"react-swipeable": "^7.0.2",
"style-to-object": "^1.0.14",
"throttle-debounce": "^5.0.2",
@@ -50,32 +50,32 @@
"websocket-heartbeat-js": "^1.1.3"
},
"devDependencies": {
"@biomejs/biome": "^2.3.11",
"@lingui/babel-plugin-lingui-macro": "^5.8.0",
"@lingui/cli": "^5.8.0",
"@lingui/vite-plugin": "^5.8.0",
"@biomejs/biome": "^2.3.14",
"@lingui/babel-plugin-lingui-macro": "^5.9.0",
"@lingui/cli": "^5.9.0",
"@lingui/vite-plugin": "^5.9.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/mousetrap": "^1.6.15",
"@types/react": "^19.2.9",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@types/react-infinite-scroller": "^1.2.5",
"@types/throttle-debounce": "^5.0.2",
"@types/tinycon": "^0.6.7",
"@vitejs/plugin-react": "^5.1.2",
"@vitejs/plugin-react": "^5.1.3",
"babel-plugin-react-compiler": "1.0.0",
"jsdom": "^27.4.0",
"jsdom": "^28.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-checker": "^0.12.0",
"vite-tsconfig-paths": "^6.0.4",
"vite-tsconfig-paths": "^6.0.5",
"vitest": "^4.0.18",
"yaml": "^2.8.2"
},
"overrides": {
"react-infinite-scroller": {
"react": "^19.2.3"
"react": "^19.2.4"
}
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>6.1.1</version>
<version>6.2.0</version>
</parent>
<artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>
@@ -15,7 +15,7 @@
<!-- renovate: datasource=node-version depName=node -->
<node.version>v24.13.0</node.version>
<!-- renovate: datasource=npm depName=npm -->
<npm.version>11.8.0</npm.version>
<npm.version>11.9.0</npm.version>
</properties>
<build>

View File

@@ -9,7 +9,7 @@ msgstr ""
"Language: gl\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-12-27 15:30+0200\n"
"PO-Revision-Date: 2026-02-04 05:30+0200\n"
"Last-Translator: José M. <correoxm@disroot.org>\n"
"Language-Team: gl\n"
"Plural-Forms: \n"
@@ -64,7 +64,7 @@ msgstr "Administración"
#: src/pages/auth/InitialSetupPage.tsx
#: src/pages/auth/InitialSetupPage.tsx
msgid "Admin user name"
msgstr ""
msgstr "Identificador de admin"
#: src/components/content/add/CategorySelect.tsx
#: src/components/header/Header.tsx
@@ -231,7 +231,7 @@ msgstr "Confirmar contrasinal"
#: src/pages/auth/PasswordResetPage.tsx
#: src/pages/auth/PasswordResetPage.tsx
msgid "Confirm Password"
msgstr ""
msgstr "Confirmar contrasinal"
#: src/components/header/ProfileMenu.tsx
msgid "Cozy"
@@ -239,7 +239,7 @@ msgstr "Acolledor"
#: src/pages/auth/InitialSetupPage.tsx
msgid "Create Admin Account"
msgstr ""
msgstr "Crear conta Admin"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
@@ -496,11 +496,11 @@ msgstr "Índigo"
#: src/pages/auth/InitialSetupPage.tsx
msgid "Initial Setup"
msgstr ""
msgstr "Configuración inicial"
#: src/pages/auth/PasswordResetPage.tsx
msgid "Invalid password reset link. Please request a new one."
msgstr ""
msgstr "A ligazón de restablecemento non é válida. Solicita unha nova."
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
@@ -648,7 +648,7 @@ msgstr "Novo contrasinal"
#: src/pages/auth/PasswordResetPage.tsx
#: src/pages/auth/PasswordResetPage.tsx
msgid "New Password"
msgstr ""
msgstr "Novo contrasinal"
#: src/pages/app/AboutPage.tsx
msgid "Newest first"
@@ -789,7 +789,7 @@ msgstr "Contrasinal"
#: src/hooks/useValidationRules.ts
msgid "Password must be at least {minimumPasswordLength} characters"
msgstr ""
msgstr "O contrasinal ten que ter {minimumPasswordLength} caracteres polo menos"
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Password Recovery"
@@ -840,7 +840,7 @@ msgstr "Non se admiten novas contas nesta instancia de CommaFeed"
#: src/pages/auth/PasswordResetPage.tsx
#: src/pages/auth/PasswordResetPage.tsx
msgid "Reset Password"
msgstr ""
msgstr "Restablecer contrasinal"
#: src/pages/app/AboutPage.tsx
msgid "REST API"
@@ -1104,7 +1104,7 @@ msgstr "Páxina web"
#: src/pages/auth/InitialSetupPage.tsx
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr ""
msgstr "Benvida! Semella que é a primeira vez que executas CommaFeed. Para comezar, crea unha conta de administración."
#: src/components/settings/DisplaySettings.tsx
msgid "Yellow"
@@ -1120,4 +1120,4 @@ msgstr "As túas canles están na cola para ser actualizadas."
#: src/pages/auth/PasswordResetPage.tsx
msgid "Your password has been changed. You can now log in with your new password."
msgstr ""
msgstr "O teu contrasinal cambiou. Xa podes acceder coas novas credenciais."

View File

@@ -6,13 +6,14 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>6.1.1</version>
<version>6.2.0</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
<packaging>quarkus</packaging>
<properties>
<quarkus.version>3.30.8</quarkus.version>
<quarkus.version>3.31.2</quarkus.version>
<querydsl.version>7.1</querydsl.version>
<rome.version>2.1.0</rome.version>
@@ -77,24 +78,14 @@
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
<configuration>
<properties>
<quarkus.package.output-name>commafeed-${project.version}</quarkus.package.output-name>
<quarkus.package.runner-suffix>
-${build.database}-${os.detected.name}-${os.detected.arch}-runner
</quarkus.package.runner-suffix>
</properties>
</configuration>
</execution>
</executions>
<configuration>
<properties>
<quarkus.package.output-name>commafeed-${project.version}</quarkus.package.output-name>
<quarkus.package.runner-suffix>
-${build.database}-${os.detected.name}-${os.detected.arch}-runner
</quarkus.package.runner-suffix>
</properties>
</configuration>
</plugin>
<plugin>
<groupId>io.quarkus</groupId>
@@ -167,6 +158,7 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<argLine>@{argLine}</argLine>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<quarkus.datasource.db-kind>${build.database}</quarkus.datasource.db-kind>
@@ -186,14 +178,13 @@
</execution>
</executions>
<configuration>
<argLine>@{argLine}</argLine>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<quarkus.datasource.db-kind>${build.database}</quarkus.datasource.db-kind>
</systemPropertyVariables>
<!-- fix for java.lang.NoClassDefFoundError: Could not initialize class org.jboss.threads.JDKSpecific$ThreadAccess (#1938) -->
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
</configuration>
<!-- failsafe plugin does not seem to be able to pick up dependencies declared in profiles -->
<dependencies>
@@ -246,7 +237,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>13.0.0</version>
<version>13.2.0</version>
</dependency>
</dependencies>
<executions>
@@ -275,7 +266,7 @@
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>3.2.0</version>
<version>3.2.1</version>
<?m2e ignore?>
<executions>
<execution>
@@ -304,7 +295,7 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>6.1.1</version>
<version>6.2.0</version>
</dependency>
<!-- compile-time processors -->
@@ -475,7 +466,7 @@
<!-- test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<artifactId>quarkus-junit-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -1,4 +1,4 @@
FROM debian:13.3@sha256:5cf544fad978371b3df255b61e209b373583cb88b733475c86e49faa15ac2104
FROM debian:13.3@sha256:2c91e484d93f0830a7e05a2b9d92a7b102be7cab562198b984a84fdbc7806d91
ARG TARGETARCH
EXPOSE 8082

View File

@@ -312,6 +312,12 @@ public interface CommaFeedConfiguration {
@WithDefault("100")
int batchSize();
/**
* Whether to keep starred entries when cleaning up old entries.
*/
@WithDefault("true")
boolean keepStarredEntries();
default Instant statusesInstantThreshold() {
return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null;
}

View File

@@ -11,6 +11,7 @@ import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.QFeedEntry;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQuery;
@Singleton
public class FeedEntryDAO extends GenericDAO<FeedEntry> {
@@ -25,15 +26,21 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
return query().select(ENTRY).from(ENTRY).where(ENTRY.guidHash.eq(guidHash), ENTRY.feed.eq(feed)).limit(1).fetchOne();
}
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max, boolean keepStarredEntries) {
NumberExpression<Long> count = ENTRY.id.count();
List<Tuple> tuples = query().select(ENTRY.feed.id, count)
.from(ENTRY)
.groupBy(ENTRY.feed)
JPAQuery<Tuple> query = query().select(ENTRY.feed.id, count).from(ENTRY);
if (keepStarredEntries) {
query.where(Predicates.isNotStarred(ENTRY));
}
return query.groupBy(ENTRY.feed)
.having(count.gt(maxCapacity))
.limit(max)
.fetch();
return tuples.stream().map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count))).toList();
.fetch()
.stream()
.map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count)))
.toList();
}
public int delete(Long feedId, long max) {
@@ -44,21 +51,30 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
/**
* Delete entries older than a certain date
*/
public int deleteEntriesOlderThan(Instant olderThan, long max) {
List<FeedEntry> list = query().selectFrom(ENTRY)
public int deleteEntriesOlderThan(Instant olderThan, long max, boolean keepStarredEntries) {
JPAQuery<FeedEntry> query = query().selectFrom(ENTRY)
.where(ENTRY.published.lt(olderThan))
.orderBy(ENTRY.published.asc())
.limit(max)
.fetch();
return delete(list);
.limit(max);
if (keepStarredEntries) {
query.where(Predicates.isNotStarred(ENTRY));
}
return delete(query.fetch());
}
/**
* Delete the oldest entries of a feed
*/
public int deleteOldEntries(Long feedId, long max) {
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.published.asc()).limit(max).fetch();
return delete(list);
public int deleteOldEntries(Long feedId, long max, boolean keepStarredEntries) {
JPAQuery<FeedEntry> query = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.published.asc()).limit(max);
if (keepStarredEntries) {
query.where(Predicates.isNotStarred(ENTRY));
}
return delete(query.fetch());
}
public record FeedCapacity(Long id, Long capacity) {

View File

@@ -0,0 +1,18 @@
package com.commafeed.backend.dao;
import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryStatus;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
import lombok.experimental.UtilityClass;
@UtilityClass
public class Predicates {
private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
public static BooleanExpression isNotStarred(QFeedEntry entry) {
return JPAExpressions.selectOne().from(STATUS).where(STATUS.entry.eq(entry).and(STATUS.starred.isTrue())).notExists();
}
}

View File

@@ -27,13 +27,13 @@ import lombok.extern.slf4j.Slf4j;
@Singleton
public class DatabaseCleaningService {
private final int batchSize;
private final UnitOfWork unitOfWork;
private final FeedDAO feedDAO;
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryContentDAO feedEntryContentDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final int batchSize;
private final boolean keepStarredEntries;
private final Meter entriesDeletedMeter;
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO,
@@ -44,6 +44,7 @@ public class DatabaseCleaningService {
this.feedEntryContentDAO = feedEntryContentDAO;
this.feedEntryStatusDAO = feedEntryStatusDAO;
this.batchSize = config.database().cleanup().batchSize();
this.keepStarredEntries = config.database().cleanup().keepStarredEntries();
this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted"));
}
@@ -86,21 +87,23 @@ public class DatabaseCleaningService {
log.info("cleaning entries exceeding feed capacity");
long total = 0;
while (true) {
List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize));
List<FeedCapacity> feeds = unitOfWork
.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries));
if (feeds.isEmpty()) {
break;
}
for (final FeedCapacity feed : feeds) {
long remaining = feed.capacity() - maxFeedCapacity;
int deleted;
do {
final long rem = remaining;
int deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem)));
deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem), keepStarredEntries));
entriesDeletedMeter.mark(deleted);
total += deleted;
remaining -= deleted;
log.debug("removed {} entries for feeds exceeding capacity", total);
} while (remaining > 0);
} while (deleted > 0 && remaining > 0);
}
}
log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total);
@@ -111,7 +114,7 @@ public class DatabaseCleaningService {
long total = 0;
long deleted;
do {
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize));
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries));
entriesDeletedMeter.mark(deleted);
total += deleted;
log.debug("removed {} old entries", total);

View File

@@ -115,13 +115,13 @@ class DatabaseCleaningServiceTest {
Mockito.when(feed2.id()).thenReturn(2L);
Mockito.when(feed2.capacity()).thenReturn(120L);
Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE))
Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE, false))
.thenReturn(Arrays.asList(feed1, feed2))
.thenReturn(Collections.emptyList());
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100)).thenReturn(80);
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50)).thenReturn(50);
Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70)).thenReturn(70);
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100, false)).thenReturn(80);
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50, false)).thenReturn(50);
Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70, false)).thenReturn(70);
service.cleanEntriesForFeedsExceedingCapacity(50);
@@ -132,11 +132,11 @@ class DatabaseCleaningServiceTest {
void cleanEntriesOlderThanDeletesOldEntries() {
Instant cutoff = LocalDate.now().minusDays(30).atStartOfDay().toInstant(ZoneOffset.UTC);
Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE)).thenReturn(100, 50, 0);
Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE, false)).thenReturn(100, 50, 0);
service.cleanEntriesOlderThan(cutoff);
Mockito.verify(feedEntryDAO, Mockito.times(3)).deleteEntriesOlderThan(cutoff, BATCH_SIZE);
Mockito.verify(feedEntryDAO, Mockito.times(3)).deleteEntriesOlderThan(cutoff, BATCH_SIZE, false);
Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong());
}

View File

@@ -0,0 +1,185 @@
package com.commafeed.integration.cleanup;
import java.time.Duration;
import java.time.Instant;
import jakarta.inject.Inject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import com.commafeed.TestConstants;
import com.commafeed.backend.service.db.DatabaseCleaningService;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.request.StarRequest;
import com.commafeed.frontend.resource.CategoryREST;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
@QuarkusTest
class DatabaseCleaningIT extends BaseIT {
@Inject
DatabaseCleaningService databaseCleaningService;
@BeforeEach
void setup() {
initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
private void starEntry(String entryId, Long subscriptionId) {
StarRequest starRequest = new StarRequest();
starRequest.setId(entryId);
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(true);
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star").then().statusCode(200);
}
private void unstarEntry(String entryId, Long subscriptionId) {
StarRequest starRequest = new StarRequest();
starRequest.setId(entryId);
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(false);
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star").then().statusCode(200);
}
@Nested
class KeepStarredEntries {
@Test
void starredEntriesAreKeptWhenCleaningFeedsExceedingCapacity() {
// Subscribe to feed and wait for entries
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// Verify we have 2 entries
Entries entriesBefore = getFeedEntries(subscriptionId);
Assertions.assertEquals(2, entriesBefore.getEntries().size());
// Star the first entry
Entry entryToStar = entriesBefore.getEntries().getFirst();
starEntry(entryToStar.getId(), subscriptionId);
// Verify the entry is starred
Entries starredEntries = getCategoryEntries(CategoryREST.STARRED);
Assertions.assertEquals(1, starredEntries.getEntries().size());
Assertions.assertEquals(entryToStar.getId(), starredEntries.getEntries().getFirst().getId());
// Run cleanup with capacity of 0 (should delete all non-starred entries)
// With keepStarredEntries=true (default), only non-starred entries are counted for capacity.
// We have 2 entries, 1 starred and 1 non-starred. With capacity=0, the 1 non-starred entry exceeds capacity.
databaseCleaningService.cleanEntriesForFeedsExceedingCapacity(0);
// Verify starred entry is still present
Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED);
Assertions.assertEquals(1, starredEntriesAfter.getEntries().size());
Assertions.assertEquals(entryToStar.getId(), starredEntriesAfter.getEntries().getFirst().getId());
// Verify the non-starred entry was deleted (only starred entry should remain)
Entries entriesAfter = getFeedEntries(subscriptionId);
Assertions.assertEquals(1, entriesAfter.getEntries().size());
Assertions.assertEquals(entryToStar.getId(), entriesAfter.getEntries().getFirst().getId());
}
@Test
void starredEntriesAreKeptWhenCleaningOldEntries() {
// Subscribe to feed and wait for entries
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// Verify we have 2 entries
Entries entriesBefore = getFeedEntries(subscriptionId);
Assertions.assertEquals(2, entriesBefore.getEntries().size());
// Star the first entry (oldest one based on published date in rss.xml)
Entry entryToStar = entriesBefore.getEntries().getFirst();
starEntry(entryToStar.getId(), subscriptionId);
// Verify the entry is starred
Entries starredEntries = getCategoryEntries(CategoryREST.STARRED);
Assertions.assertEquals(1, starredEntries.getEntries().size());
// Run cleanup for entries older than now (should try to delete all entries)
// With keepStarredEntries=true (default), the starred entry should be preserved
Instant olderThan = Instant.now().plus(Duration.ofDays(1));
databaseCleaningService.cleanEntriesOlderThan(olderThan);
// Verify starred entry is still present
Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED);
Assertions.assertEquals(1, starredEntriesAfter.getEntries().size());
Assertions.assertEquals(entryToStar.getId(), starredEntriesAfter.getEntries().getFirst().getId());
// Verify the non-starred entry was deleted
Entries entriesAfter = getFeedEntries(subscriptionId);
Assertions.assertEquals(1, entriesAfter.getEntries().size());
Assertions.assertEquals(entryToStar.getId(), entriesAfter.getEntries().getFirst().getId());
}
@Test
void multipleStarredEntriesAreAllKept() {
// Subscribe to feed and wait for entries
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// Verify we have 2 entries
Entries entriesBefore = getFeedEntries(subscriptionId);
Assertions.assertEquals(2, entriesBefore.getEntries().size());
// Star both entries
entriesBefore.getEntries().forEach(entry -> starEntry(entry.getId(), subscriptionId));
// Verify both entries are starred
Entries starredEntries = getCategoryEntries(CategoryREST.STARRED);
Assertions.assertEquals(2, starredEntries.getEntries().size());
// Run cleanup with capacity of 0 (should delete all non-starred entries)
databaseCleaningService.cleanEntriesForFeedsExceedingCapacity(0);
// Verify both starred entries are still present
Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED);
Assertions.assertEquals(2, starredEntriesAfter.getEntries().size());
// Verify all entries are preserved (since all are starred)
Entries entriesAfter = getFeedEntries(subscriptionId);
Assertions.assertEquals(2, entriesAfter.getEntries().size());
}
@Test
void unstarringEntryMakesItEligibleForCleanup() {
// Subscribe to feed and wait for entries
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// Star the first entry
Entries entriesBefore = getFeedEntries(subscriptionId);
Entry entry = entriesBefore.getEntries().getFirst();
starEntry(entry.getId(), subscriptionId);
// Verify entry is starred
Assertions.assertEquals(1, getCategoryEntries(CategoryREST.STARRED).getEntries().size());
// Unstar the entry
unstarEntry(entry.getId(), subscriptionId);
// Verify entry is no longer starred
Assertions.assertEquals(0, getCategoryEntries(CategoryREST.STARRED).getEntries().size());
// Run cleanup for entries older than now
Instant olderThan = Instant.now().plus(Duration.ofDays(1));
databaseCleaningService.cleanEntriesOlderThan(olderThan);
// Verify both entries were deleted (neither is starred)
Entries entriesAfter = getFeedEntries(subscriptionId);
Assertions.assertEquals(0, entriesAfter.getEntries().size());
}
}
}

View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>6.1.1</version>
<version>6.2.0</version>
<name>CommaFeed</name>
<packaging>pom</packaging>
@@ -19,7 +19,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<version>3.15.0</version>
<configuration>
<parameters>true</parameters>