Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pre-releases for test devices only.

## 0.2.0-beta.17

- **Header stays on one line.** The status chips (messages, update, VPN, apps …) use the free room left of *ONLINE*; the ones that do not fit go behind a small **+N** button (amber when a warning is among them), which opens them as a list under the row – the header keeps its height and the clock stays on the right. The *MELDUNG* chip no longer counts the “update available” message while the *UPDATE* chip is shown (it counted the same thing twice; the message stays under System → Meldungen and still goes out by mail/push).
- **Interface:** *Mein ShareOS* has its own tab **APPS** with all apps of the user (star = favourite); the start page keeps only the favourites at the top (*FAV APPS*). System → Allgemein: device name and access (root password) moved down, between *App & Widgets* and *Über* (section *GERÄT & ZUGANG*).
- **Scheduled jobs did not run at all** ([#82](https://github.com/UniqueDroid/ShareOS/issues/82)). BusyBox `crond` reads `<dir>/crontabs`, and buildroot's busybox config sets the dir to `/etc/cron` – while `shareos-cron` writes root's crontab to `/etc/crontabs/root`. So `crond` saw an empty `/etc/cron/crontabs`, and none of the ShareOS jobs ran: the scheduled switch-off (reported: the box switched on by schedule but not off), the hourly wake-up alarm, the message checks every 10 minutes, automatic updates, scheduled SMART tests, cloud jobs, Paperless inboxes, app tasks, the Zabbix collector. Running a job by hand always worked, which hid it. `CONFIG_FEATURE_CROND_DIR="/etc"` now, and `post-build.sh` stops the build if crond would read another directory. On a box before beta.17: `rm -rf /etc/cron/crontabs; ln -s /etc/crontabs /etc/cron/crontabs; /etc/init.d/S50crond restart`.
- **Take over disks with data instead of erasing them.** Speicher → Laufwerke shows **„<partition> übernehmen“** at every partition (or RAID array) with a file system ShareOS does not know yet – the data disks of Alt-F or the D-Link firmware. It becomes a volume as it is: nothing is erased, ext4 gets the quick check and the quota feature as every volume, the first Linux volume becomes the apps area, RAID 1/0/linear arrays get their layout and go into `mdadm.conf`. Before, only the setup wizard was offered, which erases the disks (asked by a tester right after switching from Alt-F). `shareos-storage adopt <name> <partition>`, rpcd `shareos.storage adopt`; `tests/test-storage.sh` (+4 checks). SWITCHING/UMSTIEG explain it, with the SSH commands for older versions.
- **Hardening after an external code review** (static analysis by another AI, checked against the code; the suggested patch was not taken over as it was, because it only moved one overflow into the next buffer). `dns320l-daemon`: the arguments of a `-x` command were assembled into 64 bytes with `sprintf` and copied into 512-byte messages with `strcpy` – now at most 256 bytes, `snprintf`, refused when too long (a heap overflow before, shown with AddressSanitizer); the command name is checked against its 64 bytes; the answer of the daemon is read with room for the terminating zero (one byte past the buffer before); `write_str_to_file` writes the string as it is instead of through a 64-byte copy, and completely. `dns323-fw`: complete writes (`write_all`), the disabled size check is documented (damaged files fail the checksums). `shareos-flash-bin`: one firmware write at a time (lock with the pid; a stale one is taken over). All of this was only reachable locally as root (the daemon listens on a root-only Unix socket) – nothing changes in how fan, LEDs and MCU are driven. `tests/test-flash-bin.sh` +2 checks.
Expand Down
102 changes: 75 additions & 27 deletions src/shareos-core/www/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -594,29 +594,66 @@ function renderHeader(offline = false) {
const i = S.info || {}, r = i.release || {}, ip = i.network?.eth0?.ipv4?.address;
const now = new Date();
const upd = S.check?.update_available ? S.check.latest : null;
// unread messages; the update one is left out while the UPDATE chip shows it anyway
const nu = (S.notes?.items || []).filter((x) => !x.read && !x.resolved && !(upd && x.source === 'update'));
hd.innerHTML = html`
<div>
${brand()}
<div class="sub"><b>${i.hostname || '…'}</b>${[(i.model || '').replace(/ NAS$/, ''), 'ShareOS ' + (r.version || '…'), ip].filter(Boolean).map((x) => ' · ' + x).join('')}</div>
</div>
<div class="side">
<div class="row">
${S.notes?.unread ? html`<a class="chip ${S.notes.items.some((x) => !x.read && !x.resolved && x.level === 'error') ? 'err' : 'warn'}" href="#/system/notifications"><i></i>${S.notes.unread === 1 ? '1 MELDUNG' : S.notes.unread + ' MELDUNGEN'}</a>` : ''}
${nu.length ? html`<a class="chip ${nu.some((x) => x.level === 'error') ? 'err' : 'warn'}" href="#/system/notifications"><i></i>${nu.length === 1 ? '1 MELDUNG' : nu.length + ' MELDUNGEN'}</a>` : ''}
${upd ? html`<a class="chip warn" href="#/system/firmware"><i></i>UPDATE ${upd.replace(/^v/, '')} <small>verfügbar</small></a>` : ''}
${chipsHtml()}
${offline ? html`<span class="chip warn"><i></i>OFFLINE</span>`
: html`<span class="chip"><i></i>ONLINE <small>seit ${fmtUptime(i.uptime)}</small></span>`}
<button class="chip more" type="button" data-act="chips-more" hidden></button>
${offline ? html`<span class="chip warn keep"><i></i>OFFLINE</span>`
: html`<span class="chip keep"><i></i>ONLINE <small>seit ${fmtUptime(i.uptime)}</small></span>`}
<button class="ib" type="button" data-act="theme" aria-label="Hell/Dunkel umschalten" title="Hell/Dunkel umschalten">${svg(I.moon)}</button>
<button class="ib" type="button" data-act="logout" aria-label="Abmelden">${svg(I.power)}</button>
</div>
<div class="clock">${now.toLocaleTimeString(LOC, { hour: '2-digit', minute: '2-digit' })}</div>
<div class="meta">${now.toLocaleDateString(LOC, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}</div>
</div>`.s;
fitChips();
const ft = $('#ft');
if (ft) ft.innerHTML = html`<span><b>ShareOS</b> ${r.version || ''} · Linux ${i.kernel || ''} · GPLv2</span>
<span>Last ${(i.load || []).map((x) => x.toFixed(2)).join(' ')} · <a href="https://github.com/UniqueDroid/ShareOS" target="_blank" rel="noopener">github.com/UniqueDroid/ShareOS</a></span>`.s;
}

// header chips stay on one line: the ones that do not fit go behind "+N"
// (from the end, so messages and the update stay visible); "+N" shows all of them
function fitChips() {
const row = $('#hd .side .row'), more = row?.querySelector('.more');
if (!row || !more) return;
row.parentElement.querySelector('.chippop')?.remove();
const chips = [...row.querySelectorAll('.chip:not(.keep):not(.more)')];
chips.forEach((c) => { c.hidden = false; });
more.hidden = true;
// with justify-content:flex-end an overflow goes left and scrollWidth does not see it
const need = () => { const v = [...row.children].filter((x) => !x.hidden); return v.reduce((a, x) => a + x.offsetWidth, 0) + 10 * (v.length - 1); };
const room = row.parentElement.clientWidth;
let n = 0;
while (need() > room && n < chips.length) {
chips[chips.length - 1 - n].hidden = true;
n++;
more.hidden = false; more.textContent = '+' + n;
}
more.title = more.ariaLabel = 'Alle Hinweise anzeigen';
more.classList.toggle('warn', chips.slice(chips.length - n).some((c) => c.matches('.warn, .err')));
more.setAttribute('aria-expanded', String(!!S.chipsAll && n > 0));
if (!S.chipsAll || !n) { S.chipsAll = false; return; }
// the hidden ones open as a list under the row; the header keeps its height
const pop = document.createElement('div');
pop.className = 'chippop';
chips.slice(chips.length - n).forEach((c) => { const x = c.cloneNode(true); x.hidden = false; pop.append(x); });
row.after(pop);
}
addEventListener('resize', () => fitChips());
document.addEventListener('click', (e) => {
if (S.chipsAll && !e.target.closest?.('.chip.more')) { S.chipsAll = false; fitChips(); }
});

// areas switched off under System -> Appearance are not shown
const tabOn = (id) => !S.areas || S.areas[id] !== false;
function renderTabs() {
Expand Down Expand Up @@ -1580,7 +1617,7 @@ function snapAfter() {
/* ---------- "Mein ShareOS" for plain users ------------------------------------ */

function userTabs() {
return [['me', 'MEIN SHAREOS'], ...(S.me?.files ? [['files', 'DATEIEN']] : []), ['settings', 'EINSTELLUNGEN']];
return [['me', 'MEIN SHAREOS'], ...(S.me?.files ? [['files', 'DATEIEN']] : []), ...((S.me?.apps || []).length ? [['apps', 'APPS']] : []), ['settings', 'EINSTELLUNGEN']];
}

function parseUserRoute() {
Expand Down Expand Up @@ -1649,12 +1686,14 @@ function renderUserHeader() {
<div class="side">
<div class="row">
${chipsHtml()}
<button class="chip more" type="button" data-act="chips-more" hidden></button>
<button class="ib" type="button" data-act="theme" aria-label="Hell/Dunkel umschalten" title="Hell/Dunkel umschalten">${svg(I.moon)}</button>
<button class="ib" type="button" data-act="logout" aria-label="Abmelden" title="Abmelden">${svg(I.power)}</button>
</div>
<div class="clock">${now.toLocaleTimeString(LOC, { hour: '2-digit', minute: '2-digit' })}</div>
<div class="meta">${now.toLocaleDateString(LOC, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}</div>
</div>`.s;
fitChips();
$('#ft').innerHTML = html`<span><b>ShareOS</b> · Mein ShareOS</span><span><a href="https://github.com/UniqueDroid/ShareOS" target="_blank" rel="noopener">github.com/UniqueDroid/ShareOS</a></span>`.s;
}

Expand Down Expand Up @@ -1695,6 +1734,7 @@ function renderUserMain() {
appFrameLoad();
return;
}
if (S.utab === 'apps') { main.innerHTML = userAppsView().s; return; }
if (S.utab === 'settings') {
main.innerHTML = userLookView().s;
$('#f-mypass').addEventListener('submit', async (e) => {
Expand Down Expand Up @@ -1740,6 +1780,18 @@ function myStorage(m, host, ip) {
${paths}</div>`;
}

// "APPS" of Mein ShareOS: all apps of the user; the star puts one into "FAV APPS" on the start page
function userAppsView() {
const m = S.me || {}, apps = m.apps || [];
return html`<section class="sec">
<h2 class="gh">MEINE APPS <span>${apps.length}</span></h2>
<div class="grid">${apps.map((a) => html`<div class="favwrap"><a class="card applink" href="${appHref(a, m.apps_server)}" data-act="app-launch" data-id="${a.id}">
<div class="icon">${appIcon(a, m.apps_server)}</div>
<div class="info"><div class="name">${a.name}${appMode(a) === 'page' ? '' : ' ↗'}</div><div class="desc">${txtOf(a.description)}</div></div></a>${favStar(a)}</div>`)}</div>
<p class="note">Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.</p>
</section>`;
}

function renderUserHome() {
const m = S.me || {}, host = m.host || location.hostname, ip = location.hostname;
$('#main').innerHTML = html`
Expand All @@ -1757,12 +1809,6 @@ function renderUserHome() {
<div class="ip">\\\\${host}\\${x.name}</div></div></div>`)}</div>`
: html`<div class="panel"><div class="empty">${svg(I.info)}<span>Dir wurde noch keine Freigabe zugewiesen.</span></div></div>`}
</section>
${(m.apps || []).length ? html`<section class="sec">
<h2 class="gh">MEINE APPS <span>${m.apps.length}</span></h2>
<div class="grid">${m.apps.map((a) => html`<div class="favwrap"><a class="card applink" href="${appHref(a, m.apps_server)}" data-act="app-launch" data-id="${a.id}">
<div class="icon">${appIcon(a, m.apps_server)}</div>
<div class="info"><div class="name">${a.name}${appMode(a) === 'page' ? '' : ' ↗'}</div><div class="desc">${txtOf(a.description)}</div></div></a>${favStar(a)}</div>`)}</div>
</section>` : ''}
${m.paperless ? myPaperless() : ''}
${m.cloud ? cloudSection('me') : ''}`.s;
if (m.paperless && !S.myPl) loadMyPaperless();
Expand Down Expand Up @@ -2358,23 +2404,6 @@ const SYS = {
general() {
const i = S.info || {};
return html`<div class="acts" style="margin-bottom:16px"><button class="btn ghost" type="button" data-act="fr-again">Ersteinrichtung erneut starten</button></div>
<div class="g2">
<form class="panel" id="f-host">
<div class="ph"><span class="pt">GERÄTENAME</span></div>
<div class="fields" style="grid-template-columns:1fr"><label class="f"><span>HOSTNAME</span><input name="hostname" value="${i.hostname || ''}" pattern="[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?" required></label></div>
<p class="note">Buchstaben, Ziffern und Bindestrich. Gilt sofort, der DHCP-Server sieht ihn bei der nächsten Verlängerung.</p>
<div class="acts" style="margin-top:14px"><button class="btn primary" type="submit">Speichern</button></div>
</form>
<form class="panel" id="f-pass">
<div class="ph"><span class="pt">ZUGANG · ${S.user}</span></div>
<div class="fields" style="grid-template-columns:1fr">
<label class="f"><span>NEUES PASSWORT</span><input name="p1" type="password" minlength="6" autocomplete="new-password" required></label>
<label class="f"><span>WIEDERHOLEN</span><input name="p2" type="password" minlength="6" autocomplete="new-password" required></label>
</div>
<p class="note">Gilt für die Weboberfläche und SSH. Mindestens 6 Zeichen.</p>
<div class="acts" style="margin-top:14px"><button class="btn primary" type="submit">Passwort ändern</button></div>
</form>
</div>
<h2 class="gh" style="margin-top:22px">EINSTELLUNGEN SICHERN & WIEDERHERSTELLEN</h2>
<div class="g2">
<form class="panel" id="f-backup">
Expand All @@ -2395,6 +2424,24 @@ const SYS = {
</div>
<h2 class="gh" style="margin-top:22px">APP & WIDGETS</h2>
<div class="g2">${appPanel()}${widgetPanel()}</div>
<h2 class="gh" style="margin-top:22px">GERÄT & ZUGANG</h2>
<div class="g2">
<form class="panel" id="f-host">
<div class="ph"><span class="pt">GERÄTENAME</span></div>
<div class="fields" style="grid-template-columns:1fr"><label class="f"><span>HOSTNAME</span><input name="hostname" value="${i.hostname || ''}" pattern="[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?" required></label></div>
<p class="note">Buchstaben, Ziffern und Bindestrich. Gilt sofort, der DHCP-Server sieht ihn bei der nächsten Verlängerung.</p>
<div class="acts" style="margin-top:14px"><button class="btn primary" type="submit">Speichern</button></div>
</form>
<form class="panel" id="f-pass">
<div class="ph"><span class="pt">ZUGANG · ${S.user}</span></div>
<div class="fields" style="grid-template-columns:1fr">
<label class="f"><span>NEUES PASSWORT</span><input name="p1" type="password" minlength="6" autocomplete="new-password" required></label>
<label class="f"><span>WIEDERHOLEN</span><input name="p2" type="password" minlength="6" autocomplete="new-password" required></label>
</div>
<p class="note">Gilt für die Weboberfläche und SSH. Mindestens 6 Zeichen.</p>
<div class="acts" style="margin-top:14px"><button class="btn primary" type="submit">Passwort ändern</button></div>
</form>
</div>
<h2 class="gh" style="margin-top:22px">ÜBER</h2>
${SYS.about()}`;
},
Expand Down Expand Up @@ -3536,6 +3583,7 @@ async function refreshUpdateStatus() {
/* ---------- actions (delegated clicks) -------------------------------------- */

const ACTIONS = {
'chips-more'() { S.chipsAll = !S.chipsAll; fitChips(); },
'page-reload': () => location.reload(),
theme() {
S.theme = effTheme() === 'light' ? 'dark' : 'light';
Expand Down
5 changes: 4 additions & 1 deletion src/shareos-core/www/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,10 @@
"Kompatibilitätsmodus (höchstens SMB 2.1)": "وضع التوافق (SMB 2.1 كحد أقصى)",
"Schneller mit Windows 11, das jede Verbindung signiert: Die Signatur von SMB 2.1 kostet diese Box etwa halb so viel Rechenzeit wie die von SMB 3 (gemessen etwa 9–14 statt 4–10 MB/s beim Schreiben). Ohne Signatur sind etwa 35 MB/s möglich. Dafür gibt es kein SMB 3: keine Verschlüsselung, kein Multichannel. Bestehende Verbindungen werden dabei getrennt.": "أسرع مع Windows 11 الذي يوقّع كل اتصال: توقيع SMB 2.1 يكلّف هذا الجهاز نحو نصف وقت المعالجة الذي يكلّفه توقيع SMB 3 (قيس نحو 9–14 بدلاً من 4–10 ميغابايت/ث عند الكتابة). بدون توقيع يمكن بلوغ نحو 35 ميغابايت/ث. في المقابل لا يوجد SMB 3: لا تشفير ولا تعدد قنوات. تُقطع الاتصالات المفتوحة عند التغيير.",
"Der Assistent löscht die ausgewählten Platten und richtet sie neu ein. Einzelne Platten lassen sich auch direkt an der Platte unter „Laufwerke“ formatieren. Platten mit Daten (z. B. von Alt-F oder der D-Link-Firmware) bindet „… übernehmen“ an der Platte ohne Löschen ein.": "يمسح المعالج الأقراص المختارة ويعيد إعدادها. يمكن أيضاً تهيئة قرص منفرد مباشرة تحت «محركات الأقراص». الأقراص التي تحوي بيانات (مثلاً من Alt-F أو برنامج D-Link الثابت) تُضاف دون مسح عبر «… استلام» عند القرص.",
"Nur für Geräte, die mit SMB 3 nicht zurechtkommen. Die Signatur, die Windows 11 bei jeder Verbindung verlangt, rechnet bei SMB 3 die Krypto-Einheit der Box (gemessen etwa 19–23 MB/s beim Schreiben, ohne Signatur etwa 35); SMB 2.1 rechnet sie auf dem Prozessor (etwa 9–14 MB/s) und hat keine Verschlüsselung und kein Multichannel. Bestehende Verbindungen werden beim Umschalten getrennt.": "فقط للأجهزة التي تواجه مشاكل مع SMB 3. التوقيع الذي يطلبه Windows 11 في كل اتصال يحسبه في SMB 3 محرك التشفير في الجهاز (قيس نحو 19–23 ميغابايت/ث عند الكتابة، ونحو 35 بدون توقيع)؛ أما SMB 2.1 فيحسبه على المعالج (نحو 9–14 ميغابايت/ث) ولا يدعم التشفير ولا تعدد القنوات. تُقطع الاتصالات المفتوحة عند التبديل."
"Nur für Geräte, die mit SMB 3 nicht zurechtkommen. Die Signatur, die Windows 11 bei jeder Verbindung verlangt, rechnet bei SMB 3 die Krypto-Einheit der Box (gemessen etwa 19–23 MB/s beim Schreiben, ohne Signatur etwa 35); SMB 2.1 rechnet sie auf dem Prozessor (etwa 9–14 MB/s) und hat keine Verschlüsselung und kein Multichannel. Bestehende Verbindungen werden beim Umschalten getrennt.": "فقط للأجهزة التي تواجه مشاكل مع SMB 3. التوقيع الذي يطلبه Windows 11 في كل اتصال يحسبه في SMB 3 محرك التشفير في الجهاز (قيس نحو 19–23 ميغابايت/ث عند الكتابة، ونحو 35 بدون توقيع)؛ أما SMB 2.1 فيحسبه على المعالج (نحو 9–14 ميغابايت/ث) ولا يدعم التشفير ولا تعدد القنوات. تُقطع الاتصالات المفتوحة عند التبديل.",
"GERÄT & ZUGANG": "الجهاز والوصول",
"Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "النجمة تجعل التطبيق مفضلاً – تظهر المفضلة أعلى «ShareOS الخاص بي».",
"Alle Hinweise anzeigen": "عرض كل التنبيهات"
},
"patterns": [
[
Expand Down
Loading
Loading