Compare commits
14 Commits
6157d7e27d
...
9c1d3bc04c
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c1d3bc04c | |||
| 8df587f9e2 | |||
| 3796bc2953 | |||
| 2278dfa714 | |||
| 1a5c69fcfb | |||
| a671ae392d | |||
| 4c58d175a0 | |||
| bc655f0e06 | |||
| 22bcf01fa8 | |||
| bf8a3386d7 | |||
| d182231448 | |||
| 0a32ae77cd | |||
| 98c95483d8 | |||
| bcde1da84f |
@@ -53,6 +53,9 @@ OPENAI_API_KEY=replace-this
|
|||||||
STIRLING_API_KEY=replace-this
|
STIRLING_API_KEY=replace-this
|
||||||
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
|
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
|
||||||
|
|
||||||
|
# Interner Prometheus Node Exporter für die Admin-Systemstatusseite.
|
||||||
|
NODE_EXPORTER_URL=http://node-exporter:9100
|
||||||
|
|
||||||
# Optionaler Erststart-Bootstrap. Wenn gesetzt, werden Admin, Mandant,
|
# Optionaler Erststart-Bootstrap. Wenn gesetzt, werden Admin, Mandant,
|
||||||
# Admin-Rolle und Grunddaten beim Backend-Start idempotent angelegt.
|
# Admin-Rolle und Grunddaten beim Backend-Start idempotent angelegt.
|
||||||
FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type MatrixRoomEvent = {
|
|||||||
sender: string
|
sender: string
|
||||||
origin_server_ts: number
|
origin_server_ts: number
|
||||||
type: string
|
type: string
|
||||||
|
redacts?: string
|
||||||
content?: {
|
content?: {
|
||||||
body?: string
|
body?: string
|
||||||
msgtype?: string
|
msgtype?: string
|
||||||
@@ -25,6 +26,18 @@ type MatrixRoomEvent = {
|
|||||||
mimetype?: string
|
mimetype?: string
|
||||||
size?: number
|
size?: number
|
||||||
}
|
}
|
||||||
|
"m.new_content"?: {
|
||||||
|
body?: string
|
||||||
|
msgtype?: string
|
||||||
|
}
|
||||||
|
"m.relates_to"?: {
|
||||||
|
event_id?: string
|
||||||
|
key?: string
|
||||||
|
rel_type?: string
|
||||||
|
"m.in_reply_to"?: {
|
||||||
|
event_id?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +48,33 @@ type MatrixJoinedMembersResponse = {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixRoomSearchResponse = {
|
||||||
|
search_categories?: {
|
||||||
|
room_events?: {
|
||||||
|
count?: number
|
||||||
|
results?: Array<{
|
||||||
|
result?: MatrixRoomEvent & {
|
||||||
|
room_id?: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatrixSyncResponse = {
|
||||||
|
next_batch?: string
|
||||||
|
rooms?: {
|
||||||
|
join?: Record<string, {
|
||||||
|
timeline?: {
|
||||||
|
events?: MatrixRoomEvent[]
|
||||||
|
}
|
||||||
|
state?: {
|
||||||
|
events?: MatrixRoomEvent[]
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type MatrixUserSession = {
|
type MatrixUserSession = {
|
||||||
accessToken: string
|
accessToken: string
|
||||||
matrixUserId: string
|
matrixUserId: string
|
||||||
@@ -71,6 +111,10 @@ type MatrixAttachmentInput = {
|
|||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixMessageOptions = {
|
||||||
|
replyToEventId?: string
|
||||||
|
}
|
||||||
|
|
||||||
type MatrixCachedValue<T = any> = {
|
type MatrixCachedValue<T = any> = {
|
||||||
exists: true
|
exists: true
|
||||||
cachedUntil: number
|
cachedUntil: number
|
||||||
@@ -1158,27 +1202,72 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
session.accessToken
|
session.accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const replacementByEventId = new Map<string, MatrixRoomEvent>()
|
||||||
|
const reactionsByEventId = new Map<string, Map<string, { key: string; count: number; own: boolean }>>()
|
||||||
|
|
||||||
|
for (const event of response.chunk) {
|
||||||
|
const relation = event.content?.["m.relates_to"]
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
relation?.rel_type === "m.replace" &&
|
||||||
|
relation.event_id
|
||||||
|
) {
|
||||||
|
replacementByEventId.set(relation.event_id, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.type === "m.reaction" &&
|
||||||
|
relation?.rel_type === "m.annotation" &&
|
||||||
|
relation.event_id &&
|
||||||
|
relation.key
|
||||||
|
) {
|
||||||
|
const eventReactions = reactionsByEventId.get(relation.event_id) || new Map()
|
||||||
|
const reaction = eventReactions.get(relation.key) || {
|
||||||
|
key: relation.key,
|
||||||
|
count: 0,
|
||||||
|
own: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
reaction.count += 1
|
||||||
|
reaction.own = reaction.own || event.sender === session.matrixUserId
|
||||||
|
eventReactions.set(relation.key, reaction)
|
||||||
|
reactionsByEventId.set(relation.event_id, eventReactions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = response.chunk
|
||||||
|
.filter((event) =>
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "") &&
|
||||||
|
event.content?.["m.relates_to"]?.rel_type !== "m.replace"
|
||||||
|
)
|
||||||
|
.map((event) => {
|
||||||
|
const replacement = replacementByEventId.get(event.event_id)
|
||||||
|
const content = replacement?.content?.["m.new_content"] || replacement?.content || event.content
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: event.event_id,
|
||||||
|
sender: event.sender,
|
||||||
|
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
|
||||||
|
body: content?.body || "",
|
||||||
|
attachment: attachmentFromEvent({ ...event, content }),
|
||||||
|
timestamp: replacement?.origin_server_ts || event.origin_server_ts,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
edited: Boolean(replacement),
|
||||||
|
replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
|
||||||
|
reactions: Array.from(reactionsByEventId.get(event.event_id)?.values() || []),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reverse()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
alias: room.alias,
|
alias: room.alias,
|
||||||
key: room.key,
|
key: room.key,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
matrixUserId: session.matrixUserId,
|
matrixUserId: session.matrixUserId,
|
||||||
messages: response.chunk
|
messages,
|
||||||
.filter((event) =>
|
|
||||||
event.type === "m.room.message" &&
|
|
||||||
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "")
|
|
||||||
)
|
|
||||||
.map((event) => ({
|
|
||||||
id: event.event_id,
|
|
||||||
sender: event.sender,
|
|
||||||
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
|
|
||||||
body: event.content?.body || "",
|
|
||||||
attachment: attachmentFromEvent(event),
|
|
||||||
timestamp: event.origin_server_ts,
|
|
||||||
own: event.sender === session.matrixUserId,
|
|
||||||
}))
|
|
||||||
.reverse(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1212,11 +1301,223 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchTenantRoomMessages = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
query: string
|
||||||
|
) => {
|
||||||
|
const searchTerm = query.trim()
|
||||||
|
|
||||||
|
if (searchTerm.length < 2) {
|
||||||
|
return {
|
||||||
|
roomId: "",
|
||||||
|
alias: "",
|
||||||
|
key: options.key || "allgemein",
|
||||||
|
name: options.name || options.key || "Chat",
|
||||||
|
count: 0,
|
||||||
|
results: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const [response, members] = await Promise.all([
|
||||||
|
requestMatrixJson<MatrixRoomSearchResponse>(
|
||||||
|
"/_matrix/client/v3/search",
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
search_categories: {
|
||||||
|
room_events: {
|
||||||
|
search_term: searchTerm,
|
||||||
|
keys: ["content.body"],
|
||||||
|
order_by: "recent",
|
||||||
|
filter: {
|
||||||
|
limit: 25,
|
||||||
|
rooms: [room.roomId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
requestMatrixJson<MatrixJoinedMembersResponse>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/joined_members`,
|
||||||
|
session.accessToken
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
const roomEvents = response.search_categories?.room_events
|
||||||
|
const results = (roomEvents?.results || [])
|
||||||
|
.map((item) => item.result)
|
||||||
|
.filter((event): event is MatrixRoomEvent & { room_id?: string } =>
|
||||||
|
Boolean(
|
||||||
|
event?.event_id &&
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
roomId: event.room_id || room.roomId,
|
||||||
|
key: room.key,
|
||||||
|
sender: event.sender,
|
||||||
|
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
|
||||||
|
body: event.content?.body || "",
|
||||||
|
attachment: attachmentFromEvent(event),
|
||||||
|
timestamp: event.origin_server_ts,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
count: roomEvents?.count || results.length,
|
||||||
|
results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncTenantRoomEvents = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
since?: string,
|
||||||
|
initial = false
|
||||||
|
) => {
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const filter = {
|
||||||
|
room: {
|
||||||
|
rooms: [room.roomId],
|
||||||
|
timeline: {
|
||||||
|
limit: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
presence: {
|
||||||
|
types: [],
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
types: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
timeout: since && !initial ? "25000" : "0",
|
||||||
|
filter: JSON.stringify(filter),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (since) params.set("since", since)
|
||||||
|
|
||||||
|
const response = await requestMatrixJson<MatrixSyncResponse>(
|
||||||
|
`/_matrix/client/v3/sync?${params.toString()}`,
|
||||||
|
session.accessToken
|
||||||
|
)
|
||||||
|
const joinedRoom = response.rooms?.join?.[room.roomId]
|
||||||
|
const timelineEvents = joinedRoom?.timeline?.events || []
|
||||||
|
const stateEvents = joinedRoom?.state?.events || []
|
||||||
|
|
||||||
|
if (initial) {
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
nextBatch: response.next_batch || since || "",
|
||||||
|
messages: [],
|
||||||
|
replacements: [],
|
||||||
|
reactions: [],
|
||||||
|
redactions: [],
|
||||||
|
membersChanged: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = timelineEvents
|
||||||
|
.filter((event) =>
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "") &&
|
||||||
|
event.content?.["m.relates_to"]?.rel_type !== "m.replace"
|
||||||
|
)
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
sender: event.sender,
|
||||||
|
senderDisplayName: event.sender,
|
||||||
|
body: event.content?.body || "",
|
||||||
|
attachment: attachmentFromEvent(event),
|
||||||
|
timestamp: event.origin_server_ts,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
|
||||||
|
reactions: [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const replacements = timelineEvents
|
||||||
|
.filter((event) =>
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
event.content?.["m.relates_to"]?.rel_type === "m.replace" &&
|
||||||
|
Boolean(event.content?.["m.relates_to"]?.event_id)
|
||||||
|
)
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
targetEventId: event.content?.["m.relates_to"]?.event_id,
|
||||||
|
body: event.content?.["m.new_content"]?.body || event.content?.body || "",
|
||||||
|
timestamp: event.origin_server_ts,
|
||||||
|
sender: event.sender,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const reactions = timelineEvents
|
||||||
|
.filter((event) =>
|
||||||
|
event.type === "m.reaction" &&
|
||||||
|
event.content?.["m.relates_to"]?.rel_type === "m.annotation" &&
|
||||||
|
Boolean(event.content?.["m.relates_to"]?.event_id) &&
|
||||||
|
Boolean(event.content?.["m.relates_to"]?.key)
|
||||||
|
)
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
targetEventId: event.content?.["m.relates_to"]?.event_id,
|
||||||
|
key: event.content?.["m.relates_to"]?.key,
|
||||||
|
sender: event.sender,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const redactions = timelineEvents
|
||||||
|
.filter((event) => event.type === "m.room.redaction" && Boolean(event.redacts))
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
targetEventId: event.redacts,
|
||||||
|
sender: event.sender,
|
||||||
|
timestamp: event.origin_server_ts,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
nextBatch: response.next_batch || since || "",
|
||||||
|
messages,
|
||||||
|
replacements,
|
||||||
|
reactions,
|
||||||
|
redactions,
|
||||||
|
membersChanged: [...timelineEvents, ...stateEvents].some((event) => event.type === "m.room.member"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sendTenantRoomMessage = async (
|
const sendTenantRoomMessage = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
options: MatrixTenantRoomOptions = {},
|
options: MatrixTenantRoomOptions = {},
|
||||||
text: string
|
text: string,
|
||||||
|
messageOptions: MatrixMessageOptions = {}
|
||||||
) => {
|
) => {
|
||||||
const message = text.trim()
|
const message = text.trim()
|
||||||
|
|
||||||
@@ -1235,16 +1536,26 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
})
|
})
|
||||||
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
||||||
|
|
||||||
|
const content: Record<string, any> = {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageOptions.replyToEventId) {
|
||||||
|
content["m.relates_to"] = {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
event_id: messageOptions.replyToEventId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await requestMatrixJson<{ event_id: string }>(
|
const response = await requestMatrixJson<{ event_id: string }>(
|
||||||
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
|
||||||
session.accessToken,
|
session.accessToken,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(content),
|
||||||
msgtype: "m.text",
|
|
||||||
body: message,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1258,9 +1569,163 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
alias: room.alias,
|
alias: room.alias,
|
||||||
key: room.key,
|
key: room.key,
|
||||||
|
replyToEventId: messageOptions.replyToEventId || null,
|
||||||
|
reactions: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendTenantRoomReaction = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
eventId: string,
|
||||||
|
key: string
|
||||||
|
) => {
|
||||||
|
const reactionKey = key.trim()
|
||||||
|
if (!eventId || !reactionKey) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Reaction target and key are required"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
||||||
|
await requestMatrixJson<{ event_id: string }>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.reaction/${encodeURIComponent(txnId)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.annotation",
|
||||||
|
event_id: eventId,
|
||||||
|
key: reactionKey,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return { success: true, eventId, key: reactionKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
const editTenantRoomMessage = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
eventId: string,
|
||||||
|
text: string
|
||||||
|
) => {
|
||||||
|
const message = text.trim()
|
||||||
|
if (!eventId || !message) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Nachricht und Zielnachricht sind erforderlich"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
||||||
|
const response = await requestMatrixJson<{ event_id: string }>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: `* ${message}`,
|
||||||
|
"m.new_content": {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: message,
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.replace",
|
||||||
|
event_id: eventId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: response.event_id,
|
||||||
|
targetEventId: eventId,
|
||||||
|
body: message,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
own: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const redactTenantRoomMessage = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
eventId: string
|
||||||
|
) => {
|
||||||
|
if (!eventId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Zielnachricht ist erforderlich"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
||||||
|
await requestMatrixJson(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
reason: "Nachricht in FEDEO gelöscht",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return { success: true, eventId }
|
||||||
|
}
|
||||||
|
|
||||||
|
const markTenantRoomRead = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
eventId: string
|
||||||
|
) => {
|
||||||
|
if (!eventId) return { success: true, skipped: true }
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
|
||||||
|
await requestMatrixJson(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/receipt/m.read/${encodeURIComponent(eventId)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return { success: true, eventId }
|
||||||
|
}
|
||||||
|
|
||||||
const sendTenantRoomAttachment = async (
|
const sendTenantRoomAttachment = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
@@ -1467,13 +1932,18 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
))
|
))
|
||||||
.where(eq(authTenantUsers.tenant_id, tenant.id))
|
.where(eq(authTenantUsers.tenant_id, tenant.id))
|
||||||
|
|
||||||
return rows
|
const users = rows
|
||||||
.filter((row) => row.profileActive !== false)
|
.filter((row) => row.profileActive !== false)
|
||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
userId: row.userId,
|
userId: row.userId,
|
||||||
email: row.email,
|
email: row.email,
|
||||||
displayName: row.fullName || `${row.firstName || ""} ${row.lastName || ""}`.trim() || row.email,
|
displayName: row.fullName || `${row.firstName || ""} ${row.lastName || ""}`.trim() || row.email,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
return await Promise.all(users.map(async (user) => ({
|
||||||
|
...user,
|
||||||
|
matrixUserId: await matrixUserIdForUser(user.userId, tenant.id),
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
const inviteMatrixUserToRoom = async (
|
const inviteMatrixUserToRoom = async (
|
||||||
@@ -1567,6 +2037,102 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inviteTenantRoomMember = async (
|
||||||
|
requestingUserId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
targetUserId: string
|
||||||
|
) => {
|
||||||
|
if (!targetUserId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Benutzer ist erforderlich"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await listTenantCommunicationUsers(tenantId)
|
||||||
|
const targetUser = users.find((user) => user.userId === targetUserId)
|
||||||
|
|
||||||
|
if (!targetUser) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Benutzer gehört nicht zum aktiven Mandanten"),
|
||||||
|
{ statusCode: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(requestingUserId, tenantId, options)
|
||||||
|
const account = await provisionCurrentUser(targetUser.userId, tenantId)
|
||||||
|
const inviteStatus = await inviteMatrixUserToRoom(
|
||||||
|
{ roomId: room.roomId },
|
||||||
|
account.matrixUserId,
|
||||||
|
`FEDEO-Einladung: ${room.name}`
|
||||||
|
)
|
||||||
|
await ensureCurrentUserJoinedRoom(targetUser.userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
userId: targetUser.userId,
|
||||||
|
email: targetUser.email,
|
||||||
|
displayName: targetUser.displayName,
|
||||||
|
matrixUserId: account.matrixUserId,
|
||||||
|
status: inviteStatus === "invited" ? "joined" : inviteStatus,
|
||||||
|
ok: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTenantRoomMember = async (
|
||||||
|
requestingUserId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
matrixUserId: string
|
||||||
|
) => {
|
||||||
|
if (!matrixUserId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Matrix-Benutzer ist erforderlich"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(requestingUserId, tenantId, options)
|
||||||
|
const session = await createAccessTokenForUser(requestingUserId, tenantId)
|
||||||
|
|
||||||
|
if (matrixUserId === session.matrixUserId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Du kannst dich nicht selbst aus dem Raum entfernen"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceLogin = await ensureServiceAccessToken()
|
||||||
|
await requestMatrixJson(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/kick`,
|
||||||
|
serviceLogin.accessToken,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: matrixUserId,
|
||||||
|
reason: "FEDEO-Mitgliederverwaltung",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
matrixJoinedRoomCache.delete(`${matrixUserId}:${room.roomId}`)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
matrixUserId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
|
const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
|
||||||
getTenantRoomMessages(userId, tenantId, {
|
getTenantRoomMessages(userId, tenantId, {
|
||||||
key: "allgemein",
|
key: "allgemein",
|
||||||
@@ -1579,7 +2145,12 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
name: "Allgemeiner Chat",
|
name: "Allgemeiner Chat",
|
||||||
})
|
})
|
||||||
|
|
||||||
const sendGeneralRoomMessage = (userId: string, tenantId: number | null, text: string) =>
|
const sendGeneralRoomMessage = (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
text: string,
|
||||||
|
messageOptions: MatrixMessageOptions = {}
|
||||||
|
) =>
|
||||||
sendTenantRoomMessage(
|
sendTenantRoomMessage(
|
||||||
userId,
|
userId,
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -1587,7 +2158,8 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
key: "allgemein",
|
key: "allgemein",
|
||||||
name: "Allgemeiner Chat",
|
name: "Allgemeiner Chat",
|
||||||
},
|
},
|
||||||
text
|
text,
|
||||||
|
messageOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
const sendGeneralRoomAttachment = (userId: string, tenantId: number | null, attachment: MatrixAttachmentInput) =>
|
const sendGeneralRoomAttachment = (userId: string, tenantId: number | null, attachment: MatrixAttachmentInput) =>
|
||||||
@@ -1609,16 +2181,25 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
getTenantSpaceStatus,
|
getTenantSpaceStatus,
|
||||||
provisionCurrentTenantSpace,
|
provisionCurrentTenantSpace,
|
||||||
listTenantRooms,
|
listTenantRooms,
|
||||||
|
listTenantCommunicationUsers,
|
||||||
getTenantRoomStatus,
|
getTenantRoomStatus,
|
||||||
provisionTenantRoom,
|
provisionTenantRoom,
|
||||||
createAccessTokenForUser,
|
createAccessTokenForUser,
|
||||||
getTenantRoomMessages,
|
getTenantRoomMessages,
|
||||||
getTenantRoomMembers,
|
getTenantRoomMembers,
|
||||||
|
searchTenantRoomMessages,
|
||||||
|
syncTenantRoomEvents,
|
||||||
sendTenantRoomMessage,
|
sendTenantRoomMessage,
|
||||||
|
sendTenantRoomReaction,
|
||||||
|
editTenantRoomMessage,
|
||||||
|
redactTenantRoomMessage,
|
||||||
|
markTenantRoomRead,
|
||||||
sendTenantRoomAttachment,
|
sendTenantRoomAttachment,
|
||||||
getMediaContent,
|
getMediaContent,
|
||||||
createElementRoomSession,
|
createElementRoomSession,
|
||||||
createLiveKitRoomSession,
|
createLiveKitRoomSession,
|
||||||
|
inviteTenantRoomMember,
|
||||||
|
removeTenantRoomMember,
|
||||||
syncTenantRoomMembers,
|
syncTenantRoomMembers,
|
||||||
getGeneralRoomMessages,
|
getGeneralRoomMessages,
|
||||||
getGeneralRoomMembers,
|
getGeneralRoomMembers,
|
||||||
|
|||||||
174
backend/src/modules/system-status.service.ts
Normal file
174
backend/src/modules/system-status.service.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { FastifyInstance } from "fastify"
|
||||||
|
import { matrixService } from "./matrix.service"
|
||||||
|
|
||||||
|
type MetricSample = {
|
||||||
|
labels: Record<string, string>
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const metricLinePattern = /^([a-zA-Z_:][a-zA-Z0-9_:]*)(?:\{([^}]*)\})?\s+(-?(?:\d+(?:\.\d+)?|\.\d+)(?:e[+-]?\d+)?|-?Inf|NaN)$/i
|
||||||
|
|
||||||
|
const nodeExporterUrl = () =>
|
||||||
|
(process.env.NODE_EXPORTER_URL || "http://node-exporter:9100").replace(/\/+$/, "")
|
||||||
|
|
||||||
|
const s3EndpointUrl = () =>
|
||||||
|
(process.env.S3_ENDPOINT || "").replace(/\/+$/, "")
|
||||||
|
|
||||||
|
const parseLabels = (value = "") => {
|
||||||
|
const labels: Record<string, string> = {}
|
||||||
|
const labelPattern = /(\w+)="((?:\\"|[^"])*)"/g
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
|
||||||
|
while ((match = labelPattern.exec(value))) {
|
||||||
|
labels[match[1]] = match[2].replace(/\\"/g, "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsePrometheusMetrics = (text: string) => {
|
||||||
|
const metrics = new Map<string, MetricSample[]>()
|
||||||
|
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
if (!line || line.startsWith("#")) continue
|
||||||
|
|
||||||
|
const match = line.match(metricLinePattern)
|
||||||
|
if (!match) continue
|
||||||
|
|
||||||
|
const value = Number(match[3])
|
||||||
|
if (!Number.isFinite(value)) continue
|
||||||
|
|
||||||
|
const samples = metrics.get(match[1]) || []
|
||||||
|
samples.push({
|
||||||
|
labels: parseLabels(match[2]),
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
metrics.set(match[1], samples)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstMetricValue = (metrics: Map<string, MetricSample[]>, name: string) =>
|
||||||
|
metrics.get(name)?.[0]?.value ?? null
|
||||||
|
|
||||||
|
const findMetricValue = (
|
||||||
|
metrics: Map<string, MetricSample[]>,
|
||||||
|
name: string,
|
||||||
|
predicate: (sample: MetricSample) => boolean
|
||||||
|
) => metrics.get(name)?.find(predicate)?.value ?? null
|
||||||
|
|
||||||
|
const serviceState = (ok: boolean, detail?: Record<string, any>) => ({
|
||||||
|
ok,
|
||||||
|
status: ok ? "ok" : "error",
|
||||||
|
...detail,
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkHttp = async (url: string, timeoutMs = 3000) => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { signal: controller.signal })
|
||||||
|
return serviceState(response.ok, {
|
||||||
|
httpStatus: response.status,
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
return serviceState(false, {
|
||||||
|
url,
|
||||||
|
error: err?.message || "HTTP-Abfrage fehlgeschlagen",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildSystemStatus = async (server: FastifyInstance) => {
|
||||||
|
const checkedAt = new Date()
|
||||||
|
const nodeExporterMetricsUrl = `${nodeExporterUrl()}/metrics`
|
||||||
|
let nodeMetrics: Map<string, MetricSample[]> | null = null
|
||||||
|
let nodeExporterError: string | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(nodeExporterMetricsUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Node Exporter antwortet mit ${response.status}`)
|
||||||
|
}
|
||||||
|
nodeMetrics = parsePrometheusMetrics(await response.text())
|
||||||
|
} catch (err: any) {
|
||||||
|
nodeExporterError = err?.message || "Node Exporter nicht erreichbar"
|
||||||
|
}
|
||||||
|
|
||||||
|
const memoryTotal = nodeMetrics ? firstMetricValue(nodeMetrics, "node_memory_MemTotal_bytes") : null
|
||||||
|
const memoryAvailable = nodeMetrics ? firstMetricValue(nodeMetrics, "node_memory_MemAvailable_bytes") : null
|
||||||
|
const rootSize = nodeMetrics
|
||||||
|
? findMetricValue(nodeMetrics, "node_filesystem_size_bytes", (sample) => sample.labels.mountpoint === "/")
|
||||||
|
: null
|
||||||
|
const rootAvailable = nodeMetrics
|
||||||
|
? findMetricValue(nodeMetrics, "node_filesystem_avail_bytes", (sample) => sample.labels.mountpoint === "/")
|
||||||
|
: null
|
||||||
|
const bootTime = nodeMetrics ? firstMetricValue(nodeMetrics, "node_boot_time_seconds") : null
|
||||||
|
const cpuCount = nodeMetrics
|
||||||
|
? new Set((nodeMetrics.get("node_cpu_seconds_total") || [])
|
||||||
|
.filter((sample) => sample.labels.mode === "idle")
|
||||||
|
.map((sample) => sample.labels.cpu)).size
|
||||||
|
: null
|
||||||
|
const uname = nodeMetrics?.get("node_uname_info")?.[0]?.labels || null
|
||||||
|
|
||||||
|
const databaseCheck = await server.db.execute("SELECT NOW() as now")
|
||||||
|
const matrixStatus = await matrixService(server).getStatus().catch((err: any) => ({
|
||||||
|
reachable: false,
|
||||||
|
error: err?.message || "Matrix-Status nicht verfügbar",
|
||||||
|
}))
|
||||||
|
const minioUrl = s3EndpointUrl()
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkedAt: checkedAt.toISOString(),
|
||||||
|
backend: {
|
||||||
|
status: "ok",
|
||||||
|
uptimeSeconds: Math.round(process.uptime()),
|
||||||
|
nodeVersion: process.version,
|
||||||
|
environment: process.env.NODE_ENV || "development",
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
status: nodeMetrics ? "ok" : "unavailable",
|
||||||
|
nodeExporterUrl: nodeExporterMetricsUrl,
|
||||||
|
error: nodeExporterError,
|
||||||
|
hostname: uname?.nodename || null,
|
||||||
|
kernel: uname?.release || null,
|
||||||
|
cpuCount,
|
||||||
|
load: {
|
||||||
|
one: nodeMetrics ? firstMetricValue(nodeMetrics, "node_load1") : null,
|
||||||
|
five: nodeMetrics ? firstMetricValue(nodeMetrics, "node_load5") : null,
|
||||||
|
fifteen: nodeMetrics ? firstMetricValue(nodeMetrics, "node_load15") : null,
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
totalBytes: memoryTotal,
|
||||||
|
availableBytes: memoryAvailable,
|
||||||
|
usedBytes: memoryTotal !== null && memoryAvailable !== null ? memoryTotal - memoryAvailable : null,
|
||||||
|
usedPercent: memoryTotal ? Math.round(((memoryTotal - (memoryAvailable || 0)) / memoryTotal) * 1000) / 10 : null,
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
rootTotalBytes: rootSize,
|
||||||
|
rootAvailableBytes: rootAvailable,
|
||||||
|
rootUsedBytes: rootSize !== null && rootAvailable !== null ? rootSize - rootAvailable : null,
|
||||||
|
rootUsedPercent: rootSize ? Math.round(((rootSize - (rootAvailable || 0)) / rootSize) * 1000) / 10 : null,
|
||||||
|
},
|
||||||
|
uptimeSeconds: bootTime ? Math.max(0, Math.round(Date.now() / 1000 - bootTime)) : null,
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
database: serviceState(true, {
|
||||||
|
checkedAt: String(databaseCheck.rows?.[0]?.now || checkedAt.toISOString()),
|
||||||
|
}),
|
||||||
|
nodeExporter: serviceState(Boolean(nodeMetrics), {
|
||||||
|
url: nodeExporterMetricsUrl,
|
||||||
|
error: nodeExporterError,
|
||||||
|
}),
|
||||||
|
matrix: serviceState(Boolean((matrixStatus as any).reachable), matrixStatus as Record<string, any>),
|
||||||
|
minio: minioUrl ? await checkHttp(`${minioUrl}/minio/health/live`) : serviceState(false, {
|
||||||
|
error: "S3_ENDPOINT ist nicht gesetzt",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { sendMail } from "../utils/mailer";
|
|||||||
import { ensureTenantBaseData } from "../modules/bootstrap.service";
|
import { ensureTenantBaseData } from "../modules/bootstrap.service";
|
||||||
import { buildTenantFullExport, importTenantFullExport } from "../utils/tenantFullExport";
|
import { buildTenantFullExport, importTenantFullExport } from "../utils/tenantFullExport";
|
||||||
import type { TenantFullExport } from "../utils/tenantFullExport";
|
import type { TenantFullExport } from "../utils/tenantFullExport";
|
||||||
|
import { buildSystemStatus } from "../modules/system-status.service";
|
||||||
|
|
||||||
export default async function adminRoutes(server: FastifyInstance) {
|
export default async function adminRoutes(server: FastifyInstance) {
|
||||||
const deriveNameFromEmail = (email: string) => {
|
const deriveNameFromEmail = (email: string) => {
|
||||||
@@ -393,6 +394,21 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// GET /admin/system-status
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
server.get("/admin/system-status", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const currentUser = await requireAdmin(req, reply);
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
return await buildSystemStatus(server);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("ERROR /admin/system-status:", err);
|
||||||
|
return reply.code(500).send({ error: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// POST /admin/users
|
// POST /admin/users
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
|
|||||||
@@ -603,6 +603,15 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/users", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const users = await matrix.listTenantCommunicationUsers(req.user.tenant_id)
|
||||||
|
return { users }
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix users failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/general/session", async (req, reply) => {
|
server.post("/communication/matrix/rooms/general/session", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
return await matrix.createElementRoomSession(req.user.user_id, req.user.tenant_id, {
|
return await matrix.createElementRoomSession(req.user.user_id, req.user.tenant_id, {
|
||||||
@@ -641,8 +650,10 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const body = req.body as { text?: string }
|
const body = req.body as { text?: string; replyToEventId?: string }
|
||||||
const message = await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "")
|
const message = await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "", {
|
||||||
|
replyToEventId: body.replyToEventId,
|
||||||
|
})
|
||||||
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat")
|
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat")
|
||||||
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
|
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
|
||||||
return message
|
return message
|
||||||
@@ -688,7 +699,12 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
try {
|
try {
|
||||||
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
|
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
|
||||||
const params = req.params as { roomKey: string }
|
const params = req.params as { roomKey: string }
|
||||||
return await markRoomNotificationsRead(req.user.tenant_id, req.user.user_id, params.roomKey)
|
const body = (req.body || {}) as { eventId?: string }
|
||||||
|
const result = await markRoomNotificationsRead(req.user.tenant_id, req.user.user_id, params.roomKey)
|
||||||
|
if (body.eventId) {
|
||||||
|
await matrix.markTenantRoomRead(req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), body.eventId)
|
||||||
|
}
|
||||||
|
return result
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return handleMatrixError(req, reply, err, "Matrix room read state failed")
|
return handleMatrixError(req, reply, err, "Matrix room read state failed")
|
||||||
}
|
}
|
||||||
@@ -706,6 +722,35 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/rooms/:roomKey/search", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const query = req.query as { q?: string }
|
||||||
|
return await matrix.searchTenantRoomMessages(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
query.q || ""
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix search failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/rooms/:roomKey/sync", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const query = req.query as { since?: string; initial?: string }
|
||||||
|
return await matrix.syncTenantRoomEvents(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
query.since,
|
||||||
|
query.initial === "1"
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix sync failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.get("/communication/matrix/rooms/:roomKey/members", async (req, reply) => {
|
server.get("/communication/matrix/rooms/:roomKey/members", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
return await matrix.getTenantRoomMembers(
|
return await matrix.getTenantRoomMembers(
|
||||||
@@ -718,6 +763,34 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/:roomKey/members/invite", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const body = req.body as { userId?: string }
|
||||||
|
return await matrix.inviteTenantRoomMember(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
body.userId || ""
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix member invite failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.delete("/communication/matrix/rooms/:roomKey/members/:matrixUserId", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const params = req.params as { matrixUserId: string }
|
||||||
|
return await matrix.removeTenantRoomMember(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
params.matrixUserId
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix member remove failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/:roomKey/session", async (req, reply) => {
|
server.post("/communication/matrix/rooms/:roomKey/session", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
return await matrix.createElementRoomSession(
|
return await matrix.createElementRoomSession(
|
||||||
@@ -759,12 +832,15 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => {
|
server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const body = req.body as { text?: string }
|
const body = req.body as { text?: string; replyToEventId?: string }
|
||||||
const message = await matrix.sendTenantRoomMessage(
|
const message = await matrix.sendTenantRoomMessage(
|
||||||
req.user.user_id,
|
req.user.user_id,
|
||||||
req.user.tenant_id,
|
req.user.tenant_id,
|
||||||
roomOptionsFromRequest(req),
|
roomOptionsFromRequest(req),
|
||||||
body.text || ""
|
body.text || "",
|
||||||
|
{
|
||||||
|
replyToEventId: body.replyToEventId,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, message.key)
|
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, message.key)
|
||||||
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
|
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
|
||||||
@@ -774,6 +850,52 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/:roomKey/messages/:eventId/reactions", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const params = req.params as { eventId: string }
|
||||||
|
const body = req.body as { key?: string }
|
||||||
|
return await matrix.sendTenantRoomReaction(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
params.eventId,
|
||||||
|
body.key || ""
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix reaction send failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.put("/communication/matrix/rooms/:roomKey/messages/:eventId", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const params = req.params as { eventId: string }
|
||||||
|
const body = req.body as { text?: string }
|
||||||
|
return await matrix.editTenantRoomMessage(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
params.eventId,
|
||||||
|
body.text || ""
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix message edit failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.delete("/communication/matrix/rooms/:roomKey/messages/:eventId", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const params = req.params as { eventId: string }
|
||||||
|
return await matrix.redactTenantRoomMessage(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
params.eventId
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix message delete failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/:roomKey/attachments", async (req, reply) => {
|
server.post("/communication/matrix/rooms/:roomKey/attachments", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const attachment = await uploadedAttachmentFromRequest(req)
|
const attachment = await uploadedAttachmentFromRequest(req)
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ services:
|
|||||||
- internal
|
- internal
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
image: git.federspiel.tech/flfeders/fedeo/backend:dev
|
||||||
context: ./backend
|
|
||||||
container_name: fedeo-backend
|
container_name: fedeo-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -139,6 +138,7 @@ services:
|
|||||||
MATRIX_SERVICE_USER_LOCALPART: ${MATRIX_SERVICE_USER_LOCALPART:-fedeo_service}
|
MATRIX_SERVICE_USER_LOCALPART: ${MATRIX_SERVICE_USER_LOCALPART:-fedeo_service}
|
||||||
LIVEKIT_KEY: ${LIVEKIT_KEY:-fedeo-livekit}
|
LIVEKIT_KEY: ${LIVEKIT_KEY:-fedeo-livekit}
|
||||||
LIVEKIT_SECRET: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
LIVEKIT_SECRET: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
||||||
|
NODE_EXPORTER_URL: ${NODE_EXPORTER_URL:-http://node-exporter:9100}
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)
|
- traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)
|
||||||
@@ -152,9 +152,25 @@ services:
|
|||||||
- web
|
- web
|
||||||
- internal
|
- internal
|
||||||
|
|
||||||
|
node-exporter:
|
||||||
|
image: prom/node-exporter:v1.8.2
|
||||||
|
container_name: fedeo-node-exporter
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --path.procfs=/host/proc
|
||||||
|
- --path.sysfs=/host/sys
|
||||||
|
- --path.rootfs=/rootfs
|
||||||
|
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
|
||||||
|
pid: host
|
||||||
|
volumes:
|
||||||
|
- /proc:/host/proc:ro
|
||||||
|
- /sys:/host/sys:ro
|
||||||
|
- /:/rootfs:ro,rslave
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
image: git.federspiel.tech/flfeders/fedeo/frontend:dev
|
||||||
context: ./frontend
|
|
||||||
container_name: fedeo-frontend
|
container_name: fedeo-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ services:
|
|||||||
- WEB_PUSH_PUBLIC_KEY=${WEB_PUSH_PUBLIC_KEY:-}
|
- WEB_PUSH_PUBLIC_KEY=${WEB_PUSH_PUBLIC_KEY:-}
|
||||||
- WEB_PUSH_PRIVATE_KEY=${WEB_PUSH_PRIVATE_KEY:-}
|
- WEB_PUSH_PRIVATE_KEY=${WEB_PUSH_PRIVATE_KEY:-}
|
||||||
- WEB_PUSH_SUBJECT=${WEB_PUSH_SUBJECT:-mailto:admin@example.com}
|
- WEB_PUSH_SUBJECT=${WEB_PUSH_SUBJECT:-mailto:admin@example.com}
|
||||||
|
- NODE_EXPORTER_URL=${NODE_EXPORTER_URL:-http://node-exporter:9100}
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
@@ -74,6 +75,23 @@ services:
|
|||||||
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
|
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
|
||||||
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
|
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
|
||||||
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
|
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
|
||||||
|
|
||||||
|
node-exporter:
|
||||||
|
image: prom/node-exporter:v1.8.2
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --path.procfs=/host/proc
|
||||||
|
- --path.sysfs=/host/sys
|
||||||
|
- --path.rootfs=/rootfs
|
||||||
|
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
|
||||||
|
pid: host
|
||||||
|
volumes:
|
||||||
|
- /proc:/host/proc:ro
|
||||||
|
- /sys:/host/sys:ro
|
||||||
|
- /:/rootfs:ro,rslave
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
|
||||||
matrix-db:
|
matrix-db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -355,6 +355,11 @@ const links = computed(() => {
|
|||||||
to: "/administration/tenants",
|
to: "/administration/tenants",
|
||||||
icon: "i-heroicons-building-office-2",
|
icon: "i-heroicons-building-office-2",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Systemstatus",
|
||||||
|
to: "/administration/system",
|
||||||
|
icon: "i-heroicons-server-stack",
|
||||||
|
},
|
||||||
] : []
|
] : []
|
||||||
|
|
||||||
const visibleOrganisationChildren = visibleItems(organisationChildren)
|
const visibleOrganisationChildren = visibleItems(organisationChildren)
|
||||||
|
|||||||
@@ -55,6 +55,34 @@ export type TenantImportResult = {
|
|||||||
files: { restored: number; skipped: number }
|
files: { restored: number; skipped: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SystemStatus = {
|
||||||
|
checkedAt: string
|
||||||
|
backend: {
|
||||||
|
status: string
|
||||||
|
uptimeSeconds: number
|
||||||
|
nodeVersion: string
|
||||||
|
environment: string
|
||||||
|
}
|
||||||
|
server: {
|
||||||
|
status: string
|
||||||
|
nodeExporterUrl: string
|
||||||
|
error?: string | null
|
||||||
|
hostname?: string | null
|
||||||
|
kernel?: string | null
|
||||||
|
cpuCount?: number | null
|
||||||
|
uptimeSeconds?: number | null
|
||||||
|
load: { one?: number | null; five?: number | null; fifteen?: number | null }
|
||||||
|
memory: { totalBytes?: number | null; availableBytes?: number | null; usedBytes?: number | null; usedPercent?: number | null }
|
||||||
|
disk: { rootTotalBytes?: number | null; rootAvailableBytes?: number | null; rootUsedBytes?: number | null; rootUsedPercent?: number | null }
|
||||||
|
}
|
||||||
|
services: Record<string, {
|
||||||
|
ok: boolean
|
||||||
|
status: string
|
||||||
|
error?: string | null
|
||||||
|
[key: string]: any
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
export const useAdmin = () => {
|
export const useAdmin = () => {
|
||||||
const { $api } = useNuxtApp()
|
const { $api } = useNuxtApp()
|
||||||
|
|
||||||
@@ -130,8 +158,13 @@ export const useAdmin = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSystemStatus = async (): Promise<SystemStatus> => {
|
||||||
|
return await $api("/api/admin/system-status")
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getOverview,
|
getOverview,
|
||||||
|
getSystemStatus,
|
||||||
createUser,
|
createUser,
|
||||||
createUserForProfile,
|
createUserForProfile,
|
||||||
updateUser,
|
updateUser,
|
||||||
|
|||||||
228
frontend/pages/administration/system.vue
Normal file
228
frontend/pages/administration/system.vue
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SystemStatus } from "~/composables/useAdmin"
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const toast = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
const admin = useAdmin()
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const status = ref<SystemStatus | null>(null)
|
||||||
|
|
||||||
|
const serviceLabels: Record<string, string> = {
|
||||||
|
backend: "Backend",
|
||||||
|
database: "Datenbank",
|
||||||
|
nodeExporter: "Node Exporter",
|
||||||
|
matrix: "Matrix",
|
||||||
|
minio: "Dateispeicher",
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatBytes = (value?: number | null) => {
|
||||||
|
const bytes = Number(value || 0)
|
||||||
|
if (!bytes) return "-"
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||||
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDuration = (seconds?: number | null) => {
|
||||||
|
const value = Number(seconds || 0)
|
||||||
|
if (!value) return "-"
|
||||||
|
|
||||||
|
const days = Math.floor(value / 86400)
|
||||||
|
const hours = Math.floor((value % 86400) / 3600)
|
||||||
|
const minutes = Math.floor((value % 3600) / 60)
|
||||||
|
|
||||||
|
if (days) return `${days} d ${hours} h`
|
||||||
|
if (hours) return `${hours} h ${minutes} min`
|
||||||
|
return `${minutes} min`
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStatus = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
status.value = await admin.getSystemStatus()
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("[administration/system]", err)
|
||||||
|
toast.add({
|
||||||
|
title: "Systemstatus konnte nicht geladen werden",
|
||||||
|
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
||||||
|
color: "red",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceRows = computed(() => {
|
||||||
|
const services = status.value?.services || {}
|
||||||
|
return Object.entries(services).map(([key, service]) => ({
|
||||||
|
key,
|
||||||
|
label: serviceLabels[key] || key,
|
||||||
|
...service,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const overallStatus = computed(() => {
|
||||||
|
if (!status.value) return "unavailable"
|
||||||
|
return serviceRows.value.every((service) => service.ok) ? "ok" : "warning"
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.user?.is_admin) {
|
||||||
|
await router.push("/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadStatus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardNavbar title="Administration: Systemstatus">
|
||||||
|
<template #right>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-path"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
:loading="loading"
|
||||||
|
@click="loadStatus"
|
||||||
|
>
|
||||||
|
Aktualisieren
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardPanelContent>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<UAlert
|
||||||
|
:icon="overallStatus === 'ok' ? 'i-heroicons-check-circle' : 'i-heroicons-exclamation-triangle'"
|
||||||
|
:color="overallStatus === 'ok' ? 'success' : 'warning'"
|
||||||
|
variant="soft"
|
||||||
|
:title="overallStatus === 'ok' ? 'System läuft' : 'System prüfen'"
|
||||||
|
:description="status?.checkedAt ? `Letzte Prüfung: ${new Date(status.checkedAt).toLocaleString('de-DE')}` : 'Noch keine Prüfung geladen.'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="grid gap-4 xl:grid-cols-3">
|
||||||
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-cpu-chip" class="size-5 text-primary" />
|
||||||
|
<h2 class="text-base font-semibold text-highlighted">Server</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<div class="flex justify-between gap-3">
|
||||||
|
<span class="text-muted">Host</span>
|
||||||
|
<span class="truncate text-highlighted">{{ status?.server.hostname || "-" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-3">
|
||||||
|
<span class="text-muted">CPU</span>
|
||||||
|
<span class="text-highlighted">{{ status?.server.cpuCount || "-" }} Kerne</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-3">
|
||||||
|
<span class="text-muted">Load</span>
|
||||||
|
<span class="text-highlighted">
|
||||||
|
{{ status?.server.load.one ?? "-" }} · {{ status?.server.load.five ?? "-" }} · {{ status?.server.load.fifteen ?? "-" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-3">
|
||||||
|
<span class="text-muted">Uptime</span>
|
||||||
|
<span class="text-highlighted">{{ formatDuration(status?.server.uptimeSeconds) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-circle-stack" class="size-5 text-primary" />
|
||||||
|
<h2 class="text-base font-semibold text-highlighted">Speicher</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="space-y-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 flex justify-between">
|
||||||
|
<span class="text-muted">RAM</span>
|
||||||
|
<span class="text-highlighted">{{ status?.server.memory.usedPercent ?? "-" }}%</span>
|
||||||
|
</div>
|
||||||
|
<UProgress :model-value="status?.server.memory.usedPercent || 0" />
|
||||||
|
<p class="mt-1 text-xs text-muted">
|
||||||
|
{{ formatBytes(status?.server.memory.usedBytes) }} von {{ formatBytes(status?.server.memory.totalBytes) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 flex justify-between">
|
||||||
|
<span class="text-muted">Root-Dateisystem</span>
|
||||||
|
<span class="text-highlighted">{{ status?.server.disk.rootUsedPercent ?? "-" }}%</span>
|
||||||
|
</div>
|
||||||
|
<UProgress :model-value="status?.server.disk.rootUsedPercent || 0" />
|
||||||
|
<p class="mt-1 text-xs text-muted">
|
||||||
|
{{ formatBytes(status?.server.disk.rootUsedBytes) }} von {{ formatBytes(status?.server.disk.rootTotalBytes) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-server-stack" class="size-5 text-primary" />
|
||||||
|
<h2 class="text-base font-semibold text-highlighted">Backend</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<div class="flex justify-between gap-3">
|
||||||
|
<span class="text-muted">Status</span>
|
||||||
|
<UBadge color="success" variant="soft">{{ status?.backend.status || "-" }}</UBadge>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-3">
|
||||||
|
<span class="text-muted">Laufzeit</span>
|
||||||
|
<span class="text-highlighted">{{ formatDuration(status?.backend.uptimeSeconds) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-3">
|
||||||
|
<span class="text-muted">Node.js</span>
|
||||||
|
<span class="text-highlighted">{{ status?.backend.nodeVersion || "-" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-3">
|
||||||
|
<span class="text-muted">Umgebung</span>
|
||||||
|
<span class="text-highlighted">{{ status?.backend.environment || "-" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-signal" class="size-5 text-primary" />
|
||||||
|
<h2 class="text-base font-semibold text-highlighted">Dienste</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="divide-y divide-default">
|
||||||
|
<div
|
||||||
|
v-for="service in serviceRows"
|
||||||
|
:key="service.key"
|
||||||
|
class="flex items-center justify-between gap-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="font-medium text-highlighted">{{ service.label }}</p>
|
||||||
|
<p class="truncate text-xs text-muted">
|
||||||
|
{{ service.error || service.url || service.publicBaseUrl || service.status }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<UBadge
|
||||||
|
:color="service.ok ? 'success' : 'error'"
|
||||||
|
variant="soft"
|
||||||
|
>
|
||||||
|
{{ service.ok ? "OK" : "Fehler" }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
</template>
|
||||||
File diff suppressed because it is too large
Load Diff
1
mobile/.nvmrc
Normal file
1
mobile/.nvmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
22.22.3
|
||||||
@@ -1,50 +1,37 @@
|
|||||||
# Welcome to your Expo app 👋
|
# FEDEO Mobile
|
||||||
|
|
||||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
Expo/React-Native-App für FEDEO. Der iOS-TestFlight-Build wird als Store-Build erstellt und enthält das JavaScript-Bundle. Metro wird dafür nicht benötigt.
|
||||||
|
|
||||||
## Get started
|
## Voraussetzungen
|
||||||
|
|
||||||
1. Install dependencies
|
- Node.js `22.22.3` verwenden, siehe `.nvmrc`.
|
||||||
|
- EAS CLI installieren oder über `npx eas-cli` ausführen.
|
||||||
|
- Apple Developer Zugriff für `software.federspiel.fedeo`.
|
||||||
|
- Produktions-API ist im EAS-Profil `testflight` auf `https://app.fedeo.de/backend` gesetzt.
|
||||||
|
|
||||||
```bash
|
## Entwicklung im Simulator
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Start the app
|
Wenn Port `8081` lokal belegt ist, Metro auf einem freien Port starten und den Simulator auf diesen Port setzen:
|
||||||
|
|
||||||
```bash
|
|
||||||
npx expo start
|
|
||||||
```
|
|
||||||
|
|
||||||
In the output, you'll find options to open the app in a
|
|
||||||
|
|
||||||
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
|
||||||
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
|
||||||
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
|
||||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
|
||||||
|
|
||||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
|
||||||
|
|
||||||
## Get a fresh project
|
|
||||||
|
|
||||||
When you're ready, run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run reset-project
|
npx --yes -p node@22 node ./node_modules/expo/bin/cli start --dev-client --localhost --port 8082 --clear
|
||||||
|
xcrun simctl spawn booted defaults write software.federspiel.fedeo RCT_jsLocation '127.0.0.1:8082'
|
||||||
|
xcrun simctl spawn booted defaults write software.federspiel.fedeo RCT_packager_scheme 'http'
|
||||||
|
xcrun simctl terminate booted software.federspiel.fedeo
|
||||||
|
xcrun simctl launch booted software.federspiel.fedeo
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
## TestFlight vorbereiten
|
||||||
|
|
||||||
## Learn more
|
```bash
|
||||||
|
npm run preflight:testflight
|
||||||
|
npm run build:ios:testflight
|
||||||
|
```
|
||||||
|
|
||||||
To learn more about developing your project with Expo, look at the following resources:
|
Nach erfolgreichem EAS-Build:
|
||||||
|
|
||||||
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
```bash
|
||||||
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
npm run submit:ios:testflight
|
||||||
|
```
|
||||||
|
|
||||||
## Join the community
|
Der TestFlight-Build nutzt das EAS-Profil `testflight` aus `eas.json` mit Store-Distribution und automatischer Buildnummer-Erhöhung.
|
||||||
|
|
||||||
Join our community of developers creating universal apps.
|
|
||||||
|
|
||||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
|
||||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "FEDEO",
|
"name": "FEDEO",
|
||||||
"slug": "fedeo-mobile",
|
"slug": "fedeo-mobile",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "fedeo",
|
"scheme": "fedeo",
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "de.fedeo.mobile",
|
"bundleIdentifier": "software.federspiel.fedeo",
|
||||||
"buildNumber": "1",
|
"buildNumber": "3",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSCameraUsageDescription": "Die Kamera wird benötigt, um Fotos zu Projekten und Objekten als Dokumente hochzuladen.",
|
"NSCameraUsageDescription": "Die Kamera wird benötigt, um Fotos zu Projekten und Objekten als Dokumente hochzuladen.",
|
||||||
"NSPhotoLibraryUsageDescription": "Der Zugriff auf Fotos wird benötigt, um Bilder als Dokumente hochzuladen.",
|
"NSPhotoLibraryUsageDescription": "Der Zugriff auf Fotos wird benötigt, um Bilder als Dokumente hochzuladen.",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"package": "de.fedeo.mobile",
|
"package": "software.federspiel.fedeo",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"android.permission.BLUETOOTH_SCAN",
|
"android.permission.BLUETOOTH_SCAN",
|
||||||
"android.permission.BLUETOOTH_CONNECT",
|
"android.permission.BLUETOOTH_CONNECT",
|
||||||
@@ -54,7 +54,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"react-native-ble-plx"
|
"react-native-ble-plx",
|
||||||
|
"expo-web-browser"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ export default function TabLayout() {
|
|||||||
tabBarIcon: ({ color }) => <IconSymbol size={24} name="folder.fill" color={color} />,
|
tabBarIcon: ({ color }) => <IconSymbol size={24} name="folder.fill" color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="communication"
|
||||||
|
options={{
|
||||||
|
title: 'Kommunikation',
|
||||||
|
tabBarIcon: ({ color }) => <IconSymbol size={24} name="message.fill" color={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="time"
|
name="time"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
918
mobile/app/(tabs)/communication.tsx
Normal file
918
mobile/app/(tabs)/communication.tsx
Normal file
@@ -0,0 +1,918 @@
|
|||||||
|
import * as DocumentPicker from 'expo-document-picker';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
|
FlatList,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Modal,
|
||||||
|
Platform,
|
||||||
|
Pressable,
|
||||||
|
RefreshControl,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createMatrixRoom,
|
||||||
|
deleteMatrixMessage,
|
||||||
|
editMatrixMessage,
|
||||||
|
fetchMatrixIdentity,
|
||||||
|
fetchMatrixMembers,
|
||||||
|
fetchMatrixMessages,
|
||||||
|
fetchMatrixRooms,
|
||||||
|
fetchMatrixStatus,
|
||||||
|
fetchMatrixUsers,
|
||||||
|
inviteMatrixMember,
|
||||||
|
markMatrixRoomRead,
|
||||||
|
MatrixMember,
|
||||||
|
MatrixMessage,
|
||||||
|
MatrixRoom,
|
||||||
|
MatrixStatus,
|
||||||
|
MatrixUser,
|
||||||
|
provisionMatrixRoom,
|
||||||
|
provisionMatrixUser,
|
||||||
|
reactToMatrixMessage,
|
||||||
|
removeMatrixMember,
|
||||||
|
sendMatrixMessage,
|
||||||
|
syncMatrixMembers,
|
||||||
|
syncMatrixRoom,
|
||||||
|
uploadMatrixAttachment,
|
||||||
|
} from '@/src/lib/api';
|
||||||
|
import { useAuth } from '@/src/providers/auth-provider';
|
||||||
|
|
||||||
|
const PRIMARY = '#69c350';
|
||||||
|
const REACTION_PRESETS = ['👍', '✅', '👀', '🙏'];
|
||||||
|
|
||||||
|
type DialogMode = 'create-room' | 'edit-message' | 'invite-member' | null;
|
||||||
|
|
||||||
|
function normalizeRoomKey(value: string): string {
|
||||||
|
return value
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-z0-9_-]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '')
|
||||||
|
.slice(0, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageTime(value: MatrixMessage['timestamp']): string {
|
||||||
|
if (!value) return '';
|
||||||
|
const date = typeof value === 'number' ? new Date(value) : new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
return date.toLocaleString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function messagePreview(message: MatrixMessage): string {
|
||||||
|
return message.body || message.attachment?.fileName || 'Nachricht';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortMessages(messages: MatrixMessage[]): MatrixMessage[] {
|
||||||
|
return [...messages].sort((a, b) => {
|
||||||
|
const left = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
||||||
|
const right = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
||||||
|
return left - right;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeMessages(current: MatrixMessage[], incoming: MatrixMessage[]): MatrixMessage[] {
|
||||||
|
const byId = new Map<string, MatrixMessage>();
|
||||||
|
for (const message of current) byId.set(message.id, message);
|
||||||
|
for (const message of incoming || []) byId.set(message.id, { ...byId.get(message.id), ...message });
|
||||||
|
return sortMessages(Array.from(byId.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyReplacements(current: MatrixMessage[], replacements: MatrixMessage[] = []): MatrixMessage[] {
|
||||||
|
if (!replacements.length) return current;
|
||||||
|
const byTarget = new Map<string, MatrixMessage>();
|
||||||
|
for (const replacement of replacements) {
|
||||||
|
const targetId = String(replacement.targetEventId || replacement.replyToEventId || replacement.id || '');
|
||||||
|
if (targetId) byTarget.set(targetId, replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return current.map((message) => {
|
||||||
|
const replacement = byTarget.get(message.id);
|
||||||
|
if (!replacement) return message;
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
body: replacement.body ?? message.body,
|
||||||
|
edited: true,
|
||||||
|
timestamp: replacement.timestamp || message.timestamp,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRedactions(
|
||||||
|
current: MatrixMessage[],
|
||||||
|
redactions: { redacts?: string; eventId?: string; targetEventId?: string }[] = []
|
||||||
|
): MatrixMessage[] {
|
||||||
|
if (!redactions.length) return current;
|
||||||
|
const ids = new Set(redactions.map((item) => item.redacts || item.eventId || item.targetEventId).filter(Boolean));
|
||||||
|
return current.filter((message) => !ids.has(message.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyReactions(
|
||||||
|
current: MatrixMessage[],
|
||||||
|
reactions: { targetEventId?: string; key: string; count?: number; own?: boolean; senders?: string[] }[] = []
|
||||||
|
): MatrixMessage[] {
|
||||||
|
if (!reactions.length) return current;
|
||||||
|
return current.map((message) => {
|
||||||
|
const next = reactions.filter((reaction) => reaction.targetEventId === message.id);
|
||||||
|
if (!next.length) return message;
|
||||||
|
return { ...message, reactions: next };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CommunicationScreen() {
|
||||||
|
const { token } = useAuth();
|
||||||
|
const messageListRef = useRef<FlatList<MatrixMessage>>(null);
|
||||||
|
const [status, setStatus] = useState<MatrixStatus | null>(null);
|
||||||
|
const [matrixUserId, setMatrixUserId] = useState('');
|
||||||
|
const [rooms, setRooms] = useState<MatrixRoom[]>([]);
|
||||||
|
const [members, setMembers] = useState<MatrixMember[]>([]);
|
||||||
|
const [users, setUsers] = useState<MatrixUser[]>([]);
|
||||||
|
const [messages, setMessages] = useState<MatrixMessage[]>([]);
|
||||||
|
const [activeRoomKey, setActiveRoomKey] = useState('allgemein');
|
||||||
|
const [syncSince, setSyncSince] = useState<string | undefined>();
|
||||||
|
const [draft, setDraft] = useState('');
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
|
const [provisioning, setProvisioning] = useState(false);
|
||||||
|
const [membersOpen, setMembersOpen] = useState(false);
|
||||||
|
const [replyTarget, setReplyTarget] = useState<MatrixMessage | null>(null);
|
||||||
|
const [editingMessage, setEditingMessage] = useState<MatrixMessage | null>(null);
|
||||||
|
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||||
|
const [roomName, setRoomName] = useState('');
|
||||||
|
const [roomTopic, setRoomTopic] = useState('');
|
||||||
|
const [roomKey, setRoomKey] = useState('');
|
||||||
|
const [dialogText, setDialogText] = useState('');
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState('');
|
||||||
|
|
||||||
|
const activeRoom = useMemo(
|
||||||
|
() => rooms.find((room) => room.key === activeRoomKey) || rooms[0] || null,
|
||||||
|
[activeRoomKey, rooms]
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleRooms = useMemo(() => {
|
||||||
|
const query = search.trim().toLowerCase();
|
||||||
|
const base = [...rooms].sort((a, b) => {
|
||||||
|
const groupCompare = String(a.group || '').localeCompare(String(b.group || ''));
|
||||||
|
if (groupCompare !== 0) return groupCompare;
|
||||||
|
return String(a.name || a.key).localeCompare(String(b.name || b.key));
|
||||||
|
});
|
||||||
|
if (!query) return base;
|
||||||
|
return base.filter((room) =>
|
||||||
|
[room.name, room.key, room.topic, room.email, room.projectNumber]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((value) => String(value).toLowerCase().includes(query))
|
||||||
|
);
|
||||||
|
}, [rooms, search]);
|
||||||
|
|
||||||
|
const inviteCandidates = useMemo(() => {
|
||||||
|
const existing = new Set(members.map((member) => member.matrixUserId));
|
||||||
|
return users.filter((user) => !existing.has(user.matrixUserId));
|
||||||
|
}, [members, users]);
|
||||||
|
|
||||||
|
const loadRooms = useCallback(async () => {
|
||||||
|
if (!token) return;
|
||||||
|
const [nextStatus, identity, nextRooms, nextUsers] = await Promise.all([
|
||||||
|
fetchMatrixStatus(token),
|
||||||
|
fetchMatrixIdentity(token),
|
||||||
|
fetchMatrixRooms(token),
|
||||||
|
fetchMatrixUsers(token),
|
||||||
|
]);
|
||||||
|
setStatus(nextStatus);
|
||||||
|
setMatrixUserId(identity.matrixUserId || '');
|
||||||
|
setRooms(nextRooms);
|
||||||
|
|
||||||
|
if (!nextRooms.some((room) => room.key === activeRoomKey) && nextRooms[0]) {
|
||||||
|
setActiveRoomKey(nextRooms[0].key);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUsers(nextUsers);
|
||||||
|
}, [activeRoomKey, token]);
|
||||||
|
|
||||||
|
const loadRoomContent = useCallback(
|
||||||
|
async (roomKeyToLoad: string, showSpinner = true) => {
|
||||||
|
if (!token || !roomKeyToLoad) return;
|
||||||
|
if (showSpinner) setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [nextMessages, nextMembers, sync] = await Promise.all([
|
||||||
|
fetchMatrixMessages(token, roomKeyToLoad),
|
||||||
|
fetchMatrixMembers(token, roomKeyToLoad),
|
||||||
|
syncMatrixRoom(token, roomKeyToLoad, undefined, true),
|
||||||
|
]);
|
||||||
|
const merged = mergeMessages(nextMessages, sync.messages || []);
|
||||||
|
setMessages(sortMessages(merged));
|
||||||
|
setMembers(sync.members || nextMembers);
|
||||||
|
setSyncSince(sync.nextBatch);
|
||||||
|
const last = merged.at(-1);
|
||||||
|
if (last?.id) await markMatrixRoomRead(token, roomKeyToLoad, last.id);
|
||||||
|
requestAnimationFrame(() => messageListRef.current?.scrollToEnd({ animated: true }));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Kommunikation konnte nicht geladen werden.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[token]
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshAll = useCallback(
|
||||||
|
async (showSpinner = true) => {
|
||||||
|
if (!token) return;
|
||||||
|
if (showSpinner) setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadRooms();
|
||||||
|
await loadRoomContent(activeRoomKey, false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Matrix-Kommunikation konnte nicht geladen werden.');
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activeRoomKey, loadRoomContent, loadRooms, token]
|
||||||
|
);
|
||||||
|
|
||||||
|
const pollSync = useCallback(async () => {
|
||||||
|
if (!token || !activeRoom?.exists || !syncSince) return;
|
||||||
|
try {
|
||||||
|
const sync = await syncMatrixRoom(token, activeRoom.key, syncSince);
|
||||||
|
setSyncSince(sync.nextBatch || syncSince);
|
||||||
|
setMembers((current) => sync.members || current);
|
||||||
|
setMessages((current) => {
|
||||||
|
let next = mergeMessages(current, sync.messages || []);
|
||||||
|
next = applyReplacements(next, sync.replacements);
|
||||||
|
next = applyReactions(next, sync.reactions);
|
||||||
|
next = applyRedactions(next, sync.redactions);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Polling errors are surfaced by manual refresh to avoid noisy chat usage.
|
||||||
|
}
|
||||||
|
}, [activeRoom, syncSince, token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshAll(true);
|
||||||
|
}, [refreshAll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeRoomKey) return;
|
||||||
|
setReplyTarget(null);
|
||||||
|
setEditingMessage(null);
|
||||||
|
setMessages([]);
|
||||||
|
setMembers([]);
|
||||||
|
setSyncSince(undefined);
|
||||||
|
void loadRoomContent(activeRoomKey, true);
|
||||||
|
}, [activeRoomKey, loadRoomContent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => void pollSync(), 5000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [pollSync]);
|
||||||
|
|
||||||
|
async function ensureActiveRoom(): Promise<MatrixRoom | null> {
|
||||||
|
if (!token || !activeRoom) return null;
|
||||||
|
if (activeRoom.exists) return activeRoom;
|
||||||
|
|
||||||
|
setProvisioning(true);
|
||||||
|
try {
|
||||||
|
const room = await provisionMatrixRoom(token, activeRoom);
|
||||||
|
setRooms((current) => current.map((item) => (item.key === activeRoom.key ? { ...item, ...room, exists: true } : item)));
|
||||||
|
await loadRoomContent(activeRoom.key, false);
|
||||||
|
return { ...activeRoom, ...room, exists: true };
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Raum konnte nicht bereitgestellt werden.');
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setProvisioning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessage() {
|
||||||
|
if (!token || !draft.trim()) return;
|
||||||
|
const room = await ensureActiveRoom();
|
||||||
|
if (!room) return;
|
||||||
|
|
||||||
|
setSending(true);
|
||||||
|
try {
|
||||||
|
const message = editingMessage
|
||||||
|
? await editMatrixMessage(token, room.key, editingMessage.id, draft.trim())
|
||||||
|
: await sendMatrixMessage(token, room.key, draft.trim(), replyTarget?.id);
|
||||||
|
setMessages((current) =>
|
||||||
|
editingMessage
|
||||||
|
? current.map((item) => (item.id === editingMessage.id ? { ...item, ...message, edited: true } : item))
|
||||||
|
: mergeMessages(current, [message])
|
||||||
|
);
|
||||||
|
setDraft('');
|
||||||
|
setReplyTarget(null);
|
||||||
|
setEditingMessage(null);
|
||||||
|
requestAnimationFrame(() => messageListRef.current?.scrollToEnd({ animated: true }));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Nachricht konnte nicht gesendet werden.');
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickAttachment() {
|
||||||
|
if (!token) return;
|
||||||
|
const room = await ensureActiveRoom();
|
||||||
|
if (!room) return;
|
||||||
|
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({ copyToCacheDirectory: true, multiple: false });
|
||||||
|
if (result.canceled || !result.assets[0]) return;
|
||||||
|
|
||||||
|
setSending(true);
|
||||||
|
try {
|
||||||
|
const asset = result.assets[0];
|
||||||
|
const message = await uploadMatrixAttachment(token, room.key, {
|
||||||
|
uri: asset.uri,
|
||||||
|
name: asset.name || 'Anhang',
|
||||||
|
mimeType: asset.mimeType,
|
||||||
|
});
|
||||||
|
setMessages((current) => mergeMessages(current, [message]));
|
||||||
|
requestAnimationFrame(() => messageListRef.current?.scrollToEnd({ animated: true }));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Anhang konnte nicht hochgeladen werden.');
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function provisionUser() {
|
||||||
|
if (!token) return;
|
||||||
|
setProvisioning(true);
|
||||||
|
try {
|
||||||
|
const identity = await provisionMatrixUser(token);
|
||||||
|
setMatrixUserId(identity.matrixUserId || '');
|
||||||
|
await refreshAll(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Matrix-Benutzer konnte nicht erstellt werden.');
|
||||||
|
} finally {
|
||||||
|
setProvisioning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRoomFromDialog() {
|
||||||
|
if (!token || !roomName.trim()) return;
|
||||||
|
const key = roomKey.trim() || normalizeRoomKey(roomName);
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
setProvisioning(true);
|
||||||
|
try {
|
||||||
|
const room = await createMatrixRoom(token, {
|
||||||
|
key,
|
||||||
|
name: roomName.trim(),
|
||||||
|
topic: roomTopic.trim() || null,
|
||||||
|
type: 'room',
|
||||||
|
});
|
||||||
|
setRooms((current) => [{ ...room, group: 'Räume', exists: true }, ...current.filter((item) => item.key !== key)]);
|
||||||
|
setActiveRoomKey(key);
|
||||||
|
setDialogMode(null);
|
||||||
|
setRoomName('');
|
||||||
|
setRoomTopic('');
|
||||||
|
setRoomKey('');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Raum konnte nicht erstellt werden.');
|
||||||
|
} finally {
|
||||||
|
setProvisioning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inviteSelectedUser() {
|
||||||
|
if (!token || !activeRoom || !selectedUserId) return;
|
||||||
|
const room = await ensureActiveRoom();
|
||||||
|
if (!room) return;
|
||||||
|
|
||||||
|
setProvisioning(true);
|
||||||
|
try {
|
||||||
|
await inviteMatrixMember(token, room.key, selectedUserId);
|
||||||
|
setMembers(await fetchMatrixMembers(token, room.key));
|
||||||
|
setDialogMode(null);
|
||||||
|
setSelectedUserId('');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Mitglied konnte nicht eingeladen werden.');
|
||||||
|
} finally {
|
||||||
|
setProvisioning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(message: MatrixMessage) {
|
||||||
|
setEditingMessage(message);
|
||||||
|
setDialogText(message.body || '');
|
||||||
|
setDialogMode('edit-message');
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(message: MatrixMessage) {
|
||||||
|
Alert.alert('Nachricht löschen', 'Diese Nachricht wirklich entfernen?', [
|
||||||
|
{ text: 'Abbrechen', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Löschen',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => {
|
||||||
|
if (!token || !activeRoom) return;
|
||||||
|
void deleteMatrixMessage(token, activeRoom.key, message.id)
|
||||||
|
.then(() => setMessages((current) => current.filter((item) => item.id !== message.id)))
|
||||||
|
.catch((err) => setError(err instanceof Error ? err.message : 'Nachricht konnte nicht gelöscht werden.'));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmRemoveMember(member: MatrixMember) {
|
||||||
|
Alert.alert('Mitglied entfernen', `${member.displayName || member.matrixUserId} aus dem Raum entfernen?`, [
|
||||||
|
{ text: 'Abbrechen', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Entfernen',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => {
|
||||||
|
if (!token || !activeRoom) return;
|
||||||
|
void removeMatrixMember(token, activeRoom.key, member.matrixUserId)
|
||||||
|
.then(() => setMembers((current) => current.filter((item) => item.matrixUserId !== member.matrixUserId)))
|
||||||
|
.catch((err) => setError(err instanceof Error ? err.message : 'Mitglied konnte nicht entfernt werden.'));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addReaction(message: MatrixMessage, key: string) {
|
||||||
|
if (!token || !activeRoom) return;
|
||||||
|
try {
|
||||||
|
await reactToMatrixMessage(token, activeRoom.key, message.id, key);
|
||||||
|
await pollSync();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Reaktion konnte nicht gesendet werden.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncMembersNow() {
|
||||||
|
if (!token || !activeRoom) return;
|
||||||
|
setProvisioning(true);
|
||||||
|
try {
|
||||||
|
await syncMatrixMembers(token, activeRoom.key);
|
||||||
|
setMembers(await fetchMatrixMembers(token, activeRoom.key));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Mitglieder konnten nicht synchronisiert werden.');
|
||||||
|
} finally {
|
||||||
|
setProvisioning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoom({ item }: { item: MatrixRoom }) {
|
||||||
|
const active = item.key === activeRoomKey;
|
||||||
|
return (
|
||||||
|
<Pressable style={[styles.roomChip, active && styles.roomChipActive]} onPress={() => setActiveRoomKey(item.key)}>
|
||||||
|
<Text style={[styles.roomGroup, active && styles.roomTextActive]}>{item.group || 'Raum'}</Text>
|
||||||
|
<Text style={[styles.roomName, active && styles.roomTextActive]} numberOfLines={1}>
|
||||||
|
{item.name || item.key}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.roomMetaRow}>
|
||||||
|
{!item.exists ? <Text style={styles.roomBadgeMuted}>bereitstellen</Text> : null}
|
||||||
|
{item.unread ? <Text style={styles.roomBadge}>{item.mentions ? `@${item.mentions}` : item.unread}</Text> : null}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMessage({ item }: { item: MatrixMessage }) {
|
||||||
|
const own = Boolean(item.own || item.sender === matrixUserId);
|
||||||
|
const reply = item.replyToEventId ? messages.find((message) => message.id === item.replyToEventId) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.messageRow, own && styles.messageRowOwn]}>
|
||||||
|
<View style={[styles.messageBubble, own && styles.messageBubbleOwn]}>
|
||||||
|
<View style={styles.messageHeader}>
|
||||||
|
<Text style={[styles.messageSender, own && styles.messageSenderOwn]} numberOfLines={1}>
|
||||||
|
{own ? 'Du' : item.senderDisplayName || item.sender}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.messageTime, own && styles.messageTimeOwn]}>{messageTime(item.timestamp)}</Text>
|
||||||
|
</View>
|
||||||
|
{reply ? (
|
||||||
|
<View style={[styles.replyBox, own && styles.replyBoxOwn]}>
|
||||||
|
<Text style={[styles.replyText, own && styles.replyTextOwn]} numberOfLines={2}>
|
||||||
|
{reply.senderDisplayName || reply.sender}: {messagePreview(reply)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{item.body ? <Text style={[styles.messageBody, own && styles.messageBodyOwn]}>{item.body}</Text> : null}
|
||||||
|
{item.attachment ? (
|
||||||
|
<View style={[styles.attachmentBox, own && styles.attachmentBoxOwn]}>
|
||||||
|
<Text style={[styles.attachmentTitle, own && styles.messageBodyOwn]} numberOfLines={1}>
|
||||||
|
{item.attachment.fileName || 'Anhang'}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.attachmentMeta, own && styles.messageTimeOwn]}>
|
||||||
|
{item.attachment.mimeType || 'Datei'} {item.attachment.size ? `· ${Math.round(item.attachment.size / 1024)} KB` : ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{item.edited ? <Text style={[styles.editedText, own && styles.messageTimeOwn]}>bearbeitet</Text> : null}
|
||||||
|
{item.reactions?.length ? (
|
||||||
|
<View style={styles.reactionRow}>
|
||||||
|
{item.reactions.map((reaction) => (
|
||||||
|
<Text key={reaction.key} style={[styles.reactionPill, reaction.own && styles.reactionPillOwn]}>
|
||||||
|
{reaction.key} {reaction.count || ''}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
<View style={styles.messageActions}>
|
||||||
|
<Pressable onPress={() => setReplyTarget(item)}>
|
||||||
|
<Text style={[styles.actionText, own && styles.actionTextOwn]}>Antworten</Text>
|
||||||
|
</Pressable>
|
||||||
|
{REACTION_PRESETS.map((reaction) => (
|
||||||
|
<Pressable key={reaction} onPress={() => addReaction(item, reaction)}>
|
||||||
|
<Text style={[styles.actionText, own && styles.actionTextOwn]}>{reaction}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
{own && !item.attachment ? (
|
||||||
|
<Pressable onPress={() => openEdit(item)}>
|
||||||
|
<Text style={[styles.actionText, styles.actionTextOwn]}>Bearbeiten</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
{own ? (
|
||||||
|
<Pressable onPress={() => confirmDelete(item)}>
|
||||||
|
<Text style={[styles.actionText, styles.actionTextDestructive]}>Löschen</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.screen}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={styles.headerMain}>
|
||||||
|
<Text style={styles.title}>Kommunikation</Text>
|
||||||
|
<Text style={styles.subtitle} numberOfLines={1}>
|
||||||
|
{activeRoom?.name || 'Matrix Chat'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable style={styles.headerButton} onPress={() => setDialogMode('create-room')}>
|
||||||
|
<Text style={styles.headerButtonText}>+</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.headerButton} onPress={() => setMembersOpen((value) => !value)}>
|
||||||
|
<Text style={styles.headerButtonText}>☰</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||||||
|
|
||||||
|
{status && status.enabled === false ? (
|
||||||
|
<View style={styles.notice}>
|
||||||
|
<Text style={styles.noticeTitle}>Matrix ist nicht aktiv</Text>
|
||||||
|
<Text style={styles.noticeText}>Die Kommunikation ist serverseitig noch nicht aktiviert.</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!matrixUserId ? (
|
||||||
|
<View style={styles.notice}>
|
||||||
|
<Text style={styles.noticeTitle}>Matrix-Benutzer fehlt</Text>
|
||||||
|
<Text style={styles.noticeText}>Lege deinen Matrix-Zugang an, um Räume nutzen zu können.</Text>
|
||||||
|
<Pressable style={styles.primaryButton} onPress={provisionUser} disabled={provisioning}>
|
||||||
|
<Text style={styles.primaryButtonText}>{provisioning ? 'Wird erstellt...' : 'Benutzer erstellen'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View style={styles.roomPanel}>
|
||||||
|
<TextInput
|
||||||
|
style={styles.searchInput}
|
||||||
|
placeholder="Räume, Projekte oder Personen suchen"
|
||||||
|
value={search}
|
||||||
|
onChangeText={setSearch}
|
||||||
|
/>
|
||||||
|
<FlatList
|
||||||
|
data={visibleRooms}
|
||||||
|
horizontal
|
||||||
|
keyExtractor={(item) => item.key}
|
||||||
|
renderItem={renderRoom}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.roomList}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{membersOpen ? (
|
||||||
|
<View style={styles.membersPanel}>
|
||||||
|
<View style={styles.membersHeader}>
|
||||||
|
<Text style={styles.membersTitle}>Mitglieder · {members.length}</Text>
|
||||||
|
<View style={styles.membersActions}>
|
||||||
|
<Pressable onPress={syncMembersNow}>
|
||||||
|
<Text style={styles.linkText}>Sync</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable onPress={() => setDialogMode('invite-member')}>
|
||||||
|
<Text style={styles.linkText}>Einladen</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.memberList}>
|
||||||
|
{members.map((member) => (
|
||||||
|
<Pressable key={member.matrixUserId} style={styles.memberPill} onLongPress={() => confirmRemoveMember(member)}>
|
||||||
|
<Text style={styles.memberName}>{member.displayName || member.matrixUserId}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!activeRoom?.exists ? (
|
||||||
|
<View style={styles.provisionPanel}>
|
||||||
|
<Text style={styles.noticeTitle}>Raum noch nicht bereitgestellt</Text>
|
||||||
|
<Text style={styles.noticeText}>Beim Öffnen wird der Matrix-Raum inklusive Einladungen angelegt.</Text>
|
||||||
|
<Pressable style={styles.primaryButton} onPress={ensureActiveRoom} disabled={provisioning}>
|
||||||
|
<Text style={styles.primaryButtonText}>{provisioning ? 'Wird bereitgestellt...' : 'Raum bereitstellen'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<View style={styles.loading}>
|
||||||
|
<ActivityIndicator />
|
||||||
|
<Text style={styles.loadingText}>Nachrichten werden geladen...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
ref={messageListRef}
|
||||||
|
data={messages}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
renderItem={renderMessage}
|
||||||
|
contentContainerStyle={styles.messageList}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={() => {
|
||||||
|
setRefreshing(true);
|
||||||
|
void refreshAll(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
ListEmptyComponent={<Text style={styles.emptyText}>Noch keine Nachrichten in diesem Raum.</Text>}
|
||||||
|
onContentSizeChange={() => messageListRef.current?.scrollToEnd({ animated: true })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(replyTarget || editingMessage) && !dialogMode ? (
|
||||||
|
<View style={styles.composerContext}>
|
||||||
|
<View style={styles.composerContextMain}>
|
||||||
|
<Text style={styles.composerContextLabel}>{editingMessage ? 'Bearbeiten' : 'Antwort auf'}</Text>
|
||||||
|
<Text style={styles.composerContextText} numberOfLines={1}>
|
||||||
|
{messagePreview(editingMessage || replyTarget!)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
setReplyTarget(null);
|
||||||
|
setEditingMessage(null);
|
||||||
|
setDraft('');
|
||||||
|
}}>
|
||||||
|
<Text style={styles.linkText}>Abbrechen</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View style={styles.composer}>
|
||||||
|
<Pressable style={styles.attachButton} onPress={pickAttachment} disabled={sending || !activeRoom}>
|
||||||
|
<Text style={styles.attachButtonText}>+</Text>
|
||||||
|
</Pressable>
|
||||||
|
<TextInput
|
||||||
|
style={styles.composerInput}
|
||||||
|
placeholder="Nachricht schreiben"
|
||||||
|
value={draft}
|
||||||
|
onChangeText={setDraft}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
<Pressable style={[styles.sendButton, (!draft.trim() || sending) && styles.sendButtonDisabled]} onPress={sendMessage}>
|
||||||
|
<Text style={styles.sendButtonText}>{sending ? '...' : 'Senden'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Modal transparent visible={Boolean(dialogMode)} animationType="fade" onRequestClose={() => setDialogMode(null)}>
|
||||||
|
<View style={styles.modalBackdrop}>
|
||||||
|
<View style={styles.modalCard}>
|
||||||
|
{dialogMode === 'create-room' ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.modalTitle}>Raum erstellen</Text>
|
||||||
|
<TextInput style={styles.modalInput} placeholder="Name" value={roomName} onChangeText={setRoomName} />
|
||||||
|
<TextInput
|
||||||
|
style={styles.modalInput}
|
||||||
|
placeholder="Schlüssel, optional"
|
||||||
|
value={roomKey}
|
||||||
|
onChangeText={setRoomKey}
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
<TextInput style={styles.modalInput} placeholder="Thema, optional" value={roomTopic} onChangeText={setRoomTopic} />
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<Pressable onPress={() => setDialogMode(null)}>
|
||||||
|
<Text style={styles.linkText}>Abbrechen</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.primaryButton} onPress={createRoomFromDialog}>
|
||||||
|
<Text style={styles.primaryButtonText}>Erstellen</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{dialogMode === 'edit-message' ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.modalTitle}>Nachricht bearbeiten</Text>
|
||||||
|
<TextInput style={[styles.modalInput, styles.modalTextarea]} value={dialogText} onChangeText={setDialogText} multiline />
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<Pressable onPress={() => setDialogMode(null)}>
|
||||||
|
<Text style={styles.linkText}>Abbrechen</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
style={styles.primaryButton}
|
||||||
|
onPress={() => {
|
||||||
|
setDraft(dialogText);
|
||||||
|
setDialogMode(null);
|
||||||
|
}}>
|
||||||
|
<Text style={styles.primaryButtonText}>Übernehmen</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{dialogMode === 'invite-member' ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.modalTitle}>Mitglied einladen</Text>
|
||||||
|
<ScrollView style={styles.inviteList}>
|
||||||
|
{inviteCandidates.map((user) => (
|
||||||
|
<Pressable
|
||||||
|
key={user.userId}
|
||||||
|
style={[styles.inviteRow, selectedUserId === user.userId && styles.inviteRowActive]}
|
||||||
|
onPress={() => setSelectedUserId(user.userId)}>
|
||||||
|
<Text style={styles.inviteName}>{user.displayName || user.email || user.userId}</Text>
|
||||||
|
<Text style={styles.inviteMeta}>{user.matrixUserId}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
{!inviteCandidates.length ? <Text style={styles.emptyText}>Keine weiteren Benutzer verfügbar.</Text> : null}
|
||||||
|
</ScrollView>
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<Pressable onPress={() => setDialogMode(null)}>
|
||||||
|
<Text style={styles.linkText}>Abbrechen</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={[styles.primaryButton, !selectedUserId && styles.sendButtonDisabled]} onPress={inviteSelectedUser}>
|
||||||
|
<Text style={styles.primaryButtonText}>Einladen</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
screen: { flex: 1, backgroundColor: '#f9fafb' },
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderBottomColor: '#e5e7eb',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
},
|
||||||
|
headerMain: { flex: 1, minWidth: 0 },
|
||||||
|
title: { color: '#111827', fontSize: 20, fontWeight: '800' },
|
||||||
|
subtitle: { color: '#6b7280', fontSize: 13, marginTop: 2 },
|
||||||
|
headerButton: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
headerButtonText: { color: '#111827', fontSize: 20, fontWeight: '700' },
|
||||||
|
error: { margin: 12, color: '#991b1b', backgroundColor: '#fee2e2', borderRadius: 8, padding: 10 },
|
||||||
|
notice: { margin: 12, padding: 12, borderRadius: 10, backgroundColor: '#fff7ed', borderWidth: 1, borderColor: '#fed7aa', gap: 8 },
|
||||||
|
noticeTitle: { color: '#111827', fontWeight: '700', fontSize: 15 },
|
||||||
|
noticeText: { color: '#6b7280', fontSize: 13 },
|
||||||
|
roomPanel: { backgroundColor: '#ffffff', borderBottomWidth: 1, borderBottomColor: '#e5e7eb', paddingVertical: 10 },
|
||||||
|
searchInput: {
|
||||||
|
marginHorizontal: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
borderRadius: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 9,
|
||||||
|
fontSize: 14,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
roomList: { paddingHorizontal: 12, paddingTop: 10, gap: 8 },
|
||||||
|
roomChip: { width: 150, padding: 10, borderRadius: 10, borderWidth: 1, borderColor: '#e5e7eb', backgroundColor: '#ffffff', gap: 3 },
|
||||||
|
roomChipActive: { backgroundColor: '#eff9ea', borderColor: PRIMARY },
|
||||||
|
roomGroup: { color: '#6b7280', fontSize: 11, textTransform: 'uppercase' },
|
||||||
|
roomName: { color: '#111827', fontSize: 14, fontWeight: '700' },
|
||||||
|
roomTextActive: { color: '#2f6f25' },
|
||||||
|
roomMetaRow: { flexDirection: 'row', gap: 5, minHeight: 18 },
|
||||||
|
roomBadge: { color: '#ffffff', backgroundColor: PRIMARY, borderRadius: 9, overflow: 'hidden', paddingHorizontal: 6, fontSize: 11 },
|
||||||
|
roomBadgeMuted: { color: '#6b7280', backgroundColor: '#f3f4f6', borderRadius: 9, overflow: 'hidden', paddingHorizontal: 6, fontSize: 11 },
|
||||||
|
membersPanel: { backgroundColor: '#ffffff', borderBottomWidth: 1, borderBottomColor: '#e5e7eb', padding: 12, gap: 8 },
|
||||||
|
membersHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||||
|
membersTitle: { color: '#111827', fontWeight: '700' },
|
||||||
|
membersActions: { flexDirection: 'row', gap: 16 },
|
||||||
|
memberList: { gap: 8 },
|
||||||
|
memberPill: { backgroundColor: '#f3f4f6', paddingHorizontal: 10, paddingVertical: 7, borderRadius: 16 },
|
||||||
|
memberName: { color: '#374151', fontSize: 12 },
|
||||||
|
provisionPanel: { margin: 12, padding: 12, borderRadius: 10, backgroundColor: '#ffffff', borderWidth: 1, borderColor: '#e5e7eb', gap: 8 },
|
||||||
|
loading: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8 },
|
||||||
|
loadingText: { color: '#6b7280' },
|
||||||
|
messageList: { padding: 12, gap: 10, flexGrow: 1 },
|
||||||
|
emptyText: { color: '#6b7280', textAlign: 'center', padding: 18 },
|
||||||
|
messageRow: { alignItems: 'flex-start' },
|
||||||
|
messageRowOwn: { alignItems: 'flex-end' },
|
||||||
|
messageBubble: {
|
||||||
|
maxWidth: '88%',
|
||||||
|
minWidth: 120,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#e5e7eb',
|
||||||
|
padding: 10,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
messageBubbleOwn: { backgroundColor: '#3f8f32', borderColor: '#3f8f32' },
|
||||||
|
messageHeader: { flexDirection: 'row', justifyContent: 'space-between', gap: 10 },
|
||||||
|
messageSender: { flex: 1, color: '#111827', fontSize: 12, fontWeight: '700' },
|
||||||
|
messageSenderOwn: { color: '#ffffff' },
|
||||||
|
messageTime: { color: '#6b7280', fontSize: 11 },
|
||||||
|
messageTimeOwn: { color: '#dff4d9' },
|
||||||
|
messageBody: { color: '#111827', fontSize: 15, lineHeight: 21 },
|
||||||
|
messageBodyOwn: { color: '#ffffff' },
|
||||||
|
replyBox: { backgroundColor: '#f3f4f6', borderRadius: 8, borderLeftWidth: 3, borderLeftColor: PRIMARY, padding: 7 },
|
||||||
|
replyBoxOwn: { backgroundColor: '#327628', borderLeftColor: '#dff4d9' },
|
||||||
|
replyText: { color: '#4b5563', fontSize: 12 },
|
||||||
|
replyTextOwn: { color: '#dff4d9' },
|
||||||
|
attachmentBox: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 8, padding: 8, gap: 2 },
|
||||||
|
attachmentBoxOwn: { borderColor: '#dff4d9' },
|
||||||
|
attachmentTitle: { color: '#111827', fontWeight: '700' },
|
||||||
|
attachmentMeta: { color: '#6b7280', fontSize: 12 },
|
||||||
|
editedText: { color: '#6b7280', fontSize: 11 },
|
||||||
|
reactionRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 5 },
|
||||||
|
reactionPill: { backgroundColor: '#f3f4f6', borderRadius: 12, overflow: 'hidden', paddingHorizontal: 7, paddingVertical: 2, fontSize: 12 },
|
||||||
|
reactionPillOwn: { backgroundColor: '#dff4d9' },
|
||||||
|
messageActions: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, paddingTop: 2 },
|
||||||
|
actionText: { color: '#3f8f32', fontSize: 12, fontWeight: '700' },
|
||||||
|
actionTextOwn: { color: '#ffffff' },
|
||||||
|
actionTextDestructive: { color: '#fee2e2' },
|
||||||
|
composerContext: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#e5e7eb',
|
||||||
|
},
|
||||||
|
composerContextMain: { flex: 1, minWidth: 0 },
|
||||||
|
composerContextLabel: { color: '#6b7280', fontSize: 11, textTransform: 'uppercase' },
|
||||||
|
composerContextText: { color: '#111827', fontSize: 13, fontWeight: '600' },
|
||||||
|
composer: { flexDirection: 'row', alignItems: 'flex-end', gap: 8, padding: 10, backgroundColor: '#ffffff', borderTopWidth: 1, borderTopColor: '#e5e7eb' },
|
||||||
|
attachButton: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', backgroundColor: '#f3f4f6' },
|
||||||
|
attachButtonText: { color: '#111827', fontSize: 22, fontWeight: '700' },
|
||||||
|
composerInput: { flex: 1, maxHeight: 110, borderRadius: 18, backgroundColor: '#f3f4f6', paddingHorizontal: 12, paddingVertical: 10, fontSize: 15 },
|
||||||
|
sendButton: { minWidth: 70, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', backgroundColor: PRIMARY, paddingHorizontal: 12 },
|
||||||
|
sendButtonDisabled: { opacity: 0.45 },
|
||||||
|
sendButtonText: { color: '#ffffff', fontWeight: '800' },
|
||||||
|
primaryButton: { alignSelf: 'flex-start', backgroundColor: PRIMARY, borderRadius: 9, paddingHorizontal: 12, paddingVertical: 9 },
|
||||||
|
primaryButtonText: { color: '#ffffff', fontWeight: '800' },
|
||||||
|
linkText: { color: '#3f8f32', fontWeight: '800' },
|
||||||
|
modalBackdrop: { flex: 1, backgroundColor: 'rgba(17, 24, 39, 0.45)', alignItems: 'center', justifyContent: 'center', padding: 18 },
|
||||||
|
modalCard: { width: '100%', maxWidth: 420, borderRadius: 12, backgroundColor: '#ffffff', padding: 16, gap: 12 },
|
||||||
|
modalTitle: { color: '#111827', fontSize: 18, fontWeight: '800' },
|
||||||
|
modalInput: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 9, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15 },
|
||||||
|
modalTextarea: { minHeight: 110, textAlignVertical: 'top' },
|
||||||
|
modalActions: { flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', gap: 14 },
|
||||||
|
inviteList: { maxHeight: 280 },
|
||||||
|
inviteRow: { padding: 10, borderRadius: 9, borderWidth: 1, borderColor: '#e5e7eb', marginBottom: 8 },
|
||||||
|
inviteRowActive: { borderColor: PRIMARY, backgroundColor: '#eff9ea' },
|
||||||
|
inviteName: { color: '#111827', fontWeight: '700' },
|
||||||
|
inviteMeta: { color: '#6b7280', fontSize: 12, marginTop: 2 },
|
||||||
|
});
|
||||||
@@ -18,7 +18,6 @@ export default function RootLayout() {
|
|||||||
title: 'Projekt',
|
title: 'Projekt',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -28,7 +27,6 @@ export default function RootLayout() {
|
|||||||
title: 'Konto',
|
title: 'Konto',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -38,7 +36,6 @@ export default function RootLayout() {
|
|||||||
title: 'Einstellungen',
|
title: 'Einstellungen',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -48,7 +45,6 @@ export default function RootLayout() {
|
|||||||
title: 'Wiki',
|
title: 'Wiki',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -58,7 +54,6 @@ export default function RootLayout() {
|
|||||||
title: 'Kunden',
|
title: 'Kunden',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -68,7 +63,6 @@ export default function RootLayout() {
|
|||||||
title: 'Kunde',
|
title: 'Kunde',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -78,7 +72,6 @@ export default function RootLayout() {
|
|||||||
title: 'Objekte',
|
title: 'Objekte',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -88,7 +81,6 @@ export default function RootLayout() {
|
|||||||
title: 'Objekt',
|
title: 'Objekt',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -98,7 +90,6 @@ export default function RootLayout() {
|
|||||||
title: 'Kundeninventar',
|
title: 'Kundeninventar',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -108,7 +99,6 @@ export default function RootLayout() {
|
|||||||
title: 'Nimbot M2',
|
title: 'Nimbot M2',
|
||||||
headerBackButtonDisplayMode: 'minimal',
|
headerBackButtonDisplayMode: 'minimal',
|
||||||
headerBackTitle: '',
|
headerBackTitle: '',
|
||||||
headerBackTitleVisible: false,
|
|
||||||
headerTintColor: '#111827',
|
headerTintColor: '#111827',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ type IconSymbolName = keyof typeof MAPPING;
|
|||||||
const MAPPING = {
|
const MAPPING = {
|
||||||
'house.fill': 'home',
|
'house.fill': 'home',
|
||||||
'paperplane.fill': 'send',
|
'paperplane.fill': 'send',
|
||||||
|
'message.fill': 'chat',
|
||||||
|
'folder.fill': 'folder',
|
||||||
|
checklist: 'checklist',
|
||||||
|
'clock.fill': 'schedule',
|
||||||
|
'ellipsis.circle.fill': 'more-horiz',
|
||||||
'chevron.left.forwardslash.chevron.right': 'code',
|
'chevron.left.forwardslash.chevron.right': 'code',
|
||||||
'chevron.right': 'chevron-right',
|
'chevron.right': 'chevron-right',
|
||||||
} as IconMapping;
|
} as IconMapping;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"development": {
|
"development": {
|
||||||
|
"node": "22.22.3",
|
||||||
"developmentClient": true,
|
"developmentClient": true,
|
||||||
"distribution": "internal",
|
"distribution": "internal",
|
||||||
"ios": {
|
"ios": {
|
||||||
@@ -11,13 +12,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
|
"node": "22.22.3",
|
||||||
"distribution": "internal"
|
"distribution": "internal"
|
||||||
},
|
},
|
||||||
|
"testflight": {
|
||||||
|
"node": "22.22.3",
|
||||||
|
"distribution": "store",
|
||||||
|
"autoIncrement": true,
|
||||||
|
"env": {
|
||||||
|
"EXPO_PUBLIC_API_BASE": "https://app.fedeo.de/backend"
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"simulator": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
|
"node": "22.22.3",
|
||||||
|
"distribution": "store",
|
||||||
|
"env": {
|
||||||
|
"EXPO_PUBLIC_API_BASE": "https://app.fedeo.de/backend"
|
||||||
|
},
|
||||||
"autoIncrement": true
|
"autoIncrement": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
"production": {}
|
"testflight": {
|
||||||
|
"ios": {}
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"ios": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
397
mobile/package-lock.json
generated
397
mobile/package-lock.json
generated
@@ -1,33 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "mobile",
|
"name": "mobile",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mobile",
|
"name": "mobile",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"expo": "~54.0.33",
|
"expo": "~54.0.34",
|
||||||
"expo-camera": "~17.0.10",
|
"expo-camera": "~17.0.10",
|
||||||
"expo-constants": "~18.0.13",
|
"expo-constants": "~18.0.13",
|
||||||
"expo-document-picker": "^14.0.8",
|
"expo-document-picker": "^14.0.8",
|
||||||
"expo-font": "~14.0.11",
|
"expo-font": "~14.0.11",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
"expo-image-picker": "~17.0.8",
|
"expo-image-picker": "~17.0.11",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.12",
|
||||||
"expo-router": "~6.0.23",
|
"expo-router": "~6.0.23",
|
||||||
"expo-secure-store": "^15.0.8",
|
"expo-secure-store": "^15.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-symbols": "~1.0.8",
|
"expo-symbols": "~1.0.8",
|
||||||
"expo-system-ui": "~6.0.9",
|
"expo-system-ui": "~6.0.9",
|
||||||
"expo-web-browser": "~15.0.10",
|
"expo-web-browser": "~15.0.11",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-webview": "^13.16.0",
|
"react-native-webview": "13.15.0",
|
||||||
"react-native-worklets": "0.5.1"
|
"react-native-worklets": "0.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -45,6 +45,9 @@
|
|||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint-config-expo": "~10.0.0",
|
"eslint-config-expo": "~10.0.0",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "22.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@0no-co/graphql.web": {
|
"node_modules/@0no-co/graphql.web": {
|
||||||
@@ -1873,9 +1876,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/fingerprint": {
|
"node_modules/@expo/fingerprint": {
|
||||||
"version": "0.15.4",
|
"version": "0.15.5",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.4.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.5.tgz",
|
||||||
"integrity": "sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng==",
|
"integrity": "sha512-mdVoAMcux1WlM6kd1RoWiHRNqKqS+J6mKmWQ/BKgeh937S/fcW58EE68O6nc4KDXtWi3PBeNHskOFcgyIuD4hw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/spawn-async": "^1.7.2",
|
"@expo/spawn-async": "^1.7.2",
|
||||||
@@ -1885,7 +1888,7 @@
|
|||||||
"getenv": "^2.0.0",
|
"getenv": "^2.0.0",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"ignore": "^5.3.1",
|
"ignore": "^5.3.1",
|
||||||
"minimatch": "^9.0.0",
|
"minimatch": "^10.2.2",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"resolve-from": "^5.0.0",
|
"resolve-from": "^5.0.0",
|
||||||
"semver": "^7.6.0"
|
"semver": "^7.6.0"
|
||||||
@@ -1894,34 +1897,46 @@
|
|||||||
"fingerprint": "bin/cli.js"
|
"fingerprint": "bin/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@expo/fingerprint/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@expo/fingerprint/node_modules/brace-expansion": {
|
"node_modules/@expo/fingerprint/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/fingerprint/node_modules/minimatch": {
|
"node_modules/@expo/fingerprint/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "10.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^5.0.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": "18 || 20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/fingerprint/node_modules/semver": {
|
"node_modules/@expo/fingerprint/node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -1961,24 +1976,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/json-file": {
|
"node_modules/@expo/json-file": {
|
||||||
"version": "10.0.8",
|
"version": "10.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.14.tgz",
|
||||||
"integrity": "sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ==",
|
"integrity": "sha512-yWwBFywFv+SxkJp/pIzzA416JVYflNUh7pqQzgaA6nXDqRyK7KfrqVzk8PdUfDnqbBcaZZxpzNssfQZzp5KHrA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "~7.10.4",
|
"@babel/code-frame": "^7.20.0",
|
||||||
"json5": "^2.2.3"
|
"json5": "^2.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/json-file/node_modules/@babel/code-frame": {
|
|
||||||
"version": "7.10.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
|
|
||||||
"integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/highlight": "^7.10.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@expo/metro": {
|
"node_modules/@expo/metro": {
|
||||||
"version": "54.2.0",
|
"version": "54.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz",
|
||||||
@@ -2002,9 +2008,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/metro-config": {
|
"node_modules/@expo/metro-config": {
|
||||||
"version": "54.0.14",
|
"version": "54.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.15.tgz",
|
||||||
"integrity": "sha512-hxpLyDfOR4L23tJ9W1IbJJsG7k4lv2sotohBm/kTYyiG+pe1SYCAWsRmgk+H42o/wWf/HQjE5k45S5TomGLxNA==",
|
"integrity": "sha512-SqIya4VZ9KHM1S9g+xR0A+QKw1Tfs7Gacx6bQNJ98vs4+O7I5+QP5mHZIB0QSZLUV8opiXebHYTiTu+0OAsIUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.20.0",
|
"@babel/code-frame": "^7.20.0",
|
||||||
@@ -2025,7 +2031,7 @@
|
|||||||
"hermes-parser": "^0.29.1",
|
"hermes-parser": "^0.29.1",
|
||||||
"jsc-safe-url": "^0.2.4",
|
"jsc-safe-url": "^0.2.4",
|
||||||
"lightningcss": "^1.30.1",
|
"lightningcss": "^1.30.1",
|
||||||
"minimatch": "^9.0.0",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "~8.4.32",
|
"postcss": "~8.4.32",
|
||||||
"resolve-from": "^5.0.0"
|
"resolve-from": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -2038,28 +2044,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/metro-config/node_modules/brace-expansion": {
|
"node_modules/@expo/metro-config/node_modules/picomatch": {
|
||||||
"version": "2.0.2",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@expo/metro-config/node_modules/minimatch": {
|
|
||||||
"version": "9.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/metro-runtime": {
|
"node_modules/@expo/metro-runtime": {
|
||||||
@@ -2086,25 +2080,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/osascript": {
|
"node_modules/@expo/osascript": {
|
||||||
"version": "2.3.8",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.4.3.tgz",
|
||||||
"integrity": "sha512-/TuOZvSG7Nn0I8c+FcEaoHeBO07yu6vwDgk7rZVvAXoeAK5rkA09jRyjYsZo+0tMEFaToBeywA6pj50Mb3ny9w==",
|
"integrity": "sha512-wbuj3EebM7W9hN/Wp4xTzKd6rQ2zKJzAxkFxkOOwyysLp0HOAgQ4/5RINyoS241pZUX2rUHq7mAJ7pcCQ8U0Ow==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/spawn-async": "^1.7.2",
|
"@expo/spawn-async": "^1.7.2"
|
||||||
"exec-async": "^2.2.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/package-manager": {
|
"node_modules/@expo/package-manager": {
|
||||||
"version": "1.9.10",
|
"version": "1.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.10.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.10.5.tgz",
|
||||||
"integrity": "sha512-axJm+NOj3jVxep49va/+L3KkF3YW/dkV+RwzqUJedZrv4LeTqOG4rhrCaCPXHTvLqCTDKu6j0Xyd28N7mnxsGA==",
|
"integrity": "sha512-nCP9Mebfl3jvOr0/P6VAuyah6PAtun+aihIL2zAtuE8uSe94JWkVZ7051i0MUVO+y3gFpBqnr8IIH5ch+VJjHA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/json-file": "^10.0.8",
|
"@expo/json-file": "^10.0.14",
|
||||||
"@expo/spawn-async": "^1.7.2",
|
"@expo/spawn-async": "^1.7.2",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"npm-package-arg": "^11.0.0",
|
"npm-package-arg": "^11.0.0",
|
||||||
@@ -2204,9 +2197,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@expo/xcpretty": {
|
"node_modules/@expo/xcpretty": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.4.tgz",
|
||||||
"integrity": "sha512-o2qDlTqJ606h4xR36H2zWTywmZ3v3842K6TU8Ik2n1mfW0S580VHlt3eItVYdLYz+klaPp7CXqanja8eASZjRw==",
|
"integrity": "sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.20.0",
|
"@babel/code-frame": "^7.20.0",
|
||||||
@@ -5998,36 +5991,30 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exec-async": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/expo": {
|
"node_modules/expo": {
|
||||||
"version": "54.0.33",
|
"version": "54.0.34",
|
||||||
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.34.tgz",
|
||||||
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
|
"integrity": "sha512-XkVHguZZDC8BcTQxHAd14/TQFbDp1Wt0Z/KApO9t68Ll5A127hLCPzU+a9gytfCIiyL/V1IpF1vIcOLKEVAoNQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@expo/cli": "54.0.23",
|
"@expo/cli": "54.0.24",
|
||||||
"@expo/config": "~12.0.13",
|
"@expo/config": "~12.0.13",
|
||||||
"@expo/config-plugins": "~54.0.4",
|
"@expo/config-plugins": "~54.0.4",
|
||||||
"@expo/devtools": "0.1.8",
|
"@expo/devtools": "0.1.8",
|
||||||
"@expo/fingerprint": "0.15.4",
|
"@expo/fingerprint": "0.15.5",
|
||||||
"@expo/metro": "~54.2.0",
|
"@expo/metro": "~54.2.0",
|
||||||
"@expo/metro-config": "54.0.14",
|
"@expo/metro-config": "54.0.15",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@ungap/structured-clone": "^1.3.0",
|
"@ungap/structured-clone": "^1.3.0",
|
||||||
"babel-preset-expo": "~54.0.10",
|
"babel-preset-expo": "~54.0.10",
|
||||||
"expo-asset": "~12.0.12",
|
"expo-asset": "~12.0.13",
|
||||||
"expo-constants": "~18.0.13",
|
"expo-constants": "~18.0.13",
|
||||||
"expo-file-system": "~19.0.21",
|
"expo-file-system": "~19.0.22",
|
||||||
"expo-font": "~14.0.11",
|
"expo-font": "~14.0.11",
|
||||||
"expo-keep-awake": "~15.0.8",
|
"expo-keep-awake": "~15.0.8",
|
||||||
"expo-modules-autolinking": "3.0.24",
|
"expo-modules-autolinking": "3.0.25",
|
||||||
"expo-modules-core": "3.0.29",
|
"expo-modules-core": "3.0.30",
|
||||||
"pretty-format": "^29.7.0",
|
"pretty-format": "^29.7.0",
|
||||||
"react-refresh": "^0.14.2",
|
"react-refresh": "^0.14.2",
|
||||||
"whatwg-url-without-unicode": "8.0.0-3"
|
"whatwg-url-without-unicode": "8.0.0-3"
|
||||||
@@ -6057,13 +6044,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-asset": {
|
"node_modules/expo-asset": {
|
||||||
"version": "12.0.12",
|
"version": "12.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.13.tgz",
|
||||||
"integrity": "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==",
|
"integrity": "sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/image-utils": "^0.8.8",
|
"@expo/image-utils": "^0.8.8",
|
||||||
"expo-constants": "~18.0.12"
|
"expo-constants": "~18.0.13"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*",
|
"expo": "*",
|
||||||
@@ -6115,9 +6102,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-file-system": {
|
"node_modules/expo-file-system": {
|
||||||
"version": "19.0.21",
|
"version": "19.0.22",
|
||||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.22.tgz",
|
||||||
"integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==",
|
"integrity": "sha512-l9pgahSc7sJD0bP9vBNeXvZjy8QKDpVHVxWmei/ESQOrzmoj5BidziqLVsyZdxsi+PfdbTtttLTAmddH/JafYA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*",
|
"expo": "*",
|
||||||
@@ -6174,9 +6161,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-image-picker": {
|
"node_modules/expo-image-picker": {
|
||||||
"version": "17.0.10",
|
"version": "17.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.11.tgz",
|
||||||
"integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==",
|
"integrity": "sha512-/apkoyukDvsCHHb9fzP+F34A1uQqSzUtYH/2P/xJACNEwq+mwEXjXvVU8bzlJq6ih0Qo1+tpVivIa7B9kYSwOQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-image-loader": "~6.0.0"
|
"expo-image-loader": "~6.0.0"
|
||||||
@@ -6196,12 +6183,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-linking": {
|
"node_modules/expo-linking": {
|
||||||
"version": "8.0.11",
|
"version": "8.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz",
|
||||||
"integrity": "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==",
|
"integrity": "sha512-FpXeIpFgZuxihwT9lBo86YD3y6LphBuAhN680MMxm/Y7fmsc57vimn2d3vFu68VI0+Z9w457t494mu2wvlgWTQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-constants": "~18.0.12",
|
"expo-constants": "~18.0.13",
|
||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -6210,9 +6197,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-modules-autolinking": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "3.0.24",
|
"version": "3.0.25",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz",
|
||||||
"integrity": "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==",
|
"integrity": "sha512-YmHWctJlwvOuLZccg3cOXvSiXVJrPMKl7g2YR0YHWoGL9v2RvcmgaPJWPSLVW+voNEgEPsbo5UmUrAqbnYcBeg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/spawn-async": "^1.7.2",
|
"@expo/spawn-async": "^1.7.2",
|
||||||
@@ -6226,9 +6213,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-modules-core": {
|
"node_modules/expo-modules-core": {
|
||||||
"version": "3.0.29",
|
"version": "3.0.30",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.30.tgz",
|
||||||
"integrity": "sha512-LzipcjGqk8gvkrOUf7O2mejNWugPkf3lmd9GkqL9WuNyeN2fRwU0Dn77e3ZUKI3k6sI+DNwjkq4Nu9fNN9WS7Q==",
|
"integrity": "sha512-a6IrpAn/Jbmwxi9L+hMmXKpNqnkUpoF7WHOpn02rVLyax2J0gB1vvCVE5rNydplEnt41Q6WxQwvcOjZaIkcSUg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
@@ -6501,9 +6488,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-server": {
|
"node_modules/expo-server": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.6.tgz",
|
||||||
"integrity": "sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA==",
|
"integrity": "sha512-vb5TBtskvEdzYuW79lATXutOEBfW5m6U4EFpNjCVZTnI7S//SAsLQkYEpn+EDfn84m6VQfzSGkIVR6YPaScKFA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.16.0"
|
"node": ">=20.16.0"
|
||||||
@@ -6568,9 +6555,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-web-browser": {
|
"node_modules/expo-web-browser": {
|
||||||
"version": "15.0.10",
|
"version": "15.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.11.tgz",
|
||||||
"integrity": "sha512-fvDhW4bhmXAeWFNFiInmsGCK83PAqAcQaFyp/3pE/jbdKmFKoRCWr46uZGIfN4msLK/OODhaQ/+US7GSJNDHJg==",
|
"integrity": "sha512-r2LS4Ro6DgUPZkcaEfgt8mp9eJuoA93x11Jh7S6utFe0FEzvUNn2yFhxg8XVwESaaHGt2k5V8LuK36rsp0BeIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*",
|
"expo": "*",
|
||||||
@@ -6578,9 +6565,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/@expo/cli": {
|
"node_modules/expo/node_modules/@expo/cli": {
|
||||||
"version": "54.0.23",
|
"version": "54.0.24",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.24.tgz",
|
||||||
"integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==",
|
"integrity": "sha512-5xse1bEgnVUBhOrtttc6xTNJVvjyTRavpzuF0/0nuj+312vfSbk7EiRbG+xJ2pW/iZxnhLPJkFCrPYG0nmheAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@0no-co/graphql.web": "^1.0.8",
|
"@0no-co/graphql.web": "^1.0.8",
|
||||||
@@ -6592,7 +6579,7 @@
|
|||||||
"@expo/image-utils": "^0.8.8",
|
"@expo/image-utils": "^0.8.8",
|
||||||
"@expo/json-file": "^10.0.8",
|
"@expo/json-file": "^10.0.8",
|
||||||
"@expo/metro": "~54.2.0",
|
"@expo/metro": "~54.2.0",
|
||||||
"@expo/metro-config": "~54.0.14",
|
"@expo/metro-config": "~54.0.15",
|
||||||
"@expo/osascript": "^2.3.8",
|
"@expo/osascript": "^2.3.8",
|
||||||
"@expo/package-manager": "^1.9.10",
|
"@expo/package-manager": "^1.9.10",
|
||||||
"@expo/plist": "^0.4.8",
|
"@expo/plist": "^0.4.8",
|
||||||
@@ -6615,16 +6602,16 @@
|
|||||||
"connect": "^3.7.0",
|
"connect": "^3.7.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"env-editor": "^0.4.1",
|
"env-editor": "^0.4.1",
|
||||||
"expo-server": "^1.0.5",
|
"expo-server": "^1.0.6",
|
||||||
"freeport-async": "^2.0.0",
|
"freeport-async": "^2.0.0",
|
||||||
"getenv": "^2.0.0",
|
"getenv": "^2.0.0",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"lan-network": "^0.1.6",
|
"lan-network": "^0.2.1",
|
||||||
"minimatch": "^9.0.0",
|
"minimatch": "^9.0.0",
|
||||||
"node-forge": "^1.3.3",
|
"node-forge": "^1.3.3",
|
||||||
"npm-package-arg": "^11.0.0",
|
"npm-package-arg": "^11.0.0",
|
||||||
"ora": "^3.4.0",
|
"ora": "^3.4.0",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^4.0.3",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
"pretty-format": "^29.7.0",
|
"pretty-format": "^29.7.0",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
@@ -6665,9 +6652,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/brace-expansion": {
|
"node_modules/expo/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
@@ -6689,12 +6676,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/minimatch": {
|
"node_modules/expo/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
@@ -6704,21 +6691,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/picomatch": {
|
"node_modules/expo/node_modules/picomatch": {
|
||||||
"version": "3.0.1",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/semver": {
|
"node_modules/expo/node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -6728,9 +6715,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/ws": {
|
"node_modules/expo/node_modules/ws": {
|
||||||
"version": "8.19.0",
|
"version": "8.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
@@ -8394,9 +8381,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lan-network": {
|
"node_modules/lan-network": {
|
||||||
"version": "0.1.7",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.2.1.tgz",
|
||||||
"integrity": "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==",
|
"integrity": "sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lan-network": "dist/lan-network-cli.js"
|
"lan-network": "dist/lan-network-cli.js"
|
||||||
@@ -8451,9 +8438,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
"integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
@@ -8466,23 +8453,23 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-android-arm64": "1.31.1",
|
"lightningcss-android-arm64": "1.32.0",
|
||||||
"lightningcss-darwin-arm64": "1.31.1",
|
"lightningcss-darwin-arm64": "1.32.0",
|
||||||
"lightningcss-darwin-x64": "1.31.1",
|
"lightningcss-darwin-x64": "1.32.0",
|
||||||
"lightningcss-freebsd-x64": "1.31.1",
|
"lightningcss-freebsd-x64": "1.32.0",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.31.1",
|
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||||
"lightningcss-linux-arm64-gnu": "1.31.1",
|
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||||
"lightningcss-linux-arm64-musl": "1.31.1",
|
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||||
"lightningcss-linux-x64-gnu": "1.31.1",
|
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||||
"lightningcss-linux-x64-musl": "1.31.1",
|
"lightningcss-linux-x64-musl": "1.32.0",
|
||||||
"lightningcss-win32-arm64-msvc": "1.31.1",
|
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||||
"lightningcss-win32-x64-msvc": "1.31.1"
|
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-android-arm64": {
|
"node_modules/lightningcss-android-arm64": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||||
"integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
|
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8500,9 +8487,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||||
"integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
|
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8520,9 +8507,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||||
"integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
|
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8540,9 +8527,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||||
"integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
|
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8560,9 +8547,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||||
"integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
|
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -8580,9 +8567,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||||
"integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
|
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8600,9 +8587,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||||
"integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
|
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8620,9 +8607,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||||
"integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
|
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8640,9 +8627,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||||
"integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
|
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8660,9 +8647,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||||
"integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
|
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8680,9 +8667,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.31.1",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||||
"integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
|
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -9402,9 +9389,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-forge": {
|
"node_modules/node-forge": {
|
||||||
"version": "1.3.3",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
|
||||||
"integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
|
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
|
||||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6.13.0"
|
"node": ">= 6.13.0"
|
||||||
@@ -9447,9 +9434,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/npm-package-arg/node_modules/semver": {
|
"node_modules/npm-package-arg/node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -10453,9 +10440,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react-native-webview": {
|
"node_modules/react-native-webview": {
|
||||||
"version": "13.16.0",
|
"version": "13.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
|
||||||
"integrity": "sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA==",
|
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
@@ -11722,9 +11709,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.9",
|
"version": "7.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz",
|
||||||
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
|
"integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
@@ -12155,9 +12142,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.23.0",
|
"version": "6.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz",
|
||||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
"integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
@@ -12772,9 +12759,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wonka": {
|
"node_modules/wonka": {
|
||||||
"version": "6.3.5",
|
"version": "6.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.6.tgz",
|
||||||
"integrity": "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==",
|
"integrity": "sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "mobile",
|
"name": "mobile",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "22.x"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start --dev-client --host lan",
|
"start": "expo start --dev-client --host lan",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
@@ -10,30 +13,34 @@
|
|||||||
"ios:device": "LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 expo run:ios --device",
|
"ios:device": "LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 expo run:ios --device",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
|
"doctor": "npx expo-doctor",
|
||||||
|
"preflight:testflight": "npm run lint && npm run doctor",
|
||||||
"build:ios:dev": "eas build --profile development --platform ios",
|
"build:ios:dev": "eas build --profile development --platform ios",
|
||||||
"build:ios:preview": "eas build --profile preview --platform ios"
|
"build:ios:preview": "eas build --profile preview --platform ios",
|
||||||
|
"build:ios:testflight": "eas build --profile testflight --platform ios",
|
||||||
|
"submit:ios:testflight": "eas submit --profile testflight --platform ios --latest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"expo": "~54.0.33",
|
"expo": "~54.0.34",
|
||||||
"expo-camera": "~17.0.10",
|
"expo-camera": "~17.0.10",
|
||||||
"expo-constants": "~18.0.13",
|
"expo-constants": "~18.0.13",
|
||||||
"expo-document-picker": "^14.0.8",
|
"expo-document-picker": "^14.0.8",
|
||||||
"expo-font": "~14.0.11",
|
"expo-font": "~14.0.11",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
"expo-image-picker": "~17.0.8",
|
"expo-image-picker": "~17.0.11",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.12",
|
||||||
"expo-router": "~6.0.23",
|
"expo-router": "~6.0.23",
|
||||||
"expo-secure-store": "^15.0.8",
|
"expo-secure-store": "^15.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-symbols": "~1.0.8",
|
"expo-symbols": "~1.0.8",
|
||||||
"expo-system-ui": "~6.0.9",
|
"expo-system-ui": "~6.0.9",
|
||||||
"expo-web-browser": "~15.0.10",
|
"expo-web-browser": "~15.0.11",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
@@ -43,7 +50,7 @@
|
|||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-webview": "^13.16.0",
|
"react-native-webview": "13.15.0",
|
||||||
"react-native-worklets": "0.5.1"
|
"react-native-worklets": "0.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
BIN
mobile/screenshots/vertrieb/01-dashboard.png
Normal file
BIN
mobile/screenshots/vertrieb/01-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
BIN
mobile/screenshots/vertrieb/02-dashboard-content.png
Normal file
BIN
mobile/screenshots/vertrieb/02-dashboard-content.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
BIN
mobile/screenshots/vertrieb/03-dashboard-kompakt.png
Normal file
BIN
mobile/screenshots/vertrieb/03-dashboard-kompakt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -183,6 +183,100 @@ type RequestOptions = {
|
|||||||
body?: unknown;
|
body?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MatrixStatus = {
|
||||||
|
enabled?: boolean;
|
||||||
|
ready?: boolean;
|
||||||
|
configured?: boolean;
|
||||||
|
homeserverUrl?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixIdentity = {
|
||||||
|
matrixUserId: string;
|
||||||
|
displayName?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixRoom = {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
topic?: string | null;
|
||||||
|
type?: 'room' | 'project' | 'direct' | string;
|
||||||
|
group?: string;
|
||||||
|
roomId?: string | null;
|
||||||
|
alias?: string | null;
|
||||||
|
exists?: boolean;
|
||||||
|
projectId?: number;
|
||||||
|
projectNumber?: string | null;
|
||||||
|
userId?: string;
|
||||||
|
email?: string | null;
|
||||||
|
entityType?: string | null;
|
||||||
|
entityId?: number | null;
|
||||||
|
entityUuid?: string | null;
|
||||||
|
unread?: number;
|
||||||
|
mentions?: number;
|
||||||
|
provisionEndpoint?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixAttachment = {
|
||||||
|
fileName?: string | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
size?: number | null;
|
||||||
|
mxcUri?: string | null;
|
||||||
|
previewUrl?: string | null;
|
||||||
|
downloadUrl?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixReaction = {
|
||||||
|
key: string;
|
||||||
|
count?: number;
|
||||||
|
own?: boolean;
|
||||||
|
senders?: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixMessage = {
|
||||||
|
id: string;
|
||||||
|
sender: string;
|
||||||
|
senderDisplayName?: string | null;
|
||||||
|
body?: string | null;
|
||||||
|
timestamp?: string | number | null;
|
||||||
|
own?: boolean;
|
||||||
|
edited?: boolean;
|
||||||
|
redacted?: boolean;
|
||||||
|
msgtype?: string;
|
||||||
|
attachment?: MatrixAttachment | null;
|
||||||
|
replyToEventId?: string | null;
|
||||||
|
reactions?: MatrixReaction[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixMember = {
|
||||||
|
matrixUserId: string;
|
||||||
|
displayName?: string | null;
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
membership?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixUser = {
|
||||||
|
userId: string;
|
||||||
|
matrixUserId: string;
|
||||||
|
displayName?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixSyncResponse = {
|
||||||
|
nextBatch?: string;
|
||||||
|
messages?: MatrixMessage[];
|
||||||
|
replacements?: MatrixMessage[];
|
||||||
|
reactions?: (MatrixReaction & { targetEventId?: string })[];
|
||||||
|
redactions?: { redacts?: string; eventId?: string; targetEventId?: string }[];
|
||||||
|
members?: MatrixMember[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
function buildUrl(path: string): string {
|
function buildUrl(path: string): string {
|
||||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||||
return path;
|
return path;
|
||||||
@@ -227,10 +321,231 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
|||||||
return payload as T;
|
return payload as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function apiFormRequest<T>(path: string, token: string, formData: FormData): Promise<T> {
|
||||||
|
const response = await fetch(buildUrl(path), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = await parseJson(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message =
|
||||||
|
(payload as { message?: string; error?: string } | null)?.message ||
|
||||||
|
(payload as { message?: string; error?: string } | null)?.error ||
|
||||||
|
`Request failed (${response.status}) for ${path}`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matrixRoomPath(roomKey: string, suffix = ''): string {
|
||||||
|
return `/api/communication/matrix/rooms/${encodeURIComponent(roomKey)}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkBackendHealth(): Promise<{ status: string; [key: string]: unknown }> {
|
export async function checkBackendHealth(): Promise<{ status: string; [key: string]: unknown }> {
|
||||||
return apiRequest<{ status: string; [key: string]: unknown }>('/health');
|
return apiRequest<{ status: string; [key: string]: unknown }>('/health');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchMatrixStatus(token: string): Promise<MatrixStatus> {
|
||||||
|
return apiRequest<MatrixStatus>('/api/communication/matrix/status', { token });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMatrixIdentity(token: string): Promise<MatrixIdentity> {
|
||||||
|
return apiRequest<MatrixIdentity>('/api/communication/matrix/me', { token });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function provisionMatrixUser(token: string): Promise<MatrixIdentity> {
|
||||||
|
return apiRequest<MatrixIdentity>('/api/communication/matrix/me/provision', {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMatrixRooms(token: string): Promise<MatrixRoom[]> {
|
||||||
|
const [rooms, projectRooms, directRooms, unread] = await Promise.all([
|
||||||
|
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/rooms', { token }),
|
||||||
|
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/project-rooms', { token }),
|
||||||
|
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/direct-rooms', { token }),
|
||||||
|
apiRequest<{ rooms?: Record<string, { count?: number; mentions?: number }> }>('/api/communication/matrix/unread', {
|
||||||
|
token,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const unreadByRoom = unread.rooms || {};
|
||||||
|
const decorate = (room: MatrixRoom, group: string): MatrixRoom => ({
|
||||||
|
...room,
|
||||||
|
group,
|
||||||
|
unread: unreadByRoom[room.key]?.count || 0,
|
||||||
|
mentions: unreadByRoom[room.key]?.mentions || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(rooms.rooms || []).map((room) => decorate(room, 'Räume')),
|
||||||
|
...(projectRooms.rooms || []).map((room) => decorate(room, 'Projekte')),
|
||||||
|
...(directRooms.rooms || []).map((room) => decorate(room, 'Direkt')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMatrixUsers(token: string): Promise<MatrixUser[]> {
|
||||||
|
const response = await apiRequest<{ users?: MatrixUser[] }>('/api/communication/matrix/users', { token });
|
||||||
|
return response.users || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMatrixRoom(
|
||||||
|
token: string,
|
||||||
|
payload: { key: string; name: string; topic?: string | null; type?: string }
|
||||||
|
): Promise<MatrixRoom> {
|
||||||
|
return apiRequest<MatrixRoom>('/api/communication/matrix/rooms', {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function provisionMatrixRoom(token: string, room: MatrixRoom): Promise<MatrixRoom> {
|
||||||
|
if (room.provisionEndpoint) {
|
||||||
|
return apiRequest<MatrixRoom>(room.provisionEndpoint, { method: 'POST', token });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room.type === 'project' && room.projectId) {
|
||||||
|
return apiRequest<MatrixRoom>(`/api/communication/matrix/project-rooms/${room.projectId}/provision`, {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room.type === 'direct' && room.userId) {
|
||||||
|
return apiRequest<MatrixRoom>(`/api/communication/matrix/direct-rooms/${encodeURIComponent(room.userId)}/provision`, {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiRequest<MatrixRoom>(matrixRoomPath(room.key, '/provision'), {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
topic: room.topic,
|
||||||
|
type: room.type || 'room',
|
||||||
|
entityType: room.entityType,
|
||||||
|
entityId: room.entityId,
|
||||||
|
entityUuid: room.entityUuid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMatrixMessages(token: string, roomKey: string): Promise<MatrixMessage[]> {
|
||||||
|
const response = await apiRequest<{ messages?: MatrixMessage[] }>(matrixRoomPath(roomKey, '/messages'), { token });
|
||||||
|
return response.messages || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncMatrixRoom(
|
||||||
|
token: string,
|
||||||
|
roomKey: string,
|
||||||
|
since?: string,
|
||||||
|
initial = false
|
||||||
|
): Promise<MatrixSyncResponse> {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (since) query.set('since', since);
|
||||||
|
if (initial) query.set('initial', '1');
|
||||||
|
const suffix = query.toString() ? `/sync?${query.toString()}` : '/sync';
|
||||||
|
return apiRequest<MatrixSyncResponse>(matrixRoomPath(roomKey, suffix), { token });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMatrixMembers(token: string, roomKey: string): Promise<MatrixMember[]> {
|
||||||
|
const response = await apiRequest<{ members?: MatrixMember[] }>(matrixRoomPath(roomKey, '/members'), { token });
|
||||||
|
return response.members || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMatrixMessage(
|
||||||
|
token: string,
|
||||||
|
roomKey: string,
|
||||||
|
text: string,
|
||||||
|
replyToEventId?: string | null
|
||||||
|
): Promise<MatrixMessage> {
|
||||||
|
return apiRequest<MatrixMessage>(matrixRoomPath(roomKey, '/messages'), {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: { text, replyToEventId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editMatrixMessage(token: string, roomKey: string, eventId: string, text: string): Promise<MatrixMessage> {
|
||||||
|
return apiRequest<MatrixMessage>(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}`), {
|
||||||
|
method: 'PUT',
|
||||||
|
token,
|
||||||
|
body: { text },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMatrixMessage(token: string, roomKey: string, eventId: string): Promise<void> {
|
||||||
|
await apiRequest(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}`), {
|
||||||
|
method: 'DELETE',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reactToMatrixMessage(token: string, roomKey: string, eventId: string, key: string): Promise<void> {
|
||||||
|
await apiRequest(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}/reactions`), {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: { key },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markMatrixRoomRead(token: string, roomKey: string, eventId?: string): Promise<void> {
|
||||||
|
await apiRequest(matrixRoomPath(roomKey, '/read'), {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: { eventId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncMatrixMembers(token: string, roomKey: string): Promise<void> {
|
||||||
|
await apiRequest(matrixRoomPath(roomKey, '/members/sync'), {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function inviteMatrixMember(token: string, roomKey: string, userId: string): Promise<void> {
|
||||||
|
await apiRequest(matrixRoomPath(roomKey, '/members/invite'), {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: { userId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeMatrixMember(token: string, roomKey: string, matrixUserId: string): Promise<void> {
|
||||||
|
await apiRequest(matrixRoomPath(roomKey, `/members/${encodeURIComponent(matrixUserId)}`), {
|
||||||
|
method: 'DELETE',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadMatrixAttachment(
|
||||||
|
token: string,
|
||||||
|
roomKey: string,
|
||||||
|
file: { uri: string; name: string; mimeType?: string | null }
|
||||||
|
): Promise<MatrixMessage> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', {
|
||||||
|
uri: file.uri,
|
||||||
|
name: file.name,
|
||||||
|
type: file.mimeType || 'application/octet-stream',
|
||||||
|
} as unknown as Blob);
|
||||||
|
|
||||||
|
return apiFormRequest<MatrixMessage>(matrixRoomPath(roomKey, '/attachments'), token, formData);
|
||||||
|
}
|
||||||
|
|
||||||
export async function renderPrintLabel(
|
export async function renderPrintLabel(
|
||||||
token: string,
|
token: string,
|
||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
@@ -508,7 +823,7 @@ export async function createCustomerInventoryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPlants(token: string, includeArchived = false): Promise<Plant[]> {
|
export async function fetchPlants(token: string, includeArchived = false): Promise<Plant[]> {
|
||||||
const plants = await apiRequest<Array<Plant & { description?: unknown }>>('/api/resource/plants', { token });
|
const plants = await apiRequest<(Plant & { description?: unknown })[]>('/api/resource/plants', { token });
|
||||||
const normalized = (plants || []).map((plant) => {
|
const normalized = (plants || []).map((plant) => {
|
||||||
const legacyDescription = typeof plant.description === 'string'
|
const legacyDescription = typeof plant.description === 'string'
|
||||||
? plant.description
|
? plant.description
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ FEDEO Selfhost Setup
|
|||||||
Dieses Script führt dich durch die lokale Betriebsstruktur:
|
Dieses Script führt dich durch die lokale Betriebsstruktur:
|
||||||
|
|
||||||
$ROOT_DIR/
|
$ROOT_DIR/
|
||||||
docker-compose.selfhost.yml Docker Stack für FEDEO, Traefik, PostgreSQL, MinIO und Matrix
|
docker-compose.selfhost.yml Docker Stack für FEDEO, Traefik, PostgreSQL, MinIO, Matrix und Monitoring
|
||||||
.env Zielkonfiguration, wird von diesem Script geschrieben
|
.env Zielkonfiguration, wird von diesem Script geschrieben
|
||||||
postgres/ persistente FEDEO-Datenbank
|
postgres/ persistente FEDEO-Datenbank
|
||||||
minio/ lokaler S3-kompatibler Dateispeicher
|
minio/ lokaler S3-kompatibler Dateispeicher
|
||||||
@@ -320,6 +320,7 @@ DOKUBOX_IMAP_PASSWORD=$(env_quote "$dokubox_password")
|
|||||||
OPENAI_API_KEY=$(env_quote "$openai_key")
|
OPENAI_API_KEY=$(env_quote "$openai_key")
|
||||||
STIRLING_API_KEY=$(env_quote "$stirling_key")
|
STIRLING_API_KEY=$(env_quote "$stirling_key")
|
||||||
NUXT_PUBLIC_PDF_LICENSE=$(env_quote "$pdf_license")
|
NUXT_PUBLIC_PDF_LICENSE=$(env_quote "$pdf_license")
|
||||||
|
NODE_EXPORTER_URL=$(env_quote "http://node-exporter:9100")
|
||||||
|
|
||||||
FEDEO_BOOTSTRAP_ADMIN_EMAIL=$(env_quote "$admin_email")
|
FEDEO_BOOTSTRAP_ADMIN_EMAIL=$(env_quote "$admin_email")
|
||||||
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=$(env_quote "$admin_password")
|
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=$(env_quote "$admin_password")
|
||||||
@@ -484,7 +485,11 @@ main() {
|
|||||||
else
|
else
|
||||||
echo
|
echo
|
||||||
echo "Start später mit:"
|
echo "Start später mit:"
|
||||||
echo " docker compose -f docker-compose.selfhost.yml up -d"
|
if [[ "${FEDEO_USE_SUDO_DOCKER:-false}" == "true" ]]; then
|
||||||
|
echo " sudo docker compose -f $COMPOSE_FILE up -d"
|
||||||
|
else
|
||||||
|
echo " docker compose -f $COMPOSE_FILE up -d"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ footer {
|
|||||||
|
|
||||||
.workflow-section,
|
.workflow-section,
|
||||||
.open-source-section,
|
.open-source-section,
|
||||||
|
.script-section,
|
||||||
.contact-section {
|
.contact-section {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -423,6 +424,86 @@ footer {
|
|||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.script-section {
|
||||||
|
border-top: 1px solid rgba(23, 33, 31, 0.12);
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 920px;
|
||||||
|
padding-bottom: 5rem;
|
||||||
|
padding-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-section p {
|
||||||
|
color: #51605c;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-section > div:first-child {
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-window {
|
||||||
|
background: #17211f;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 1.5rem 4rem rgba(23, 33, 31, 0.22);
|
||||||
|
color: #dff8d8;
|
||||||
|
margin-top: 2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-window-toolbar {
|
||||||
|
align-items: center;
|
||||||
|
background: #24302d;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-window-toolbar span {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
height: 0.68rem;
|
||||||
|
width: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-window-toolbar span:nth-child(2) {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-window-toolbar strong {
|
||||||
|
color: rgba(255, 255, 255, 0.68);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-window pre {
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: clamp(1.25rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-window code {
|
||||||
|
color: #dff8d8;
|
||||||
|
display: block;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: clamp(0.92rem, 1.7vw, 1rem);
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-window code::before {
|
||||||
|
color: var(--accent);
|
||||||
|
content: "$ ";
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.contact-section {
|
.contact-section {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid rgba(23, 33, 31, 0.1);
|
border: 1px solid rgba(23, 33, 31, 0.1);
|
||||||
@@ -522,6 +603,7 @@ footer p {
|
|||||||
.hero-section,
|
.hero-section,
|
||||||
.workflow-section,
|
.workflow-section,
|
||||||
.open-source-section,
|
.open-source-section,
|
||||||
|
.script-section,
|
||||||
.contact-section {
|
.contact-section {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<nav aria-label="Hauptnavigation">
|
<nav aria-label="Hauptnavigation">
|
||||||
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
|
||||||
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
<NuxtLink to="/#open-source">Open Source</NuxtLink>
|
||||||
|
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
|
||||||
<NuxtLink to="/#kontakt">Kontakt</NuxtLink>
|
<NuxtLink to="/#kontakt">Kontakt</NuxtLink>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<nav aria-label="Hauptnavigation">
|
<nav aria-label="Hauptnavigation">
|
||||||
<a href="#funktionen">Funktionen</a>
|
<a href="#funktionen">Funktionen</a>
|
||||||
<a href="#open-source">Open Source</a>
|
<a href="#open-source">Open Source</a>
|
||||||
|
<a href="#selfhost">Selfhost</a>
|
||||||
<a href="#kontakt">Kontakt</a>
|
<a href="#kontakt">Kontakt</a>
|
||||||
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -130,6 +131,26 @@
|
|||||||
<a class="secondary-action" href="https://git.federspiel.tech/flfeders/FEDEO">Repository ansehen</a>
|
<a class="secondary-action" href="https://git.federspiel.tech/flfeders/FEDEO">Repository ansehen</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="selfhost" class="script-section">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Selfhosting</p>
|
||||||
|
<h2>FEDEO auf deinem Server starten.</h2>
|
||||||
|
<p>
|
||||||
|
Der Installer prüft die Basisumgebung, richtet Docker bei Bedarf ein, lädt FEDEO und startet danach den geführten Selfhost-Assistenten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-window" aria-label="FEDEO Selfhost Installationsbefehl">
|
||||||
|
<div class="code-window-toolbar">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<strong>Terminal</strong>
|
||||||
|
</div>
|
||||||
|
<pre><code>{{ installCommand }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="kontakt" class="contact-section">
|
<section id="kontakt" class="contact-section">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Jetzt anfragen</p>
|
<p class="eyebrow">Jetzt anfragen</p>
|
||||||
@@ -196,4 +217,6 @@ const features = [
|
|||||||
description: 'FEDEO ist für Arbeit am Schreibtisch und für mobile Abläufe im Einsatz vorbereitet.'
|
description: 'FEDEO ist für Arbeit am Schreibtisch und für mobile Abläufe im Einsatz vorbereitet.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const installCommand = 'curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/scripts/selfhost-install.sh | bash -s -- --simple --start'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user