Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7f82018f9 | |||
| a245f849e5 | |||
| 4c3312daca | |||
| a6983780f5 | |||
| 6e146c1577 | |||
| 49cbd328e6 | |||
| 1db28b5bc0 | |||
| 285343218b | |||
| f402122d0c | |||
| 4339e4fd98 | |||
| 16cbcdff60 | |||
| 8dc77f52e4 | |||
| b04b9b92a6 | |||
| 8d66ed4cbe | |||
| b0ee825e0a | |||
| a321c813de | |||
| fda40d72f8 | |||
| 2375746d91 | |||
| cf0cf2f5e9 | |||
| 3e71ecc6cb | |||
| 1302165ab2 | |||
| d08138c9e1 | |||
| 4cd1ea3277 | |||
| 1059164b4a | |||
| f438bea4cc | |||
| 9394ca597c | |||
| 53fe79fbbd | |||
| d62534b3eb |
+15
-10
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.db-journal
|
||||
Binary file not shown.
Binary file not shown.
@@ -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.
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.
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.
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,4 +9,5 @@ tasks.processResources {
|
||||
from("../frontend/dist") {
|
||||
into("web")
|
||||
}
|
||||
}
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
@@ -57,8 +57,9 @@ footer {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
img {
|
||||
img, svg {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
nav {
|
||||
|
||||
@@ -57,8 +57,9 @@ footer {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
img {
|
||||
img, svg {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
nav {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user