Files
syncthing/gui/default/syncthing/settings/settingsModalView.html
T
tomasz1986 6fc0b41f97 feat(gui): add option to limit bandwidth in LAN to Settings (ref #10046) (#10182)
gui: Add option to limit bandwidth in LAN to Settings (ref #10046)

Currently, the option to limit bandwidth in LAN is available only via
the Advanced Configuration, which makes it difficult to discover by new
users, who are confused why their bandwidth limits are not working.

For this reason, make the option easily discoverable by adding it
directly to Connections in the normal Settings in the Web GUI.

Ref:
https://github.com/syncthing/syncthing/issues/10046
https://github.com/syncthing/syncthing/issues/2046
https://github.com/syncthing/syncthing/issues/2569
https://forum.syncthing.net/t/upload-rate-not-work/19025

https://forum.syncthing.net/t/rate-limits-dont-work-over-lan-in-v1-20-1/18521

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>

### Screenshots


![image](https://github.com/user-attachments/assets/bd38b3ab-a053-4992-ba86-7fb8b3ba09d9)

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2025-06-17 09:46:38 +02:00

356 lines
18 KiB
HTML

<modal id="settings" status="default" icon="fas fa-cog" heading="{{'Settings' | translate}}" large="yes" closeable="yes">
<div class="modal-body">
<form role="form" name="settingsEditor">
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#settings-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
<li><a data-toggle="tab" href="#settings-gui"><span class="fas fa-desktop"></span> <span translate>GUI</span></a></li>
<li><a data-toggle="tab" href="#settings-connections"><span class="fas fa-sitemap"></span> <span translate>Connections</span></a></li>
<li>
<a data-toggle="tab" href="#settings-ignored-devices">
<span class="fas fa-laptop"></span>
&nbsp;
<span translate>Ignored Devices</span>
&nbsp;
<span class="badge">{{ tmpRemoteIgnoredDevices.length }}</span>
</a>
</li>
<li>
<a data-toggle="tab" href="#settings-ignored-folders">
<span class="fas fa-folder"></span>
&nbsp;
<span translate>Ignored Folders</span>
&nbsp;
<span class="badge">{{ ignoredFoldersCountTmpConfig() }}</span>
</a>
</li>
</ul>
<div class="tab-content">
<div id="settings-general" class="tab-pane in active">
<div class="form-group">
<label translate for="DeviceName">Device Name</label>
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.deviceName" />
</div>
<div class="row">
<div class="col-md-6">
<div class="form-horizontal">
<div class="form-group" ng-class="{'has-error': settingsEditor.minHomeDiskFree.$invalid && settingsEditor.minHomeDiskFree.$dirty}">
<label class="col-xs-12" for="minHomeDiskFree"><span translate>Minimum Free Disk Space</span></label><br />
<div class="col-xs-9"><input name="minHomeDiskFree" id="minHomeDiskFree" class="form-control" type="number" ng-model="tmpOptions.minHomeDiskFree.value" required="" aria-required="true" min="0" step="0.01" /></div>
<div class="col-xs-3"><select class="col-sm-3 form-control" ng-model="tmpOptions.minHomeDiskFree.unit">
<option value="%">%</option>
<option value="kB">kB</option>
<option value="MB">MB</option>
<option value="GB">GB</option>
<option value="TB">TB</option>
</select></div>
<p class="col-xs-12 help-block">
<span translate ng-show="settingsEditor.minHomeDiskFree.$invalid">Enter a non-negative number (e.g., "2.35") and select a unit. Percentages are as part of the total disk size.</span>
<span translate ng-hide="settingsEditor.minHomeDiskFree.$invalid">This setting controls the free space required on the home (i.e., index database) disk.</span>
</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label translate>API Key</label>
<div class="input-group">
<input type="text" readonly class="text-monospace form-control" value="{{tmpGUI.apiKey || '-'}}" />
<span class="input-group-btn">
<button type="button" class="btn btn-default btn-secondary" ng-click="setAPIKey(tmpGUI)">
<span class="fas fa-redo"></span>&nbsp;<span translate>Generate</span>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label translate for="urVersion">Anonymous Usage Reporting</label> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
<div ng-if="tmpOptions.upgrades != 'candidate' && !version.isCandidate">
<select class="form-control" id="urVersion" ng-model="tmpOptions._urAcceptedStr">
<option ng-repeat="n in urVersions()" value="{{n}}">{{'Version' | translate}} {{n}}</option>
<!-- 1 does not exist, as we did not support incremental formats back then. -->
<option value="0" translate>Undecided (will prompt)</option>
<option value="-1" translate>Disabled</option>
</select>
</div>
<p class="help-block" ng-if="tmpOptions.upgrades == 'candidate' || version.isCandidate">
<span translate>Usage reporting is always enabled for candidate releases.</span>
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label translate>Automatic upgrades</label>&emsp;<a href="{{docsURL('users/releases')}}" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
<select class="form-control" ng-model="tmpOptions.upgrades" ng-if="upgradeInfo">
<option ng-if="!version.isCandidate" value="none" translate>No upgrades</option>
<option value="stable" translate>Stable releases only</option>
<option value="candidate" translate>Stable releases and release candidates</option>
</select>
<p class="help-block" ng-if="!upgradeInfo">
<span translate>Unavailable/Disabled by administrator or maintainer</span>
</p>
<p class="help-block" ng-if="version.isCandidate && upgradeInfo">
<span translate>Automatic upgrades are always enabled for candidate releases.</span>
</p>
</div>
</div>
</div>
<div>
<label translate>Default Configuration</label>
<p>
<button type="button" class="btn btn-default btn-secondary" ng-click="editFolderDefaults()">
<span translate>Edit Folder Defaults</span>
</button>
<button type="button" class="btn btn-default btn-secondary" ng-click="editDeviceDefaults()">
<span translate>Edit Device Defaults</span>
</button>
</p>
</div>
</div>
<div id="settings-gui" class="tab-pane">
<div class="form-group" ng-class="{'has-error': settingsEditor.Address.$invalid && settingsEditor.Address.$dirty}">
<label translate for="Address">GUI Listen Address</label>&emsp;<a href="{{docsURL('users/guilisten')}}" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
<p class="text-warning" ng-show="system.guiAddressOverridden">
<span class="fas fa-exclamation-triangle"></span>
<span translate>The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.</span>
</p>
<input id="Address" name="Address" class="form-control" type="text" ng-model="tmpGUI.address" ng-pattern="/^(/.*)|(.*:0*((102[4-9])|(10[3-9][0-9])|(1[1-9][0-9][0-9])|([2-9][0-9][0-9][0-9])|([1-6]\d{4})))$/" />
<p class="help-block" ng-show="settingsEditor.Address.$invalid" translate>
Enter a non-privileged port number (1024 - 65535).
</p>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label translate for="user">GUI Authentication User</label>
<input id="user" class="form-control" type="text" name="user" ng-model="tmpGUI.user" autocomplete="username" />
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label translate for="password">GUI Authentication Password</label>
<input id="password" class="form-control" type="password" name="password" ng-model="tmpGUI.password" ng-trim="false" autocomplete="new-password" />
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.useTLS" /> <span translate>Use HTTPS for GUI</span>
</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.startBrowser" /> <span translate>Start Browser</span>
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label translate>GUI Theme</label>
<select class="form-control" ng-model="tmpGUI.theme" ng-if="themes.length > 1">
<option ng-repeat="theme in themes.sort()" value="{{ theme }}">
{{ themeName(theme) }}
</option>
</select>
<p class="help-block" ng-if="themes.length < 2">
<span translate>Unavailable</span>
</p>
</div>
</div>
<div class="col-md-6">
<div ng-if="isUnixAddress(tmpGUI.address)" class="form-group" ng-class="{'has-error': settingsEditor.UnixSocketPermissions.$invalid && settingsEditor.UnixSocketPermissions.$dirty}">
<label translate>UNIX Permissions</label>
<input id="UnixSocketPermissions" name="UnixSocketPermissions" class="form-control" type="text" ng-model="tmpGUI.unixSocketPermissions" ng-pattern="/^0?[0-7]{0,3}$/" />
<p class="help-block" ng-show="settingsEditor.UnixSocketPermissions.$invalid" translate>
Enter up to three octal digits.
</p>
</div>
</div>
</div>
</div>
<div id="settings-connections" class="tab-pane">
<div class="form-group">
<label translate for="ListenAddressesStr">Sync Protocol Listen Addresses</label>&emsp;<a href="{{docsURL('users/config#listen-addresses')}}" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
<input id="ListenAddressesStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressesStr" />
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
<input id="MaxRecvKbps" name="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps" min="0" />
<p class="help-block">
<span translate ng-if="settingsEditor.MaxRecvKbps.$error.min && settingsEditor.MaxRecvKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxSendKbps.$invalid && settingsEditor.MaxSendKbps.$dirty}">
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
<input id="MaxSendKbps" name="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.maxSendKbps" min="0" />
<p class="help-block">
<span translate ng-if="settingsEditor.MaxSendKbps.$error.min && settingsEditor.MaxSendKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
</p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="LimitBandwidthInLan" type="checkbox" ng-model="tmpOptions.limitBandwidthInLan" /> <span translate>Limit Bandwidth in LAN</span>
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="NATEnabled" type="checkbox" ng-model="tmpOptions.natEnabled" /> <span translate>Enable NAT traversal</span>
</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.localAnnounceEnabled" /> <span translate>Local Discovery</span>
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.globalAnnounceEnabled" /> <span translate>Global Discovery</span>
</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="RelaysEnabled" type="checkbox" ng-model="tmpOptions.relaysEnabled" /> <span translate>Enable Relaying</span>
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label translate for="GlobalAnnServersStr">Global Discovery Servers</label>
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr" />
</div>
</div>
<div class="col-md-6">
</div>
</div>
</div>
<div id="settings-ignored-devices" class="tab-pane">
<div class="form-group">
<span ng-if="tmpRemoteIgnoredDevices.length === 0" translate>You have no ignored devices.</span>
<div class="table-responsive" ng-if="tmpRemoteIgnoredDevices.length > 0">
<table class="table-condensed table-striped table" style="table-layout: auto;">
<thead>
<tr>
<th translate>Ignored at</th>
<th translate>Device</th>
<th translate>Address</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="ignoredDevice in tmpRemoteIgnoredDevices">
<td class="no-overflow-ellipse">{{ ignoredDevice.time | date:"yyyy-MM-dd HH:mm:ss" }}</td>
<td>
<span ng-if="!ignoredDevice.name">{{ ignoredDevice.deviceID }}</span>
<span tooltip data-original-title="{{ ignoredDevice.deviceID }}" ng-if="ignoredDevice.name">{{ ignoredDevice.name }}</span>
</td>
<td class="no-overflow-ellipse">{{ ignoredDevice.address }}</td>
<td>
<a href="" ng-click="unignoreDeviceFromTemporaryConfig(ignoredDevice)">
<span class="fas fa-times"></span>&nbsp;<span translate>Unignore</span>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="settings-ignored-folders" class="tab-pane">
<div class="form-group">
<span ng-if="ignoredFoldersCountTmpConfig() === 0" translate>You have no ignored folders.</span>
<div class="table-responsive" ng-if="ignoredFoldersCountTmpConfig() > 0">
<table class="table-condensed table-striped table" style="table-layout: auto;">
<thead>
<tr>
<th translate>Ignored at</th>
<th translate>Folder</th>
<th translate>Device</th>
<th></th>
</tr>
</thead>
<tbody ng-repeat-start="device in tmpDevices">
<tr ng-repeat="ignoredFolder in device.ignoredFolders">
<td class="no-overflow-ellipse">{{ ignoredFolder.time | date:"yyyy-MM-dd HH:mm:ss" }}</td>
<td>{{ folderLabel(ignoredFolder.id) }}</td>
<td>
<span tooltip data-original-title="{{ device.deviceID }}">{{ friendlyNameFromID(device.deviceID) }}</span>
</td>
<td>
<a href="" ng-click="unignoreFolderFromTemporaryConfig(device.deviceID, ignoredFolder.id)">
<span class="fas fa-times"></span>&nbsp;<span translate>Unignore</span>
</a>
</td>
</tr>
</tbody>
<tfoot ng-repeat-end></tfoot>
</table>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()">
<span class="fas fa-check"></span>&nbsp;<span translate>Save</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</modal>