Compare commits

...

27 Commits

Author SHA1 Message Date
b7f82018f9 extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 22:53:49 +01:00
a245f849e5 extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 21:19:06 +01:00
4c3312daca Merge branch 'main' into demodata 2026-01-28 20:45:37 +01:00
a6983780f5 enabling creation of tag database
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m13s
Build Docker Image / Clean-Registry (push) Successful in -2s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 20:44:01 +01:00
6e146c1577 extending demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 20:42:56 +01:00
49cbd328e6 started creating demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 20:26:31 +01:00
1db28b5bc0 improved note titles for documents
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m26s
Build Docker Image / Clean-Registry (push) Successful in -2s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 15:52:27 +01:00
285343218b added translations
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 15:33:59 +01:00
f402122d0c improved search: motes now showing name/title of related entities
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 15:22:20 +01:00
4339e4fd98 added notification to search
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 08:44:52 +01:00
16cbcdff60 Merge branch 'feature/wiki_fulltext_search'
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m17s
Build Docker Image / Clean-Registry (push) Successful in -2s
2026-01-27 23:38:46 +01:00
8dc77f52e4 Merge branch 'search_order' 2026-01-27 23:38:31 +01:00
b04b9b92a6 Merge branch 'docker-optimize'
Some checks failed
Build Docker Image / Clean-Registry (push) Has been cancelled
Build Docker Image / Docker-Build (push) Has been cancelled
2026-01-27 23:37:39 +01:00
8d66ed4cbe implemented full-text search for wiki
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 22:05:56 +01:00
b0ee825e0a moved notes to the bottom of the search result list
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 21:41:12 +01:00
a321c813de working on timezone problems
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 15:34:33 +01:00
fda40d72f8 re-ordered docker commands
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 00:46:45 +01:00
2375746d91 further optimization of dockerfile
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 00:29:43 +01:00
cf0cf2f5e9 fixed missing permissions for setting localtime
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-23 23:17:00 +01:00
3e71ecc6cb optimized dockerfile
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-23 23:09:25 +01:00
1302165ab2 fixing timezone for local deployment
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-23 23:00:41 +01:00
d08138c9e1 adding debug messages
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m32s
Build Docker Image / Clean-Registry (push) Successful in -1s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-23 22:36:56 +01:00
4cd1ea3277 now caching plantuml diagrams
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-22 23:46:20 +01:00
1059164b4a bugfix:
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m36s
Build Docker Image / Clean-Registry (push) Successful in 0s
markdown rendering glitched when several @startuml…@enduml sections were present in one document

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-22 20:46:43 +01:00
f438bea4cc Merge branch 'bugfix/plantuml'
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m25s
Build Docker Image / Clean-Registry (push) Successful in 0s
2026-01-20 23:14:46 +01:00
9394ca597c fixed another bug
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-20 22:52:30 +01:00
d62534b3eb fixed plantuml permissions
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-20 22:20:17 +01:00
31 changed files with 211 additions and 44 deletions

View File

@@ -1,15 +1,15 @@
FROM alpine:3.22 AS svelte_build
RUN apk add npm
RUN adduser -Dh /home/svelte svelte
ADD . /home/svelte/Umbrella
ADD frontend /home/svelte/Umbrella/frontend
RUN chown -R svelte /home/svelte/Umbrella
USER svelte
WORKDIR /home/svelte/Umbrella/frontend
RUN npm install && npm run build
FROM alpine AS java_build
RUN apk add gradle fontconfig font-opensans openjdk21-jre
FROM alpine:3.22 AS java_build
RUN apk add gradle
ADD . /Umbrella
WORKDIR /Umbrella
COPY --from=svelte_build /home/svelte/Umbrella/frontend/dist web/src/main/resources/web
@@ -17,13 +17,18 @@ RUN gradle --no-daemon build
FROM alpine
RUN apk add bash fontconfig font-opensans graphviz openjdk21-jre weasyprint
RUN adduser -D umbrella
COPY --from=java_build /Umbrella/backend/build/libs/backend.jar /home/umbrella/jar/
RUN chown -R umbrella /home/umbrella
ADD https://github.com/plantuml/plantuml/releases/download/v1.2025.10/plantuml-1.2025.10.jar /home/umbrella/plantuml.jar
USER umbrella
RUN apk --no-cache add bash fontconfig font-opensans graphviz openjdk21-jre tzdata weasyprint \
&& adduser -D umbrella
WORKDIR /home/umbrella
RUN mkdir .config && ln -s /host/config.json .config/Umbrella.json
EXPOSE 80
CMD java -jar jar/backend.jar
ADD https://github.com/plantuml/plantuml/releases/download/v1.2025.10/plantuml-1.2025.10.jar /home/umbrella/plantuml.jar
COPY --from=java_build /Umbrella/backend/build/libs/backend.jar /home/umbrella/jar/
RUN mkdir .config \
&& ln -s /host/config.json .config/Umbrella.json \
&& chmod a+rx plantuml.jar \
&& chown -R umbrella . \
&& ln -s /usr/share/zoneinfo/Europe/Berlin /etc/localtime
USER umbrella

View File

@@ -1,5 +1,5 @@
FROM alpine:3.22
LABEL Maintainer "Stephan Richter <s.richter@srsoftware.de>"
LABEL Maintainer "Stephan Richter"
ARG UID=1000
ARG GID=1000
RUN apk add bash npm

View File

@@ -35,11 +35,12 @@ 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 UML_PATTERN = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL);
private static File plantumlJar = null;
private static final JParsedown MARKDOWN = new JParsedown();
public static final String SHA1 = "SHA-1";
private static final MessageDigest SHA1_DIGEST;
private static final Map<Integer,String> umlCache = new HashMap<>();
static {
try {
@@ -79,11 +80,22 @@ public class Util {
try {
if (plantumlJar != null && plantumlJar.exists()) {
var matcher = UML_PATTERN.matcher(source);
if (matcher.find()) {
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();
@@ -94,8 +106,11 @@ public class Util {
try (InputStream is = process.getInputStream()) {
byte[] out = is.readAllBytes();
var svg = new String(out, UTF_8);
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);
}
}
}

1
demodata/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.db-journal

BIN
demodata/bookmark.db Normal file

Binary file not shown.

BIN
demodata/company.db Normal file

Binary file not shown.

68
demodata/config.json Normal file
View File

@@ -0,0 +1,68 @@
{
"umbrella": {
"base_url": "http://127.0.0.1:5173",
"logging": {
"rootLevel": "INFO"
},
"http": {
"port": 8080
},
"threads": 16,
"modules": {
"notes": {
"database": "demodata/notes.db"
},
"document": {
"database": "demodata/documents.db",
"templates": "demodata/templates"
},
"wiki": {
"database": "demodata/wiki.db"
},
"project": {
"database": "demodata/projects.db"
},
"message": {
"database": "demodata/message.db",
"smtp": {
"pass": "none",
"port": 587,
"host": "none",
"user": "none"
}
},
"tags": {
"database": "demodata/tags.db"
},
"bookmark": {
"database": "demodata/bookmark.db"
},
"task": {
"database": "demodata/tasks.db"
},
"journal": {
"database": "demodata/journal.db"
},
"contact": {
"database": "demodata/contacts.db"
},
"files": {
"database": "demodata/files.db",
"base_dir": "demodata/filestore"
},
"company": {
"database": "demodata/company.db"
},
"time": {
"database": "demodata/times.db"
},
"stock": {
"database": "demodata/stock.db"
},
"items": {},
"user": {
"database": "demodata/users.db"
}
}
}
}

BIN
demodata/contacts.db Normal file

Binary file not shown.

BIN
demodata/documents.db Normal file

Binary file not shown.

BIN
demodata/files.db Normal 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

BIN
demodata/message.db Normal file

Binary file not shown.

BIN
demodata/notes.db Normal file

Binary file not shown.

BIN
demodata/projects.db Normal file

Binary file not shown.

BIN
demodata/stock.db Normal file

Binary file not shown.

BIN
demodata/tags.db Normal file

Binary file not shown.

BIN
demodata/tasks.db Normal file

Binary file not shown.

BIN
demodata/times.db Normal file

Binary file not shown.

BIN
demodata/users.db Normal file

Binary file not shown.

BIN
demodata/wiki.db Normal file

Binary file not shown.

View File

@@ -541,6 +541,11 @@ public class DocumentApi extends BaseHandler implements DocumentService {
var userCompanyIds = companyService().listCompaniesOf(user).keySet();
var documents = db.find(userCompanyIds,keys,fulltext);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return sendContent(ex,mapValues(documents));
}

View File

@@ -1,14 +1,15 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, post, target } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte';
import { api, get, post, target } from '../../urls.svelte.js';
import { error, warn, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js';
import { display } from '../../time.svelte';
import Bookmark from '../bookmark/Template.svelte';
const router = useTinyRouter();
let counter = 9;
let bookmarks = $state(null);
let companies = $state(null);
let documents = $state(null);
@@ -34,12 +35,12 @@
});
function doSearch(ignored){
warn(t('searching…'));
let url = window.location.origin + window.location.pathname;
if (key) url += '?key=' + encodeURI(key);
window.history.replaceState(history.state, '', url);
const data = { key : key, fulltext : fulltext };
post(api('bookmark/search'),data).then(handleBookmarks);
post(api('company/search '),data).then(handleCompanies);
post(api('document/search'),data).then(handleDocuments);
@@ -51,6 +52,22 @@
post(api('wiki/search' ),data).then(handleWikiPages);
}
async function getTitle(key,module,entity_id){
get(api(module+'/'+entity_id)).then(res => setTitle(res,key,module))
}
async function setTitle(resp,key,module){
if (resp.ok){
const json = await resp.json();
if (json.name) notes[key].title = t(module)+": "+json.name;
if (json.title) notes[key].title = t(module)+": "+json.title;
if (module == 'document'){
notes[key].title = t(json.type)+" "+json.number;
}
}
}
function onclick(e){
e.preventDefault();
var target = e.target;
@@ -61,6 +78,7 @@
}
async function handleBookmarks(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
bookmarks = Object.keys(res).length ? res : null;
@@ -70,6 +88,7 @@
}
async function handleCompanies(resp){
quitOne();
if (resp.ok){
const json = await resp.json();
companies = Object.keys(json).length ? json : null;
@@ -79,6 +98,7 @@
}
async function handleDocuments(resp){
quitOne();
if (resp.ok){
const json = await resp.json();
documents = Object.keys(json).length ? json : null;
@@ -88,15 +108,25 @@
}
async function handleNotes(resp){
quitOne();
if (resp.ok){
const json = await resp.json();
notes = Object.keys(json).length ? json : null;
if ( Object.keys(json).length ) {
for (let key of Object.keys(json)){
let module = json[key].module;
let entity_id = json[key].entity_id;
json[key].title = t(module)+' '+entity_id;
getTitle(key,module,entity_id);
}
notes = json;
} else notes = null;
} else {
error(resp);
}
}
async function handleProjects(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
projects = Object.keys(res).length ? res : null;
@@ -106,6 +136,7 @@
}
async function handleStock(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
stock = Object.keys(res).length ? res : null;
@@ -115,6 +146,7 @@
}
async function handleTasks(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
tasks = Object.keys(res).length ? res : null;
@@ -124,6 +156,7 @@
}
async function handleTimes(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
times = Object.keys(res).length ? res : null;
@@ -133,6 +166,7 @@
}
async function handleWikiPages(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
pages = Object.keys(res).length ? res : null;
@@ -140,6 +174,13 @@
error(resp);
}
}
function quitOne(){
counter--;
if (counter > 0) {
warn(t('searching…')+" "+counter);
} else yikes();
}
$effect(() => doSearch(key))
</script>
@@ -236,23 +277,6 @@
</ul>
</fieldset>
{/if}
{#if notes}
<fieldset>
<legend>
{t('notes')}
</legend>
<ul>
{#each Object.values(notes) as note}
<li>
<b>
<a href="/{note.module}/{note.entity_id}/view" {onclick} >{t(note.module)} {note.entity_id}:</a>
</b>
{@html target(note.text.rendered)}
</li>
{/each}
</ul>
</fieldset>
{/if}
{#if times}
<fieldset>
<legend>
@@ -308,3 +332,20 @@
</ul>
</fieldset>
{/if}
{#if notes}
<fieldset>
<legend>
{t('notes')}
</legend>
<ul>
{#each Object.values(notes) as note}
<li>
<b>
<a href="/{note.module}/{note.entity_id}/view" {onclick} >{note.title}</a>
</b>
{@html target(note.text.rendered)}
</li>
{/each}
</ul>
</fieldset>
{/if}

View File

@@ -132,7 +132,23 @@ public class StockModule extends BaseHandler implements StockService {
yield super.doGet(path,ex);
}
}
case null, default -> super.doGet(path,ex);
case null -> super.doGet(path,ex);
default -> {
try {
var id = Long.parseLong(head);
Item item = stockDb.loadItem(id);
Owner owner = item.owner().resolve();
if (owner instanceof Company company) {
if (!companyService().membership(company.id(),user.get().id())) throw forbidden("You are not allowed to access {0}",OBJECT);
}
if (owner instanceof UmbrellaUser u){
if (u.id() != user.get().id()) throw forbidden("You are not allowed to access {0}",OBJECT);
}
yield sendContent(ex,item);
} catch (NumberFormatException nfe){
yield super.doGet(path,ex);
}
}
};
} catch (UmbrellaException e){
return send(ex,e);

View File

@@ -39,6 +39,7 @@ public class SqliteDb extends BaseDb implements TagDB{
public SqliteDb(Connection tagDb, Connection bmDb) {
super(tagDb);
bookmarks = new de.srsoftware.umbrella.bookmarks.SqliteDb(bmDb);
createTables();
}
@Override

View File

@@ -278,6 +278,7 @@
"saved": "gespeichert",
"save_object": "{object} speichern",
"search": "Suche",
"searching…": "suche…",
"select_company" : "Wählen Sie eine ihrer Firmen:",
"select_customer": "Kunde auswählen",
"select_property": "Eigenschaft auswählen",

View File

@@ -278,6 +278,7 @@
"saved": "saved",
"save_object": "save {object}",
"search": "search",
"searching…": "searhcing…",
"select_company" : "select on of you companies:",
"select_customer": "select customer",
"select_property": "select property",

View File

@@ -9,4 +9,5 @@ tasks.processResources {
from("../frontend/dist") {
into("web")
}
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

View File

@@ -152,15 +152,27 @@ public class SqliteDb extends BaseDb implements WikiDb {
@Override
public Map<Long, WikiPage> find(long userId, List<String> keys, boolean fulltext) {
try {
var query = select(ALL).from(TABLE_PAGES).leftJoin(ID,TABLE_PAGES_USERS,PAGE_ID).where(USER_ID,equal(userId));
for (var key : keys) query.where(TITLE,like("%"+key+"%"));
var rs = query.exec(db);
var map = new HashMap<Long,WikiPage>();
while (rs.next()) {
var page = WikiPage.of(rs);
map.put(page.id(),page);
{
var query = select(ALL).from(TABLE_PAGES).leftJoin(ID, TABLE_PAGES_USERS, PAGE_ID).where(USER_ID, equal(userId));
for (var key : keys) query.where(TITLE, like("%" + key.replaceAll("[ÄäÖöÜüß]", "%") + "%"));
var rs = query.exec(db);
while (rs.next()) {
var page = WikiPage.of(rs);
map.put(page.id(), page);
}
rs.close();
}
if (fulltext) {
var query = select(ALL).from(TABLE_PAGES).leftJoin(ID, TABLE_PAGES_USERS, PAGE_ID).where(USER_ID, equal(userId));
for (var key : keys) query.where(CONTENT, like("%" + key.replaceAll("[ÄäÖöÜüß]", "%") + "%"));
var rs = query.exec(db);
while (rs.next()) {
var page = WikiPage.of(rs);
map.put(page.id(), page);
}
rs.close();
}
rs.close();
return map;
} catch (SQLException e) {
throw failedToSearchDb(t(WIKI_PAGES)).causedBy(e);