Compare commits

...

28 Commits

Author SHA1 Message Date
StephanRichter b7f82018f9 extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 22:53:49 +01:00
StephanRichter a245f849e5 extended demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 21:19:06 +01:00
StephanRichter 4c3312daca Merge branch 'main' into demodata 2026-01-28 20:45:37 +01:00
StephanRichter a6983780f5 enabling creation of tag database
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
StephanRichter 6e146c1577 extending demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 20:42:56 +01:00
StephanRichter 49cbd328e6 started creating demo data
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 20:26:31 +01:00
StephanRichter 1db28b5bc0 improved note titles for documents
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
StephanRichter 285343218b added translations
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 15:33:59 +01:00
StephanRichter 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
StephanRichter 4339e4fd98 added notification to search
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-28 08:44:52 +01:00
StephanRichter 16cbcdff60 Merge branch 'feature/wiki_fulltext_search'
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
StephanRichter 8dc77f52e4 Merge branch 'search_order' 2026-01-27 23:38:31 +01:00
StephanRichter b04b9b92a6 Merge branch 'docker-optimize'
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
StephanRichter 8d66ed4cbe implemented full-text search for wiki
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 22:05:56 +01:00
StephanRichter 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
StephanRichter a321c813de working on timezone problems
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 15:34:33 +01:00
StephanRichter fda40d72f8 re-ordered docker commands
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 00:46:45 +01:00
StephanRichter 2375746d91 further optimization of dockerfile
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-27 00:29:43 +01:00
StephanRichter cf0cf2f5e9 fixed missing permissions for setting localtime
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-23 23:17:00 +01:00
StephanRichter 3e71ecc6cb optimized dockerfile
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-23 23:09:25 +01:00
StephanRichter 1302165ab2 fixing timezone for local deployment
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-23 23:00:41 +01:00
StephanRichter d08138c9e1 adding debug messages
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
StephanRichter 4cd1ea3277 now caching plantuml diagrams
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-22 23:46:20 +01:00
StephanRichter 1059164b4a bugfix:
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
StephanRichter f438bea4cc Merge branch 'bugfix/plantuml'
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
StephanRichter 9394ca597c fixed another bug
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-20 22:52:30 +01:00
StephanRichter 53fe79fbbd fixed image heights when scaling width
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-20 22:44:30 +01:00
StephanRichter d62534b3eb fixed plantuml permissions
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-20 22:20:17 +01:00
34 changed files with 223 additions and 53 deletions
+15 -10
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
+1 -1
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
@@ -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
View File
@@ -0,0 +1 @@
*.db-journal
Binary file not shown.
Binary file not shown.
+68
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"
}
}
}
}
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.
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.
@@ -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));
}
+62 -21
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}
@@ -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);
@@ -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
+1
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",
+1
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",
+2 -1
View File
@@ -9,4 +9,5 @@ tasks.processResources {
from("../frontend/dist") {
into("web")
}
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
+2 -1
View File
@@ -57,8 +57,9 @@ footer {
margin: 5px;
}
img {
img, svg {
max-width: 100%;
height: auto !important;
}
nav {
+2 -1
View File
@@ -57,8 +57,9 @@ footer {
margin: 5px;
}
img {
img, svg {
max-width: 100%;
height: auto !important;
}
nav {
+8 -7
View File
@@ -57,17 +57,18 @@ footer {
margin: 5px;
}
img {
img, svg {
max-width: 100%;
height: auto !important;
}
nav {
position: sticky;
z-index: 100;
top: 0;
padding: 5px;
margin: 0 0 10px 0;
border-bottom: 1px solid;
position: sticky;
z-index: 100;
top: 0;
padding: 5px;
margin: 0 0 10px 0;
border-bottom: 1px solid;
}
td, tr{
@@ -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);