Compare commits

..

83 Commits

Author SHA1 Message Date
StephanRichter 38430feca0 extended documentation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 73f74a0929 extended documentation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 0a3f33c70d working on task description
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter e7c8039249 working on demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter cc0dd85814 adding demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 33bff55811 working on demodata
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 182060134e extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 64be02d863 extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 70ea315fa7 extending demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter 889599dc40 started creating demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:13:03 +02:00
StephanRichter ac0b61dca0 Merge branch 'main' into dev
Build Docker Image / Clean-Registry (push) Failing after 12m21s
Build Docker Image / Docker-Build (push) Failing after 16m34s
2026-06-17 15:07:52 +02:00
StephanRichter 1d7b47aaa0 made project page the entrypoint
Build Docker Image / Docker-Build (push) Successful in 2m39s
Build Docker Image / Clean-Registry (push) Successful in 5s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 15:07:44 +02:00
StephanRichter f49b44cd56 Merge branch 'bugfix/pipeline'
Build Docker Image / Clean-Registry (push) Successful in 6s
Build Docker Image / Docker-Build (push) Failing after 14m18s
2026-06-17 09:21:55 +02:00
StephanRichter 07492d34de Merge branch 'bugfix/pipeline' into dev
Build Docker Image / Docker-Build (push) Successful in 2m9s
Build Docker Image / Clean-Registry (push) Failing after 10m20s
2026-06-17 09:18:17 +02:00
StephanRichter 60777feaa3 dropped call to make.eldorado.srsoftware.de
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 09:18:03 +02:00
StephanRichter 9e4158ad19 Merge branch 'bugfix/timecomplete' into dev
Build Docker Image / Clean-Registry (push) Successful in 6s
Build Docker Image / Docker-Build (push) Failing after 14m27s
2026-06-17 09:15:08 +02:00
StephanRichter bcc1182dea improved time table: now updating started time when starting new time track
Build Docker Image / Docker-Build (push) Successful in 2m25s
Build Docker Image / Clean-Registry (push) Failing after 10m18s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-17 09:14:57 +02:00
StephanRichter b81d518a2b Merge branch 'main' into dev
Build Docker Image / Docker-Build (push) Successful in 2m30s
Build Docker Image / Clean-Registry (push) Failing after 12m3s
2026-06-16 08:32:36 +02:00
StephanRichter 4a83bb6bee overhauled account index page:
Build Docker Image / Docker-Build (push) Successful in 2m34s
Build Docker Image / Clean-Registry (push) Successful in 5s
- dropped ids from display
- ordered accounts by name

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-16 08:32:06 +02:00
StephanRichter e17fdbc619 Merge branch 'main' into dev
Build Docker Image / Docker-Build (push) Successful in 2m21s
Build Docker Image / Clean-Registry (push) Successful in 5s
2026-06-16 08:26:56 +02:00
StephanRichter 0879b53a88 improved translations
Build Docker Image / Clean-Registry (push) Successful in 5s
Build Docker Image / Docker-Build (push) Failing after 17m15s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-16 08:26:47 +02:00
StephanRichter 433ea6ddd3 Merge branch 'main' into dev
Build Docker Image / Clean-Registry (push) Successful in 5s
Build Docker Image / Docker-Build (push) Failing after 14m25s
2026-06-08 10:53:23 +02:00
StephanRichter 6aa858e299 improved range of states of tasks in easylist
Build Docker Image / Docker-Build (push) Successful in 3m47s
Build Docker Image / Clean-Registry (push) Successful in 6s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-06-08 10:53:05 +02:00
StephanRichter 6249cdb7b9 re-implemented new transaction form
Build Docker Image / Docker-Build (push) Failing after 5m19s
Build Docker Image / Clean-Registry (push) Failing after 11m53s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-30 22:19:29 +02:00
StephanRichter 31545b8b11 css improvement
Build Docker Image / Clean-Registry (push) Failing after 10m34s
Build Docker Image / Docker-Build (push) Failing after 14m39s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-24 20:21:48 +02:00
StephanRichter feafb44a9f Merge branch 'bugfix/autocomplete' into dev
Build Docker Image / Clean-Registry (push) Successful in 5s
Build Docker Image / Docker-Build (push) Failing after 14m59s
2026-05-22 08:46:39 +02:00
StephanRichter d187b6a2fc fixed typo
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-22 08:46:30 +02:00
StephanRichter 8d7de4b1b6 Merge branch 'bugfix/autocomplete' into dev
Build Docker Image / Clean-Registry (push) Failing after 11m24s
Build Docker Image / Docker-Build (push) Failing after 16m57s
2026-05-22 08:37:27 +02:00
StephanRichter e61e09d834 fixing dropdown disabling on dropdown click
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-22 08:37:22 +02:00
StephanRichter 3cff613335 Merge branch 'bugfix/autocomplete' into dev
Build Docker Image / Docker-Build (push) Successful in 3m13s
Build Docker Image / Clean-Registry (push) Has been cancelled
2026-05-22 08:26:25 +02:00
StephanRichter 0edeef2a9d implemented closing of autocomplete dropdown when leaving input
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-22 08:26:08 +02:00
StephanRichter 3a7779a665 Merge branch 'feature/easy-complete' into dev
Build Docker Image / Docker-Build (push) Successful in 2m52s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-05-19 16:06:22 +02:00
StephanRichter a1164e416a implemented saving of task/project from tag input field
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-19 16:05:43 +02:00
StephanRichter 2f8276c1be Merge branch 'bugfix/external-links' into dev
Build Docker Image / Docker-Build (push) Successful in 2m40s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-05-19 10:46:37 +02:00
StephanRichter 8b139b1bed moved markdown rendering from Util to MarkdownApi
Build Docker Image / Docker-Build (push) Successful in 2m44s
Build Docker Image / Clean-Registry (push) Successful in 6s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-19 10:46:28 +02:00
StephanRichter 3b2371ad64 Merge branch 'bugfix/external-links' into dev
Build Docker Image / Docker-Build (push) Successful in 2m56s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-05-19 09:54:03 +02:00
StephanRichter aa48bbcbf5 implemented adding targets to links in markdown
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-19 09:53:43 +02:00
StephanRichter c5e31db99e Merge branch 'feature/parent_selector' into dev
Build Docker Image / Docker-Build (push) Successful in 4m25s
Build Docker Image / Clean-Registry (push) Successful in 1s
2026-05-14 00:32:23 +02:00
StephanRichter 098811547a Merge branch 'module/projects' into dev
Build Docker Image / Docker-Build (push) Successful in 5m20s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-05-11 20:14:32 +02:00
StephanRichter 6193b727bd Merge branch 'module/projects' into dev
Build Docker Image / Docker-Build (push) Successful in 3m5s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-05-07 18:26:44 +02:00
StephanRichter 1d1520534c Merge branch 'module/notes' into dev
Build Docker Image / Docker-Build (push) Successful in 3m7s
Build Docker Image / Clean-Registry (push) Successful in 2s
2026-05-06 09:26:13 +02:00
StephanRichter 883b90faa7 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 3m0s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-05-06 08:43:22 +02:00
StephanRichter a8a4ab0985 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m55s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-05-05 22:56:26 +02:00
StephanRichter bc9df18307 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m51s
Build Docker Image / Clean-Registry (push) Successful in 4s
2026-05-05 20:34:54 +02:00
StephanRichter a8e3122152 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 3m23s
Build Docker Image / Clean-Registry (push) Successful in 4s
2026-05-05 10:01:16 +02:00
StephanRichter dc1a5f4e94 Merge branch 'bugfix/markdown-edit' into dev
Build Docker Image / Docker-Build (push) Successful in 3m0s
Build Docker Image / Clean-Registry (push) Successful in 4s
2026-05-05 08:27:19 +02:00
StephanRichter 80153ada13 Merge branch 'main' into dev
Build Docker Image / Docker-Build (push) Successful in 2m33s
Build Docker Image / Clean-Registry (push) Successful in 4s
2026-05-04 20:58:39 +02:00
StephanRichter 9bf9fba9df Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m35s
Build Docker Image / Clean-Registry (push) Successful in 4s
2026-05-04 15:28:02 +02:00
StephanRichter 01d56ca451 Merge branch 'main' into dev
Build Docker Image / Docker-Build (push) Successful in 2m51s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-05-04 15:13:49 +02:00
StephanRichter b4af8d1876 Merge branch 'bugfix/empty-markdown' into dev
Build Docker Image / Docker-Build (push) Successful in 2m57s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-05-04 14:39:04 +02:00
StephanRichter 69c8d0fe9c Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 3m8s
Build Docker Image / Clean-Registry (push) Successful in 4s
2026-05-03 14:02:04 +02:00
StephanRichter bf1a6684a7 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m36s
Build Docker Image / Clean-Registry (push) Successful in 4s
2026-04-25 20:15:10 +02:00
StephanRichter 5a47ebae2b Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m28s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-25 10:12:09 +02:00
StephanRichter 9d62f15b8f Merge branch 'improvement/autocomplete' into dev 2026-04-24 11:45:39 +02:00
StephanRichter 5524ea7878 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m31s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-23 23:25:19 +02:00
StephanRichter 3d1850b2d2 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m32s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-23 20:17:25 +02:00
StephanRichter 02283d57ba Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m1s
Build Docker Image / Clean-Registry (push) Successful in 5s
2026-04-23 16:03:21 +02:00
StephanRichter 1768a48e5e Merge branch 'improvemnet/autocomplete-delay' into dev
Build Docker Image / Docker-Build (push) Successful in 2m27s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-23 15:54:18 +02:00
StephanRichter 1cdf825bcb Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m5s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-23 11:10:21 +02:00
StephanRichter 71c86e512d Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m32s
Build Docker Image / Clean-Registry (push) Successful in 5s
2026-04-23 10:37:56 +02:00
StephanRichter 9d8013bc33 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m26s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-22 08:38:09 +02:00
StephanRichter 05733d3b7a improved autocomplete
Build Docker Image / Docker-Build (push) Successful in 2m25s
Build Docker Image / Clean-Registry (push) Successful in 6s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-21 09:30:48 +02:00
StephanRichter 71c071bbdd Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m30s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-21 08:27:45 +02:00
StephanRichter 1241fee61d Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m6s
Build Docker Image / Clean-Registry (push) Successful in 5s
2026-04-20 23:04:31 +02:00
StephanRichter d64cb886c9 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m12s
Build Docker Image / Clean-Registry (push) Successful in 5s
2026-04-20 22:38:58 +02:00
StephanRichter ac8149e6bb Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m1s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-20 22:23:05 +02:00
StephanRichter 493b61465b Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m11s
Build Docker Image / Clean-Registry (push) Successful in 5s
2026-04-20 21:55:23 +02:00
StephanRichter df372e9cfd Merge branch 'accounting' into dev 2026-04-20 08:25:39 +02:00
StephanRichter ff58f3ae82 Merge branch 'bugfix/trim-tags' into dev
Build Docker Image / Docker-Build (push) Successful in 2m19s
Build Docker Image / Clean-Registry (push) Successful in 5s
2026-04-19 11:07:54 +02:00
StephanRichter b71db96b47 improved build workflow
Build Docker Image / Docker-Build (push) Successful in 1m56s
Build Docker Image / Clean-Registry (push) Successful in 5s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-19 10:45:58 +02:00
StephanRichter 02434419f4 Merge branch 'bugfix/trim-tags' into dev
Build Docker Image / Docker-Build (push) Successful in 2m14s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-19 10:40:09 +02:00
StephanRichter bd096dc61f Merge branch 'bugfix/trim-tags' into dev
Build Docker Image / Docker-Build (push) Successful in 2m18s
Build Docker Image / Clean-Registry (push) Successful in -18s
2026-04-18 15:44:37 +02:00
StephanRichter 9f286f3121 Merge branch 'bugfix/trim-tags' into dev
Build Docker Image / Docker-Build (push) Successful in 1m53s
Build Docker Image / Clean-Registry (push) Successful in -18s
2026-04-18 15:28:30 +02:00
StephanRichter 2211f4f39d Merge branch 'bugfix/tag-header' into dev
Build Docker Image / Docker-Build (push) Successful in 3m1s
Build Docker Image / Clean-Registry (push) Successful in -18s
2026-04-18 15:08:02 +02:00
StephanRichter a6b988df3a Merge branch 'accounting' into dev 2026-04-18 15:05:10 +02:00
StephanRichter 1316d3fb1e Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m15s
Build Docker Image / Clean-Registry (push) Successful in -18s
2026-04-15 18:16:37 +02:00
StephanRichter 99fa75a980 Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m17s
Build Docker Image / Clean-Registry (push) Successful in -16s
2026-04-14 23:22:35 +02:00
StephanRichter 6fc590d795 Merge branch 'accounting' into dev 2026-04-14 22:54:44 +02:00
StephanRichter 7afc804586 added menu symbol for accounting
Build Docker Image / Docker-Build (push) Successful in 1m59s
Build Docker Image / Clean-Registry (push) Successful in -17s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-14 22:54:03 +02:00
StephanRichter f40692dd3d Merge branch 'accounting' into dev
Build Docker Image / Docker-Build (push) Successful in 2m23s
Build Docker Image / Clean-Registry (push) Successful in -16s
2026-04-14 22:49:23 +02:00
StephanRichter 1c91699bf5 Merge branch 'module/wiki' into dev
Build Docker Image / Docker-Build (push) Successful in 3m18s
Build Docker Image / Clean-Registry (push) Successful in -15s
2026-04-09 09:12:22 +02:00
StephanRichter 9f5e1e0853 Merge branch 'main' into dev 2026-04-01 18:14:24 +02:00
StephanRichter 55dfea65b0 Merge branch 'bugfix/wiki-css' into dev
Build Docker Image / Docker-Build (push) Successful in 2m34s
Build Docker Image / Clean-Registry (push) Successful in -12s
2026-03-30 23:59:17 +02:00
51 changed files with 569 additions and 220 deletions
+2 -5
View File
@@ -41,11 +41,6 @@ jobs:
docker push ${{ secrets.REGISTRY_PATH }}/umbrella:${{ gitea.ref_name }}
docker push ${{ secrets.REGISTRY_PATH }}/umbrella:$TAG
- name: Restart umbrella.srsoftware.de
if: github.ref == 'refs/heads/dev'
run: |
curl -X POST -H "Authorization: Bearer ${{ secrets.ELDORADO_MAKE_BEARER }}" -d umbrella_25_start https://make.eldorado.srsoftware.de/launch
Clean-Registry:
runs-on: ubuntu-latest
steps:
@@ -53,8 +48,10 @@ jobs:
run: |
TAGS="$(curl -s -u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASS }}" https://${{ secrets.REGISTRY_PATH }}/v2/umbrella/tags/list | jq -r ".tags[]")"
COUNT=$(echo "$TAGS" | wc -l)
echo found $COUNT tags: $TAGS
if [ $COUNT -gt 10 ]; then
REMAIN=$((COUNT - 10))
echo $REMAIN tags will be kept!
echo "$TAGS" | head -n $REMAIN > /tmp/old_tags
else
echo less than 10 tags, skipping cleanup
@@ -16,7 +16,7 @@ public interface AccountDb {
Collection<UmbrellaUser> getMembers(long accountId);
Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount);
Optional<Transaction> lastTransaction(long accountId, String source, String destination, Double amount);
Collection<Account> listAccounts(long userId);
@@ -311,12 +311,11 @@ public class AccountingModule extends BaseHandler implements AccountingService {
var source = src.get(src.has(Field.ID) ? Field.ID : Field.DISPLAY).toString();
if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION);
if (!(json.get(Field.DESTINATION) instanceof JSONObject dst)) throw invalidField(Field.SOURCE,JSON);
var dest = dst.get(dst.has(Field.ID) ? Field.ID : Field.DISPLAY).toString();
if (!json.has(Field.AMOUNT)) throw missingField(Field.AMOUNT);
if (!(json.get(Field.AMOUNT) instanceof Number amt)) throw invalidField(Field.AMOUNT,Text.NUMBER);
var amount = amt.doubleValue();
String destination = dst.has(Field.ID) ? dst.get(Field.ID).toString() : dst.has(Field.DISPLAY) ? dst.get(Field.DISPLAY).toString() : null;
Double amount = null;
if (json.has(Field.AMOUNT) && json.get(Field.AMOUNT) instanceof Number amt) amount = amt.doubleValue();
var transaction = accountDb.lastTransaction(accountId, source, dest, amount);
var transaction = accountDb.lastTransaction(accountId, source, destination, amount);
return transaction.isPresent() ? sendContent(ex,transaction.get()) : notFound(ex);
}
@@ -170,22 +170,42 @@ public class SqliteDb extends BaseDb implements AccountDb {
}
@Override
public Optional<Transaction> lastTransaction(long accountId, String source, String dest, double amount) {
public Optional<Transaction> lastTransaction(long accountId, String source, String destination, Double amount) {
try {
var rs = select(ALL).from(TABLE_TRANSACTIONS)
.where(ACCOUNT,equal(accountId)).where(SOURCE,equal(source)).where(DESTINATION,equal(dest)).where(AMOUNT,equal(amount))
.sort(ID+" DESC")
.limit(1)
.exec(db);
var query = select(ALL).from(TABLE_TRANSACTIONS).where(ACCOUNT,equal(accountId));
if (source != null) query = query.where(SOURCE,equal(source));
if (destination != null) query = query.where(DESTINATION,equal(destination));
if (amount != null) query = query.where(AMOUNT,equal(amount));
var rs = query.sort(ID+" DESC").limit(1).exec(db);
Transaction ta = null;
if (rs.next()) ta = Transaction.of(rs);
rs.close();
if (ta == null && amount != null) { // try to search by amount, ignore source and dest
rs = select(ALL).from(TABLE_TRANSACTIONS).where(ACCOUNT, equal(accountId)).where(AMOUNT, equal(amount))
.sort(ID + " DESC").limit(1).exec(db);
if (rs.next()) ta = Transaction.of(rs);
rs.close();
}
if (ta == null && source != null && destination != null) { // try to search by amount, ignore source and dest
rs = select(ALL).from(TABLE_TRANSACTIONS)
.where(SOURCE,equal(source))
.where(DESTINATION,equal(destination))
.where(ACCOUNT, equal(accountId))
.sort(ID + " DESC").limit(1).exec(db);
if (rs.next()) ta = Transaction.of(rs);
rs.close();
}
if (ta != null){
var tags = ta.tags();
rs = select(TAG).from(TABLE_TAGS_TRANSACTIONS).leftJoin(TAG_ID,TABLE_TAGS,ID).where(TRANSACTION_ID,equal(ta.id())).exec(db);
while (rs.next()) tags.add(rs.getString(1));
rs.close();
}
return nullable(ta);
} catch (SQLException e) {
throw failedToSearchDb(t(Text.ACCOUNTING));
@@ -14,7 +14,6 @@ import de.srsoftware.umbrella.bookmarks.BookmarkApi;
import de.srsoftware.umbrella.company.CompanyModule;
import de.srsoftware.umbrella.contact.ContactModule;
import de.srsoftware.umbrella.core.SettingsService;
import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.documents.DocumentApi;
import de.srsoftware.umbrella.files.FileModule;
@@ -34,7 +33,6 @@ import de.srsoftware.umbrella.translations.Translations;
import de.srsoftware.umbrella.user.UserModule;
import de.srsoftware.umbrella.web.WebHandler;
import de.srsoftware.umbrella.wiki.WikiModule;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
@@ -63,8 +61,6 @@ public class Application {
var port = config.get("umbrella.http.port", 8080);
var threads = config.get("umbrella.threads", 16);
config.get("umbrella.plantuml").map(Object::toString).map(File::new).filter(File::exists).ifPresent(Util::setPlantUmlJar);
var server = HttpServer.create(new InetSocketAddress(port), 0);
try {
new Translations(config).bindPath("/api/translations").on(server);
@@ -80,7 +76,7 @@ public class Application {
new DocumentApi(config).bindPath("/api/document").on(server);
new UserLegacy(config).bindPath("/legacy/user").on(server);
new NotesLegacy(config).bindPath("/legacy/notes").on(server);
new MarkdownApi().bindPath("/api/markdown").on(server);
new MarkdownApi(config).bindPath("/api/markdown").on(server);
new NoteModule(config).bindPath("/api/notes").on(server);
new StockModule(config).bindPath("/api/stock").on(server);
new PollModule(config).bindPath("/api/poll").on(server);
@@ -1,7 +1,6 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core;
import de.srsoftware.umbrella.core.api.*;
public class ModuleRegistry {
@@ -7,6 +7,7 @@ import static de.srsoftware.tools.PathHandler.GET;
import static de.srsoftware.tools.PathHandler.POST;
import static de.srsoftware.tools.Strings.hex;
import static de.srsoftware.umbrella.core.Errors.INVALID_URL;
import static de.srsoftware.umbrella.core.ModuleRegistry.markdownService;
import static de.srsoftware.umbrella.core.constants.Constants.TIME_FORMATTER;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.serverError;
@@ -14,7 +15,6 @@ import static java.lang.System.Logger.Level.*;
import static java.lang.System.Logger.Level.WARNING;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.xrbpowered.jparsedown.JParsedown;
import de.srsoftware.tools.Mappable;
import de.srsoftware.tools.Query;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
@@ -31,18 +31,12 @@ import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.json.JSONObject;
public class Util {
public static final System.Logger LOG = System.getLogger("Util");
private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL);
private static final Pattern SPREADSHEET_PATTERN = Pattern.compile("@startsheet(.*?)@endsheet",Pattern.DOTALL);
private static File plantumlJar = null;
private static final JParsedown MARKDOWN = new JParsedown();
public static final String SHA1 = "SHA-1";
public static final System.Logger LOG = System.getLogger("Util");
public static final String SHA1 = "SHA-1";
private static final MessageDigest SHA1_DIGEST;
private static final Map<Integer,String> umlCache = new HashMap<>();
private static final String SCRIPT = """
<script src="http://127.0.0.1:8080/js/jspreadsheet-ce.js"></script>
@@ -111,71 +105,11 @@ jspreadsheet(document.getElementById('spreadsheet'), {
public static HashMap<String, Object> mapMarkdown(String source){
var map = new HashMap<String,Object>();
map.put(SOURCE,source);
map.put(RENDERED,markdown(source));
map.put(RENDERED,markdownService().markdown(source));
return map;
}
public static String markdown(String source){
if (source == null) return source;
try {
var matcher = SPREADSHEET_PATTERN.matcher(source);
var count = 0;
while (matcher.find()){
count++;
var sheetData = matcher.group(0).trim();
var start = matcher.start(0);
var end = matcher.end(0);
source = source.substring(0, start)
+ "<div class=\"spreadsheet\" id=\"spreadsheet-"+count+"\">"
+ sheetData.substring(11,sheetData.length()-10)
+ "</div>"
+ source.substring(end);
matcher = SPREADSHEET_PATTERN.matcher(source);
}
if (plantumlJar != null && plantumlJar.exists()) {
matcher = UML_PATTERN.matcher(source);
while (matcher.find()) {
var uml = matcher.group(0).trim();
var start = matcher.start(0);
var end = matcher.end(0);
var umlHash = uml.hashCode();
LOG.log(DEBUG,"Hash of Plantuml code: {0}",umlHash);
var svg = umlCache.get(umlHash);
if (svg != null){
LOG.log(DEBUG,"Serving Plantuml generated SVG from cache…");
source = source.substring(0, start) + svg + source.substring(end);
matcher = UML_PATTERN.matcher(source);
continue;
}
LOG.log(DEBUG,"Cache miss. Generating SVG from plantuml code…");
ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", plantumlJar.getAbsolutePath(), "-tsvg", "-pipe");
var ignored = processBuilder.redirectErrorStream();
var process = processBuilder.start();
try (OutputStream os = process.getOutputStream()) {
os.write(uml.getBytes(UTF_8));
os.flush();
}
try (InputStream is = process.getInputStream()) {
byte[] out = is.readAllBytes();
LOG.log(DEBUG,"Generated SVG. Pushing to cache…");
svg = new String(out, UTF_8);
umlCache.put(umlHash,svg);
source = source.substring(0, start) + svg + source.substring(end);
matcher = UML_PATTERN.matcher(source);
}
}
}
return MARKDOWN.text(source);
} catch (Throwable e){
if (LOG.isLoggable(TRACE)){
LOG.log(TRACE,"Failed to render markdown, input was: \n{0}",source,e);
} else LOG.log(WARNING,"Failed to render markdown. Enable TRACE log level for details.");
return source;
}
}
public static HttpURLConnection open(URL url) throws IOException {
var conn = (HttpURLConnection) url.openConnection();
@@ -249,11 +183,6 @@ jspreadsheet(document.getElementById('spreadsheet'), {
return new Hash(hex(bytes),SHA1);
}
public static void setPlantUmlJar(File file){
LOG.log(INFO,"Using plantuml @ {0}",file.getAbsolutePath());
plantumlJar = file;
}
public static String dateTimeOf(long epochMilis){
return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilis), ZoneId.systemDefault()).format(TIME_FORMATTER);
}
@@ -2,4 +2,5 @@
package de.srsoftware.umbrella.core.api;
public interface MarkdownService {
String markdown(String source);
}
@@ -91,10 +91,7 @@ public class Poll implements Mappable {
return Map.of(
ID,id,
NAME,name,
DESCRIPTION, Map.of(
SOURCE,description,
RENDERED,Util.markdown(description)
),
DESCRIPTION, Util.mapMarkdown(description),
STATUS,status
);
}
@@ -292,10 +289,7 @@ public class Poll implements Mappable {
ID, id,
Field.OWNER, owner.toMap(),
NAME,name,
Field.DESCRIPTION, Map.of(
Field.SOURCE,description,
Field.RENDERED,Util.markdown(description)
),
Field.DESCRIPTION, Util.mapMarkdown(description),
Field.OPTIONS, options.stream().collect(Collectors.toMap(Option::id,Option::toMap)),
Field.PERMISSION, mapPermissions(),
Field.PRIVATE, isPrivate,
+1
View File
@@ -0,0 +1 @@
*.db-journal
Binary file not shown.
Binary file not shown.
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
{
"umbrella": {
"base_url": "http://127.0.0.1:5173",
"logging": {
"rootLevel": "INFO"
},
"http": {
"port": 8080
},
"threads": 16,
"modules": {
"accounting": {
"database": "demodata/accounting.db"
},
"bookmark": {
"database": "demodata/bookmark.db"
},
"company": {
"database": "demodata/company.db"
},
"contact": {
"database": "demodata/contacts.db"
},
"document": {
"database": "demodata/documents.db",
"templates": "demodata/templates"
},
"files": {
"database": "demodata/files.db",
"base_dir": "demodata/filestore"
},
"journal": {
"database": "demodata/journal.db"
},
"message": {
"database": "demodata/message.db",
"smtp": {
"from": "umbrella@example.com",
"host": "none",
"pass": "none",
"port": 587,
"user": "none"
}
},
"notes": {
"database": "demodata/notes.db"
},
"poll": {
"database": "demodata/poll.db"
},
"project": {
"database": "demodata/projects.db"
},
"stock": {
"database": "demodata/stock.db"
},
"tags": {
"database": "demodata/tags.db"
},
"task": {
"database": "demodata/tasks.db"
},
"time": {
"database": "demodata/times.db"
},
"user": {
"database": "demodata/users.db"
},
"wiki": {
"database": "demodata/wiki.db"
}
}
}
}
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -90,7 +90,7 @@
{#if messages.warning}
<span class="warn">{@html messages.warning}</span>
{/if}
<Route path="/" component={User} />
<Route path="/" component={ProjectList} />
<Route path="/account/:id" component={Account} />
<Route path="/accounting" component={Accounts} />
<Route path="/accounting/new" component={NewAccount} />
@@ -116,6 +116,7 @@
<Route path="/project/:project_id/add_task" component={AddTask} />
<Route path="/project/:id/kanban" component={Kanban} />
<Route path="/project/:id/view" component={ViewPrj} />
<Route path="/project/:id" component={ViewPrj} />
<Route path="/search" component={Search} />
<Route path="/stock" component={Stock} />
<Route path="/stock/location/:location_id" component={Stock} />
+17 -14
View File
@@ -49,12 +49,16 @@
console.warn(`${candidate.display} selected, but onSelect not overridden!`)
}
function disableDropdown(){
candidates = [];
selected = null;
}
async function ondblclick(evt){
const select = evt.target;
const idx = select.value;
candidate = candidates[idx];
candidates = [];
selected = null;
disableDropdown();
onSelect(candidate);
}
@@ -63,6 +67,10 @@
selected = null;
}
function onblur(ev){
setTimeout(disableDropdown,400);
}
async function onkeyup(ev){
if (ignore.includes(ev.key)) return;
if (ev.key == 'ArrowDown'){
@@ -83,22 +91,19 @@
ev.preventDefault();
if (selected != null && selected < candidates.length) {
candidate = candidates[selected];
candidates = [];
selected = null;
disableDropdown();
onSelect(candidate);
return false;
}
if (ev.key == 'Enter') {
candidates = [];
selected = null;
disableDropdown();
if (onCommit(candidate)) candidate = { display : '' };
}
return false;
}
if (ev.key == 'Escape'){
ev.preventDefault();
candidates = [];
selected = null;
disableDropdown();
return false;
}
@@ -110,10 +115,8 @@
function select(index){
candidate = candidates[index];
selected = null;
candidates = [];
<disableDropdown></disableDropdown>();
onSelect(candidate);
}
function scrollTo(index){
@@ -145,11 +148,11 @@
</style>
<span class="autocomplete">
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} {id} />
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} {id} {onblur} />
{#if candidates && candidates.length > 0}
<ul bind:this={list_elem} class="suggestions">
<ul bind:this={list_elem} class="suggestions" tabindex="-1">
{#each candidates as candidate,i}
<li class="option {selected==i?'highlight':''}" onclick={e => select(i)} ondblclick={e => select(i)}>{candidate.display}</li>
<li class="option {selected==i?'highlight':''}" tabindex="-1" onclick={e => select(i)} ondblclick={e => select(i)}>{candidate.display}</li>
{/each}
</ul>
{/if}
@@ -4,7 +4,7 @@
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
import EntryForm from './add_entry.svelte';
import EntryForm from './add_entry_new.svelte';
import Transaction from './transaction.svelte';
let { id } = $props();
@@ -163,5 +163,5 @@
</table>
</fieldset>
<EntryForm {account} {onSave} />
<EntryForm {account} {onSave} {users} />
{/if}
@@ -0,0 +1,205 @@
<script>
import { useTinyRouter } from 'svelte-tiny-router';
import { t } from '../../translations.svelte';
import { api, post } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { user } from '../../user.svelte';
import Autocomplete from '../../Components/Autocomplete.svelte';
import Tags from '../tags/TagList.svelte';
let defaultAccount = {
id : 0,
name : '',
currency : ''
};
let { account = defaultAccount, new_account = false, onSave = () => {}, users } = $props();
let entry = $state({
account,
date : new Date().toISOString().substring(0, 10),
source : {
display: user.name,
id: user.id
},
destination : {},
amount : 0.0,
purpose : {},
tags : []
});
let router = useTinyRouter();
async function dst_selected(destination){
destination = JSON.parse(JSON.stringify(destination));
let source = JSON.parse(JSON.stringify(entry.source));
const url = api(`accounting/${entry.account.id}/tags`)
const res = await post(url,{source,destination});
if (res.ok) {
yikes();
const json = await res.json();
await proposePurpose();
entry.tags = json;
} else error(res);
}
function focusOnEnter(ev,id){
if (ev.key == 'Enter') {
proposePurpose();
document.getElementById(id).focus();
}
}
async function getAccountTags(text){
if (!text) return [];
const url = api(`accounting/${entry.account.id}/tags`)
return await getProposals(text,url);
}
async function getDestinations(text){
const url = api('accounting/destinations');
return await getProposals(text,url);
}
async function getProposals(text,url){
const res = await post(url,text);
if (res.ok){
yikes();
const input = await res.json();
return Object.values(input).map(mapDisplay);
} else {
error(res);
return {};
}
}
async function getPurposes(text) {
const url = api('accounting/purposes');
return await getProposals(text,url);
}
async function getSources(text){
const url = api('accounting/sources');
return await getProposals(text,url);
}
function gotoTags(purpose){
document.getElementById('new_tag_input');
}
function mapDisplay(object){
if (object.display){
return object;
} else if (object.name) {
return {...object, display: object.name};
} else {
return { display : object }
}
}
async function proposePurpose(){
console.log('proposePurpose()');
const amount = entry.amount;
const source = entry.source;
const destination = entry.destination;
const url = api(`accounting/${account.id}/purposes`);
const res = await post(url,{source,destination,amount});
if (res.ok) {
yikes();
var lastTransaction = await res.json();
console.log({lastTransaction,users:JSON.parse(JSON.stringify(users))});
entry.purpose = { display: lastTransaction.purpose};
entry.tags = lastTransaction.tags;
if (lastTransaction.source.value){
if (users[lastTransaction.source.value]){
let user = users[lastTransaction.source.value];
entry.source = { id : +lastTransaction.source.value, display : user.name };
} else entry.source = { display: lastTransaction.source.value };
}
if (lastTransaction.destination.value){
if (users[lastTransaction.destination.value]){
let user = users[lastTransaction.destination.value];
entry.destination = { id : +lastTransaction.destination.value, display : user.name };
} else entry.destination = { display: lastTransaction.destination.value };
}
} else error(res);
}
async function save(){
let data = {
...entry,
purpose: entry.purpose.display
}
let url = api('accounting');
let res = await post(url, data);
if (res.ok) {
yikes();
if (new_account){
router.navigate('/accounting');
return;
}
//entry.tags = [];
onSave();
document.getElementById('date-input').focus();
} else error(res);
}
</script>
<style>
hr{
grid-column: 1 / -1;
margin: 0.5rem 0;
border: 0;
height: 1px;
align-self: center;
background: red;
}
</style>
<fieldset class="grid2 new_transaction">
{#if new_account}
<legend>{t('create_new_object',{object:t('account')})}</legend>
<span style="display:none"></span>
<span>{t('account name')}</span>
<span>
<input type="text" bind:value={entry.account.name} />
</span>
<span>{t('currency')}</span>
<span>
<input type="text" bind:value={entry.account.currency} />
</span>
<hr/>
<span style="grid-column-end: span 2">{t('first transaction')}</span>
{:else}
<legend>{t('add_object',{object:t('transaction')})}</legend>
<span style="display:none"></span>
{/if}
<span>{t('date')}</span>
<span>
<input type="date" bind:value={entry.date} id="date-input" />
</span>
<span>{t('amount')}</span>
<span>
<input type="number" bind:value={entry.amount} onkeyup={e => focusOnEnter(e,'source-input')} />&nbsp;{entry.account.currency}
</span>
<span>{t('source')}</span>
<Autocomplete bind:candidate={entry.source} getCandidates={getSources} id="source-input" />
<span>{t('destination')}</span>
<Autocomplete bind:candidate={entry.destination} getCandidates={getDestinations} onSelect={dst_selected} />
<span>{t('purpose')}</span>
<Autocomplete bind:candidate={entry.purpose} getCandidates={getPurposes} onCommit={gotoTags} id="purpose_input" />
<span>{t('tags')}</span>
<Tags getCandidates={getAccountTags} module={null} bind:tags={entry.tags} onEmptyCommit={save} />
<span></span>
<span>
<button onclick={save}>{t('save')}</button>
</span>
</fieldset>
+2 -2
View File
@@ -44,9 +44,9 @@
</span>
<ul>
{#each accounts as account (account.id)}
{#each accounts.toSorted((a,b) => a.name.localeCompare(b.name)) as account (account.id)}
<li>
<a {onclick} href="/account/{account.id}">{account.name} ({account.id})</a>
<a {onclick} href="/account/{account.id}">{account.name}</a>
</li>
{/each}
</ul>
+68 -74
View File
@@ -1,7 +1,7 @@
<script>
import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte.js';
import { api, post } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js';
@@ -21,13 +21,9 @@
});
async function onsubmit(ev){
ev.preventDefault();
if (ev) ev.preventDefault();
const url = api('project');
var resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : JSON.stringify(project)
});
var resp = await post(url,project);
if (resp.ok){
var newProject = await resp.json();
router.navigate(`/project/${newProject.id}/view`);
@@ -50,72 +46,70 @@
label{ display: block }
</style>
<form {onsubmit}>
<fieldset>
<legend>
{t('create_new_project')}
</legend>
<fieldset>
<legend>
{t('create_new_project')}
</legend>
<fieldset>
<legend>{t('basic_data')}</legend>
<table>
<tbody>
<tr>
<th>
{t('company_optional')}
</th>
<td>
<CompanySelector caption={t('no_company')} {onselect} />
</td>
</tr>
<tr>
<th>
{t('Name')}
</th>
<td>
<input type="text" bind:value={project.name}/>
</td>
</tr>
<tr>
<th>
{t('description')}
</th>
<td>
<MarkdownEditor bind:value={project.description} simple={true} />
</td>
</tr>
{#if showSettings}
<tr>
<th>
{t('settings')}
</th>
<td>
<label>
<input type="checkbox" bind:checked={project.settings.show_closed} />
{t('display_closed_tasks')}
</label>
</td>
</tr>
{:else}
<tr>
<th>
{t('settings')}
</th>
<td>
<button onclick={toggleSettings} >{t('extended_settings')}</button>
</td>
</tr>
{/if}
<tr>
<th>
{t('tags')}
</th>
<td>
<Tags module="project" bind:tags={project.tags} />
</td>
</tr>
</tbody>
</table>
</fieldset>
<button type="submit" disabled={!ready}>{t('create')}</button>
<legend>{t('basic_data')}</legend>
<table>
<tbody>
<tr>
<th>
{t('company_optional')}
</th>
<td>
<CompanySelector caption={t('no_company')} {onselect} />
</td>
</tr>
<tr>
<th>
{t('Name')}
</th>
<td>
<input type="text" bind:value={project.name}/>
</td>
</tr>
<tr>
<th>
{t('description')}
</th>
<td>
<MarkdownEditor bind:value={project.description} simple={true} />
</td>
</tr>
{#if showSettings}
<tr>
<th>
{t('settings')}
</th>
<td>
<label>
<input type="checkbox" bind:checked={project.settings.show_closed} />
{t('display_closed_tasks')}
</label>
</td>
</tr>
{:else}
<tr>
<th>
{t('settings')}
</th>
<td>
<button onclick={toggleSettings} >{t('extended_settings')}</button>
</td>
</tr>
{/if}
<tr>
<th>
{t('tags')}
</th>
<td>
<Tags module={null} bind:tags={project.tags} onEmptyCommit={onsubmit} />
</td>
</tr>
</tbody>
</table>
</fieldset>
</form>
<button onclick={onsubmit} disabled={!ready}>{t('create')}</button>
</fieldset>
+2 -1
View File
@@ -79,6 +79,7 @@
task.members[assignee] = project.members[assignee];
task.members[assignee].permission = { name : "ASSIGNEE", code : 3 }
}
if (task.taks.length < 1) task.tags = project.tags;
yikes();
} else {
error(resp);
@@ -154,7 +155,7 @@
</div>
<div>{t('tags')}</div>
<div>
<Tags module="task" bind:tags={task.tags} />
<Tags module={null} bind:tags={task.tags} onEmptyCommit={saveTask} />
</div>
{#if extendedSettings}
+1 -1
View File
@@ -137,7 +137,7 @@
<legend>{t('state_open')}</legend>
{#if sorted}
{#each sorted as task}
{#if task.status == 20 && match(task)}
{#if task.status < 60 && task.status >= 20 && match(task)}
<div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
{task.name}
</div>
+11 -3
View File
@@ -1,7 +1,7 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, drop, patch, post } from '../../urls.svelte.js';
import { api, drop, get, patch, post } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js';
import { timetrack } from '../../user.svelte.js';
@@ -41,10 +41,18 @@
});
async function addTime(task_id){
const url = api(`time/track_task/${task_id}`);
const resp = await post(url,now()); // create new time or return time with assigned tasks
let url = api(`time/track_task/${task_id}`);
let resp = await post(url,now()); // create new time or return time with assigned tasks
if (resp.ok) {
const track = await resp.json();
if (timetrack.running){
url = api(`time/${timetrack.running.id}`);
resp = await get(url);
if (resp.ok){
let previous = await resp.json();
times[previous.id] = previous;
}
}
timetrack.running = track;
} else {
error(resp);
@@ -3,9 +3,7 @@ package de.srsoftware.umbrella.legacy;
import static de.srsoftware.tools.Optionals.nullable;
import static de.srsoftware.umbrella.core.ModuleRegistry.noteService;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.Util.markdown;
import static de.srsoftware.umbrella.core.ModuleRegistry.*;
import static de.srsoftware.umbrella.core.constants.Field.TOKEN;
import static de.srsoftware.umbrella.core.constants.Field.URI;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField;
@@ -72,7 +70,7 @@ public class NotesLegacy extends BaseHandler {
new Tag("fieldset")
.add(new Tag("legend").content(authorName))
.add(new Tag("legend").content(note.timestamp().format(DateTimeFormatter.ISO_DATE_TIME)))
.add(new Tag("div").content(markdown(note.text())))
.add(new Tag("div").content(markdownService().markdown(note.text())))
.addTo(html);
}
return sendContent(ex,html.toString(2));
@@ -3,21 +3,50 @@ package de.srsoftware.umbrella.markdown;
import static de.srsoftware.tools.MimeType.MIME_HTML;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.constants.Field.BASE_URL;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField;
import static java.lang.System.Logger.Level.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.sun.net.httpserver.HttpExchange;
import com.xrbpowered.jparsedown.JParsedown;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Path;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.api.MarkdownService;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
public class MarkdownApi extends BaseHandler implements MarkdownService {
private static final System.Logger LOG = System.getLogger(MarkdownApi.class.getSimpleName());
private static final Pattern PATTERN_ANCHOR = Pattern.compile("(?is)<a\\b[^>]*>.*?</a>");
private static final Pattern PATTERN_HREF = Pattern.compile("(?i)\\bhref\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))");
private static final Pattern PATTERN_SPREADSHEET = Pattern.compile("@startsheet(.*?)@endsheet",Pattern.DOTALL);
private static final Pattern PATTERN_TARGET = Pattern.compile("(?i)\\btarget\\s*=");
private static final Pattern PATTERN_URL = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL);
private static final Map<Integer,String> umlCache = new HashMap<>();
private static final JParsedown MARKDOWN = new JParsedown();
public MarkdownApi() {
private final String baseUrl;
private final File plantumlJar;
public MarkdownApi(Configuration config) {
super();
Optional<String> baseUrl = config.get(BASE_URL);
if (baseUrl.isEmpty()) throw missingField(BASE_URL);
this.baseUrl = baseUrl.get();
plantumlJar = config.get("umbrella.plantuml").map(Object::toString).map(File::new).filter(File::exists).orElse(null);
if (plantumlJar != null) LOG.log(INFO,"Using plant uml @ {}",plantumlJar);
ModuleRegistry.add(this);
}
@@ -28,11 +57,98 @@ public class MarkdownApi extends BaseHandler implements MarkdownService {
var user = userService().refreshSession(ex);
if (user.isEmpty()) throw UmbrellaException.forbidden("You must be logged in to use the markdown renderer!");
var rendered = Util.markdown(body(ex));
var rendered = markdown(body(ex));
ex.getResponseHeaders().add(CONTENT_TYPE,MIME_HTML);
return sendContent(ex,rendered);
} catch (UmbrellaException e){
return send(ex,e);
}
}
public String markdown(String source){
if (source == null || source.isBlank()) return source;
try {
var matcher = PATTERN_SPREADSHEET.matcher(source);
var count = 0;
while (matcher.find()){
LOG.log(DEBUG,"Processing spreadsheet code…");
count++;
var sheetData = matcher.group(0).trim();
var start = matcher.start(0);
var end = matcher.end(0);
source = source.substring(0, start)
+ "<div class=\"spreadsheet\" id=\"spreadsheet-"+count+"\">"
+ sheetData.substring(11,sheetData.length()-10)
+ "</div>"
+ source.substring(end);
LOG.log(DEBUG,"Updated markdown with spreadsheet div.");
matcher.reset(source);
}
if (plantumlJar != null && plantumlJar.exists()) {
matcher = PATTERN_URL.matcher(source);
while (matcher.find()) {
var uml = matcher.group(0).trim();
var start = matcher.start(0);
var end = matcher.end(0);
var umlHash = uml.hashCode();
LOG.log(DEBUG,"Hash of Plantuml code: {0}",umlHash);
var svg = umlCache.get(umlHash);
if (svg != null){
LOG.log(DEBUG,"Serving Plantuml generated SVG from cache…");
source = source.substring(0, start) + svg + source.substring(end);
matcher.reset(source);
continue;
}
LOG.log(DEBUG,"Cache miss. Generating SVG from plantuml code…");
ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", plantumlJar.getAbsolutePath(), "-tsvg", "-pipe");
var ignored = processBuilder.redirectErrorStream();
var process = processBuilder.start();
try (OutputStream os = process.getOutputStream()) {
os.write(uml.getBytes(UTF_8));
os.flush();
}
try (InputStream is = process.getInputStream()) {
byte[] out = is.readAllBytes();
LOG.log(DEBUG,"Generated SVG. Pushing to cache…");
svg = new String(out, UTF_8);
umlCache.put(umlHash,svg);
source = source.substring(0, start) + svg + source.substring(end);
matcher.reset(source);
}
}
}
var rendered = MARKDOWN.text(source);
if (baseUrl == null) return rendered;
var anchors = PATTERN_ANCHOR.matcher(rendered);
while (anchors.find()){
String anchor = anchors.group();
LOG.log(TRACE,"Processing anchor: {}",anchor);
var urls = PATTERN_HREF.matcher(anchor);
if (!urls.find()) continue; // no url nothing to do
var href = urls.group(1) != null ? urls.group(1) : (urls.group(2) != null ? urls.group(2) : urls.group(3));
LOG.log(TRACE," encountered href = {}",href);
if (!href.startsWith("http")) continue; // relative url? good!
LOG.log(TRACE," {} is not a relative url!",href);
var target = PATTERN_TARGET.matcher(anchor);
if (target.find()) continue; // target already set leave untouched
LOG.log(TRACE," anchor has no target!");
if (href.startsWith(baseUrl)) continue; // local url don`t touch
LOG.log(TRACE," {} is not an internal url, adding anchor…",href);
var replacement = "<a target=\"_blank\""+anchor.substring(2);
rendered = rendered.replace(anchor, replacement);
anchors.reset(rendered);
}
return rendered;
} catch (Throwable e){
if (LOG.isLoggable(TRACE)){
LOG.log(TRACE,"Failed to render markdown, input was: \n{0}",source,e);
} else LOG.log(WARNING,"Failed to render markdown. Enable TRACE log level for details.");
return source;
}
}
}
@@ -21,7 +21,6 @@ import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
import static de.srsoftware.umbrella.project.Constants.PERMISSIONS;
import static de.srsoftware.umbrella.task.Constants.*;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.WARNING;
import static java.net.URLDecoder.decode;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -199,7 +198,6 @@ public class TaskModule extends BaseHandler implements TaskService {
var projectTasks = taskDb.listProjectTasks(project.id(),null, false);
var mapped = projectTasks.values().stream().collect(Collectors.toMap(Task::id,Task::toMap));
var roots = new HashMap<Long,Map<String,Object>>();
LOG.log(ERROR,"getParentCandidates not fully functional");
for (var map : mapped.values()){
if (!(map.get(ID) instanceof Long id)) continue;
if (id == taskId) continue;
@@ -79,8 +79,15 @@ public class TimeModule extends BaseHandler implements TimeService {
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head) {
case STARTED -> getStartedTime(user.get(),ex);
case null, default -> super.doGet(path,ex);
case STARTED -> getStartedTime(user.get(),ex);
case null -> super.doGet(path,ex);
default -> {
try {
yield sendContent(ex,timeDb.load(Long.parseLong(head)));
} catch (NumberFormatException ignored) {
yield super.doGet(path,ex);
}
}
};
} catch (UmbrellaException e){
return send(ex,e);
+3
View File
@@ -17,6 +17,7 @@
"abort": "abbrechen",
"account": "Konto",
"accounting": "Konten",
"account name": "Konto-Name",
"accounts": "Konten",
"actions": "Aktionen",
"add_login_service": "Login-Service anlegen",
@@ -75,6 +76,7 @@
"CREATE_USERS": "Nutzer anlegen",
"create_pdf": "PDF erzeugen",
"created_with": "erzeugt mit {tool} von {producer}",
"currency": "Währung",
"customer": "Kunde",
"customer_address": "Adresse",
"customer_email": "Emailadresse des Kunden",
@@ -162,6 +164,7 @@
"files": "Dateien",
"filter": "Filter",
"filter by tags": "Nach Tags filtern",
"first_transaction": "erste Transaktion",
"footer": "Fuß-Text",
"foreign_id": "externe Kennung",
"forgot_pass" : "Password vergessen?",
+5 -2
View File
@@ -17,6 +17,7 @@
"abort": "abort",
"account": "account",
"accounting": "accounting",
"account name": "Account name",
"accounts": "accounts",
"actions": "actions",
"add_login_service": "add login service",
@@ -75,6 +76,7 @@
"CREATE_USERS": "create users",
"create_pdf": "create PDF",
"created_with": "created with {tool} by {producer}",
"currency": "Currency",
"customer": "customer",
"customer_address": "address",
"customer_email": "customer email address",
@@ -162,6 +164,7 @@
"files": "files",
"filter": "filter",
"filter by tags": "filter by tags",
"first_transaction": "first transaction",
"footer": "footer",
"foreign_id": "external ID",
"forgot_pass" : "forgot password?",
@@ -459,11 +462,11 @@
"wiki_pages": "wiki pages",
"year": "year",
"You have been added to the new project '{project}', created by {user}:\n\n{body}": "You have been added to the new project '{project}', created by {user}:\n\n{body}",
"You can view/edit this transaction at {base_url}/account/{id}": "Du kannst diese transaktion unter {base_url}/account/{id} ansehen",
"You can view/edit this transaction at {base_url}/account/{id}": "You can view/edit this transaction at {base_url}/account/{id}",
"You can view/edit this project at {base_url}/project/{id}/view": "You can view/edit this project at {base_url}/project/{id}/view",
"You can view/edit this task at {base_url}/task/{id}/view": "You can view/edit this task at {base_url}/task/{id}/view",
"You can view/edit this wiki page at {base_url}/wiki/{id}/view": "You can view/edit this wiki page at {base_url}/wiki/{id}/view",
"You have been added to the new project '{project}', created by {user}:\n\n{body}": "You have been added to the new project '{project}', created by {user}:\n\n{body}",
"You may change your notification settings at {base_url}/message/settings": "You may change your notification settings at {base_url}/message/settings .",
"Your token to create a new password" : "Your token to create a new password",
"your_profile": "your profile"
@@ -74,6 +74,7 @@ tr:hover .taglist .tag button {
color: black;
}
code,
.code{
color: chocolate;
}
+2 -1
View File
@@ -15,9 +15,10 @@ body {
background-position: 98% 70px;
background-attachment: fixed;
}
.code,
code {
font-size: 16px;
font-family: monospace;
}
fieldset {