Compare commits

..

3 Commits

33 changed files with 62 additions and 210 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 frontend /home/svelte/Umbrella/frontend
ADD . /home/svelte/Umbrella
RUN chown -R svelte /home/svelte/Umbrella
USER svelte
WORKDIR /home/svelte/Umbrella/frontend
RUN npm install && npm run build
FROM alpine:3.22 AS java_build
RUN apk add gradle
FROM alpine AS java_build
RUN apk add gradle fontconfig font-opensans openjdk21-jre
ADD . /Umbrella
WORKDIR /Umbrella
COPY --from=svelte_build /home/svelte/Umbrella/frontend/dist web/src/main/resources/web
@@ -17,18 +17,14 @@ RUN gradle --no-daemon build
FROM alpine
RUN apk --no-cache add bash fontconfig font-opensans graphviz openjdk21-jre tzdata weasyprint \
&& adduser -D umbrella
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
WORKDIR /home/umbrella
RUN chmod a+rx plantuml.jar
USER 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"
LABEL Maintainer "Stephan Richter <s.richter@srsoftware.de>"
ARG UID=1000
ARG GID=1000
RUN apk add bash npm

1
demodata/.gitignore vendored
View File

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

Binary file not shown.

Binary file not shown.

View File

@@ -1,68 +0,0 @@
{
"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.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -541,11 +541,6 @@ 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,15 +1,14 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, get, post, target } from '../../urls.svelte.js';
import { error, warn, yikes } from '../../warn.svelte';
import { api, post, target } from '../../urls.svelte.js';
import { error, 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);
@@ -35,12 +34,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);
@@ -52,22 +51,6 @@
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;
@@ -78,7 +61,6 @@
}
async function handleBookmarks(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
bookmarks = Object.keys(res).length ? res : null;
@@ -88,7 +70,6 @@
}
async function handleCompanies(resp){
quitOne();
if (resp.ok){
const json = await resp.json();
companies = Object.keys(json).length ? json : null;
@@ -98,7 +79,6 @@
}
async function handleDocuments(resp){
quitOne();
if (resp.ok){
const json = await resp.json();
documents = Object.keys(json).length ? json : null;
@@ -108,25 +88,15 @@
}
async function handleNotes(resp){
quitOne();
if (resp.ok){
const json = await resp.json();
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;
notes = Object.keys(json).length ? json : null;
} else {
error(resp);
}
}
async function handleProjects(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
projects = Object.keys(res).length ? res : null;
@@ -136,7 +106,6 @@
}
async function handleStock(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
stock = Object.keys(res).length ? res : null;
@@ -146,7 +115,6 @@
}
async function handleTasks(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
tasks = Object.keys(res).length ? res : null;
@@ -156,7 +124,6 @@
}
async function handleTimes(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
times = Object.keys(res).length ? res : null;
@@ -166,7 +133,6 @@
}
async function handleWikiPages(resp){
quitOne();
if (resp.ok){
const res = await resp.json();
pages = Object.keys(res).length ? res : null;
@@ -174,13 +140,6 @@
error(resp);
}
}
function quitOne(){
counter--;
if (counter > 0) {
warn(t('searching…')+" "+counter);
} else yikes();
}
$effect(() => doSearch(key))
</script>
@@ -277,6 +236,23 @@
</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>
@@ -332,20 +308,3 @@
</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

@@ -5,6 +5,7 @@ public class Constants {
public static final String AUTH = "mail.smtp.auth";
public static final String CONFIG_DB = "umbrella.modules.message.database";
public static final String CONFIG_SMTP_FROM = "umbrella.modules.message.smtp.from";
public static final String CONFIG_SMTP_HOST = "umbrella.modules.message.smtp.host";
public static final String CONFIG_SMTP_PASS = "umbrella.modules.message.smtp.pass";
public static final String CONFIG_SMTP_PORT = "umbrella.modules.message.smtp.port";

View File

@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.message;
import static de.srsoftware.tools.PathHandler.CONTENT_TYPE;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.translator;
import static de.srsoftware.umbrella.core.constants.Constants.UTF8;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingConfig;
import static de.srsoftware.umbrella.message.Constants.*;
@@ -12,6 +13,7 @@ import de.srsoftware.configuration.Configuration;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.PostBox;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.EmailAddress;
import de.srsoftware.umbrella.core.model.Envelope;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.core.model.User;
@@ -76,10 +78,10 @@ public class MessageSystem implements PostBox {
db = new SqliteMessageDb(connect(dbFile));
debugAddress = config.get(DEBUG_ADDREESS).map(Object::toString).orElse(null);
port = config.get(CONFIG_SMTP_PORT,587);
host = config.get(CONFIG_SMTP_HOST).map(Object::toString).orElseThrow(() -> new RuntimeException("umbrella.modules.message.smtp.host not configured!"));
user = config.get(CONFIG_SMTP_USER).map(Object::toString).orElseThrow(() -> new RuntimeException("umbrella.modules.message.smtp.user not configured!"));
pass = config.get(CONFIG_SMTP_PASS).map(Object::toString).orElseThrow(() -> new RuntimeException("umbrella.modules.message.smtp.pass not configured!"));
from = user;
host = config.get(CONFIG_SMTP_HOST).map(Object::toString).orElseThrow(() -> missingConfig(CONFIG_SMTP_HOST));
user = config.get(CONFIG_SMTP_USER).map(Object::toString).orElseThrow(() -> missingConfig(CONFIG_SMTP_USER));
pass = config.get(CONFIG_SMTP_PASS).map(Object::toString).orElseThrow(() -> missingConfig(CONFIG_SMTP_PASS));
from = config.get(CONFIG_SMTP_FROM).map(Object::toString).orElseThrow(() -> missingConfig(CONFIG_SMTP_FROM));
ModuleRegistry.add(this);
new SubmissionTask(8).schedule();
new SubmissionTask(10).schedule();
@@ -116,9 +118,9 @@ public class MessageSystem implements PostBox {
var date = new Date();
for (var receiver : dueRecipients){
BiFunction<String,Map<String,String>,String> translateFunction = (text,fills) -> ModuleRegistry.translator().translate(receiver.language(),text,fills);
var combined = new CombinedMessage("Collected messages",translateFunction);
BiFunction<String,Map<String,String>,String> translateFunction = (text,fills) -> translator().translate(receiver.language(),text,fills);
var fallbackSender = new User("Umbrella",new EmailAddress(from),null);
var combined = new CombinedMessage("Collected messages",translateFunction,fallbackSender);
var envelopes = queue.stream().filter(env -> env.isFor(receiver)).toList();
for (var envelope : envelopes) combined.merge(envelope.message());

View File

@@ -5,9 +5,7 @@ import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.TRACE;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.model.Attachment;
import de.srsoftware.umbrella.core.model.Message;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.core.model.*;
import java.util.*;
import java.util.function.BiFunction;
@@ -16,15 +14,17 @@ public class CombinedMessage {
private final Set<Attachment> attachments = new HashSet<>();
private final StringBuilder combinedBody = new StringBuilder();
private final User fallbackSender;
private String combinedSubject = null;
private final List<Message> mergedMessages = new ArrayList<>();
private final String subjectForCombinedMessage;
private final BiFunction<String,Map<String,String>,String> translate;
private UmbrellaUser sender = null;
private User sender = null;
public CombinedMessage(String subjectForCombinedMessage, BiFunction<String, Map<String,String>,String> translateFunction){
public CombinedMessage(String subjectForCombinedMessage, BiFunction<String, Map<String,String>,String> translateFunction, User fallbackSender){
LOG.log(DEBUG,"Creating combined message…");
this.subjectForCombinedMessage = subjectForCombinedMessage;
this.subjectForCombinedMessage = translateFunction.apply(subjectForCombinedMessage,null);
this.fallbackSender = fallbackSender;
translate = translateFunction;
}
@@ -39,12 +39,12 @@ public class CombinedMessage {
combinedSubject = subject;
break;
case 1:
combinedBody.insert(0,format("# {0}:\n# {1}:\n\n",sender,subject)); // insert sender and subject of first message right before the body of the first message
if (!sender.equals(message.sender())) sender = fallbackSender;
combinedBody.insert(0,format("# {0} / {1}:\n\n",sender,subject)); // insert sender and subject of first message right before the body of the first message
combinedSubject = subjectForCombinedMessage;
// no break here, we need to append the subject and content
default:
combinedBody.append("\n\n# ").append(message.sender()).append(":\n");
combinedBody.append("# ").append(subject).append(":\n\n");
combinedBody.append("\n-----\n# ").append(message.sender()).append(" / ").append(subject).append(":\n\n");
combinedBody.append(body);
}
if (message.attachments() != null) attachments.addAll(message.attachments());
@@ -59,7 +59,7 @@ public class CombinedMessage {
return combinedBody.toString();
}
public UmbrellaUser sender() {
public User sender() {
return sender;
}

View File

@@ -132,23 +132,7 @@ public class StockModule extends BaseHandler implements StockService {
yield 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);
}
}
case null, default -> super.doGet(path,ex);
};
} catch (UmbrellaException e){
return send(ex,e);

View File

@@ -39,7 +39,6 @@ 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,7 +278,6 @@
"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,7 +278,6 @@
"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,5 +9,4 @@ tasks.processResources {
from("../frontend/dist") {
into("web")
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
}

View File

@@ -152,27 +152,15 @@ 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>();
{
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();
while (rs.next()) {
var page = WikiPage.of(rs);
map.put(page.id(),page);
}
rs.close();
return map;
} catch (SQLException e) {
throw failedToSearchDb(t(WIKI_PAGES)).causedBy(e);