Compare commits

..

22 commits
main ... load

Author SHA1 Message Date
yflory
02e0d3ba5d Merge branch 'sodium' into load 2024-07-05 14:31:43 +02:00
yflory
7b8e83d72b Merge branch 'main' into load 2024-07-05 14:30:24 +02:00
yflory
411039abeb Add support for a serverside crypto plugin 2024-06-27 16:26:57 +02:00
yflory
43cfc926bb Merge branch 'monitoring' into load 2024-06-24 11:22:38 +02:00
yflory
1d639a7653 update package.json and package-lock.json 2024-06-12 15:32:26 +02:00
yflory
1310979994 lint compliance 2024-06-12 15:01:57 +02:00
yflory
2de0a0d4b9 Merge branch 'stats' into monitoring 2024-06-12 14:59:55 +02:00
yflory
8c6acf9578 Guard against null channels 2024-05-23 17:41:26 +02:00
yflory
73891d819e Add new monitoring data 2024-05-23 16:01:38 +02:00
yflory
83dd6fa16c Add response time to load testing results 2024-05-23 11:57:46 +02:00
yflory
a8e03cef9d Fix offset with load test data creation 2024-05-22 18:16:12 +02:00
yflory
c17312c3fb More config options for load test 2024-05-22 16:28:37 +02:00
yflory
cf153b8474 Load testing script 2024-05-22 13:54:50 +02:00
yflory
eb1249b6cd Fix cpu usage monitoring 2024-05-17 11:22:32 +02:00
yflory
332edec162 Merge branch 'main' into monitoring 2024-05-17 10:45:37 +02:00
yflory
bde6ef8044 Add CPU monitoring 2024-05-17 10:30:39 +02:00
yflory
9b8e487b70 Merge branch 'main' into monitoring 2024-05-16 17:19:40 +02:00
yflory
99f1cf650e Fix package.json 2024-05-16 17:18:00 +02:00
yflory
9c22a25ba2 Merge branch 'main' into monitoring 2024-05-16 17:15:09 +02:00
yflory
fff4e7581e Add heap memory data to the monitoring tools 2023-08-30 16:13:09 +02:00
yflory
49af0533b5 lint compliance 2023-08-22 18:36:48 +02:00
yflory
4f2a48a72e Add memory monitoring tools 2023-08-22 18:09:22 +02:00
41 changed files with 1057 additions and 2124 deletions

View file

@ -89,7 +89,6 @@ body:
label: Version label: Version
description: What version of CryptPad are you running? description: What version of CryptPad are you running?
options: options:
- 2024.6.1
- 2024.6.0 - 2024.6.0
- 2024.3.1 - 2024.3.1
- 2024.3.0 - 2024.3.0

View file

@ -4,51 +4,6 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and cont
SPDX-License-Identifier: AGPL-3.0-or-later SPDX-License-Identifier: AGPL-3.0-or-later
--> -->
# 2024.6.1
## Goals
This is a bugfix release to address issues that were reported by Cryptpad.fr users. We took the opportunity to update the translations with some new languages contributed by the community.
## Improvements
- Translations update from CryptPad Translations [#1575](https://github.com/cryptpad/cryptpad/pull/1575)
- Added: Español cubano, اَلْعَرَبِيَّةُ Arabic, Svenska
- Removed some languages without enough coverage
- Greek (16%)
- Romanian (36%)
## Fixes
- Calendar events sometimes dont appear when created [#1551](https://github.com/cryptpad/cryptpad/issues/1551) fixed by [072dba2](https://github.com/cryptpad/cryptpad/commit/072dba254e3c2be32cd6b261d84510909deb713f)
- Revert the new method of counting registered users in the admin panel [4544be6](https://github.com/cryptpad/cryptpad/commit/4544be6b4d9fa7291b19cb366f7dd492dfe07340)
- Fix broken OnlyOffice Document [#1572](https://github.com/cryptpad/cryptpad/issues/1572)
- Fix printing in Code documents [#1557](https://github.com/cryptpad/cryptpad/pull/1557) [#1478](https://github.com/cryptpad/cryptpad/pull/1478)
- Fix OnlyOffice undefined functions [#1550](https://github.com/cryptpad/cryptpad/pull/1550)
- Fix keyboard operation of confirm modals [#1576](https://github.com/cryptpad/cryptpad/issues/1576)
- Pressing Enter on the "Cancel" button triggered the "OK" button instead
## Upgrade notes
If you are upgrading from a version older than `2024.6.0` please read the upgrade notes of all versions between yours and `2024.6.1` to avoid configuration issues.
To upgrade:
1. Stop your server
2. Get the latest code with git
```bash
git fetch origin --tags
git checkout 2024.6.1
npm ci
npm run install:components
./install-onlyoffice.sh
```
3. Restart your server
4. Review your instance's checkup page to ensure that you are passing all tests
# 2024.6.0 # 2024.6.0
## Goals ## Goals
@ -80,7 +35,6 @@ This release introduces a new onboarding flow to guide administrators through th
- Remove x2t from the CryptPad repo [#1454](https://github.com/cryptpad/cryptpad/issues/1454) - Remove x2t from the CryptPad repo [#1454](https://github.com/cryptpad/cryptpad/issues/1454)
- Other OnlyOffice users are shown as "Guest" [#1446](https://github.com/cryptpad/cryptpad/issues/1446) - Other OnlyOffice users are shown as "Guest" [#1446](https://github.com/cryptpad/cryptpad/issues/1446)
- Document PDF exports are empty when remote embedding is disabled [#1472](https://github.com/cryptpad/cryptpad/issues/1472) - Document PDF exports are empty when remote embedding is disabled [#1472](https://github.com/cryptpad/cryptpad/issues/1472)
- Sometimes images of a presentation are not exported to PDF [#1500](https://github.com/cryptpad/cryptpad/issues/1500)
- Automatic upgrade of an OnlyOffice document fails sometimes [#1534](https://github.com/cryptpad/cryptpad/issues/1534) - Automatic upgrade of an OnlyOffice document fails sometimes [#1534](https://github.com/cryptpad/cryptpad/issues/1534)
- Import/Export is broken [#1532](https://github.com/cryptpad/cryptpad/issues/1532) - Import/Export is broken [#1532](https://github.com/cryptpad/cryptpad/issues/1532)
- Print is broken [#1533](https://github.com/cryptpad/cryptpad/issues/1533) - Print is broken [#1533](https://github.com/cryptpad/cryptpad/issues/1533)

View file

@ -5,13 +5,11 @@
(function () { (function () {
// add your module to this map so it gets used // add your module to this map so it gets used
var map = { var map = {
'ar': 'اَلْعَرَبِيَّةُ',
'ca': 'Català', 'ca': 'Català',
'cs': 'Čeština', 'cs': 'Čeština',
'de': 'Deutsch', 'de': 'Deutsch',
//'el': 'Ελληνικά', 'el': 'Ελληνικά',
'es': 'Español', 'es': 'Español',
'es_CU': 'Español cubano',
'eu': 'Euskara', 'eu': 'Euskara',
'fi': 'Suomi', 'fi': 'Suomi',
'fr': 'Français', 'fr': 'Français',
@ -24,9 +22,9 @@ var map = {
'pl': 'Polski', 'pl': 'Polski',
'pt-br': 'Português do Brasil', 'pt-br': 'Português do Brasil',
'pt-pt': 'Português do Portugal', 'pt-pt': 'Português do Portugal',
//'ro': 'Română', 'ro': 'Română',
'ru': 'Русский', 'ru': 'Русский',
'sv': 'Svenska', //'sv': 'Svenska',
//'te': 'తెలుగు', //'te': 'తెలుగు',
'uk': 'Українська', 'uk': 'Українська',
'zh': '中文(簡體)', 'zh': '中文(簡體)',

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@import (reference) "../include/infopages.less";
@import (reference) "../include/colortheme-all.less";
@import (reference) "../include/alertify.less";
@import (reference) "../include/checkmark.less";
@import (reference) "../include/forms.less";
&.cp-page-load {
.infopages_main();
.forms_main();
.alertify_main();
.checkmark_main(20px);
.form {
max-width: 400px;
padding: 20px;
}
.alertify {
// workaround for alertify making empty p
p:empty {
display: none;
}
nav {
display: flex;
align-items: center;
justify-content: flex-end;
}
@media screen and (max-width: 600px) {
nav .btn-danger {
line-height: inherit;
}
}
}
}

View file

@ -8,8 +8,3 @@
margin: 3cm; margin: 3cm;
size: A4 portrait; size: A4 portrait;
} }
@media print {
body {
background: white !important;
}
}

View file

@ -1,18 +0,0 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.ar.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

View file

@ -1,18 +0,0 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.es_CU.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

View file

@ -5,7 +5,7 @@
--- ---
services: services:
cryptpad: cryptpad:
image: "cryptpad/cryptpad:version-2024.6.1" image: "cryptpad/cryptpad:version-2024.6.0"
hostname: cryptpad hostname: cryptpad
environment: environment:

View file

@ -31,7 +31,7 @@ fi
cd $CPAD_HOME cd $CPAD_HOME
if [ "$CPAD_INSTALL_ONLYOFFICE" == "yes" ]; then if [ "$CPAD_INSTALL_ONLYOFFICE" == "yes" ]; then
./install-onlyoffice.sh --accept-license --trust-repository ./install-onlyoffice.sh --accept-license
fi fi
npm run build npm run build

View file

@ -33,7 +33,7 @@ main() {
install_version v4 6ebc6938 install_version v4 6ebc6938
install_version v5 88a356f0 install_version v5 88a356f0
install_version v6 abd8a309 install_version v6 abd8a309
install_version v7 e1267803 install_version v7 ba82142f
install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1 install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1
rm -rf "$BUILDS_DIR" rm -rf "$BUILDS_DIR"
@ -69,10 +69,6 @@ parse_arguments() {
ACCEPT_LICENSE="1" ACCEPT_LICENSE="1"
shift shift
;; ;;
-t | --trust-repository)
TRUST_REPOSITORY="1"
shift
;;
*) *)
show_help show_help
shift shift
@ -114,26 +110,17 @@ OPTIONS:
Accept the license of OnlyOffice and do not ask when running this Accept the license of OnlyOffice and do not ask when running this
script. Read and accept this before using this option: script. Read and accept this before using this option:
https://github.com/ONLYOFFICE/web-apps/blob/master/LICENSE.txt https://github.com/ONLYOFFICE/web-apps/blob/master/LICENSE.txt
-t, --trust-repository
Automatically configure the cloned onlyoffice-builds repository
as a safe.directory.
https://git-scm.com/docs/git-config/#Documentation/git-config.txt-safedirectory
EOF EOF
exit 1 exit 1
} }
ensure_oo_is_downloaded() { ensure_oo_is_downloaded() {
ensure_command_available git ensure_command_available git
if ! [ -d "$BUILDS_DIR" ]; then if ! [ -d "$BUILDS_DIR" ]; then
echo "Downloading OnlyOffice..." echo "Downloading OnlyOffice..."
git clone --bare https://github.com/cryptpad/onlyoffice-builds.git "$BUILDS_DIR" git clone --bare https://github.com/cryptpad/onlyoffice-builds.git "$BUILDS_DIR"
fi fi
if [ ${TRUST_REPOSITORY+x} ] || [ "${PROPS[trust_repository]:-no}" == yes ]; then
git config --global --add safe.directory /cryptpad/onlyoffice-conf/onlyoffice-builds.git
fi
} }
install_version() { install_version() {

View file

@ -108,7 +108,7 @@ nThen(function (w) {
}; };
// spawn ws server and attach netflux event handlers // spawn ws server and attach netflux event handlers
let Server = NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer})) let Server = Env.Server = NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer}))
.on('channelClose', historyKeeper.channelClose) .on('channelClose', historyKeeper.channelClose)
.on('channelMessage', historyKeeper.channelMessage) .on('channelMessage', historyKeeper.channelMessage)
.on('channelOpen', historyKeeper.channelOpen) .on('channelOpen', historyKeeper.channelOpen)

View file

@ -104,8 +104,10 @@ var shutdown = function (Env, Server, cb) {
var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) { var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) {
Env.batchRegisteredUsers('', cb, function (done) { Env.batchRegisteredUsers('', cb, function (done) {
var dir = Env.paths.pin; var dir = Env.paths.pin;
var folders; var dirB = Env.paths.block;
var folders, foldersB;
var users = 0; var users = 0;
var blocks = 0;
nThen(function (waitFor) { nThen(function (waitFor) {
Fs.readdir(dir, waitFor(function (err, list) { Fs.readdir(dir, waitFor(function (err, list) {
if (err) { if (err) {
@ -114,6 +116,13 @@ var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) {
} }
folders = list; folders = list;
})); }));
Fs.readdir(dirB, waitFor(function (err, list) {
if (err) {
waitFor.abort();
return void done(err);
}
foldersB = list;
}));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
folders.forEach(function (f) { folders.forEach(function (f) {
var dir = Env.paths.pin + '/' + f; var dir = Env.paths.pin + '/' + f;
@ -126,8 +135,20 @@ var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) {
users += list.length; users += list.length;
})); }));
}); });
}).nThen(function (waitFor) {
foldersB.forEach(function (f) {
var dir = Env.paths.block + '/' + f;
Fs.readdir(dir, waitFor(function (err, list) {
if (err) { return; }
// Don't count placeholders
list = list.filter(name => {
return !/\.placeholder$/.test(name);
});
blocks += list.length;
}));
});
}).nThen(function () { }).nThen(function () {
done(void 0, {users}); done(void 0, {users, blocks});
}); });
}); });
}; };

View file

@ -161,11 +161,13 @@ var queryAccountServer = function (Env, cb) {
nThen(waitFor => { nThen(waitFor => {
Admin.getRegisteredUsers(Env, null, waitFor((err, data) => { Admin.getRegisteredUsers(Env, null, waitFor((err, data) => {
if (err) { return; } if (err) { return; }
stats.registered = data.users; stats.registered = data.blocks;
if (Env.lastPingRegisteredUsers) { if (Env.lastPingRegisteredUsers) {
stats.usersDiff = stats.registered - Env.lastPingRegisteredUsers; stats.usersDiff = stats.registered - Env.lastPingRegisteredUsers;
} }
Env.lastPingRegisteredUsers = stats.registered; Env.lastPingRegisteredUsers = stats.registered;
let teams = (data.users - data.blocks);
if (teams > 0) { stats.teams = teams; }
})); }));
}).nThen(() => { }).nThen(() => {
if (Env.maxConcurrentWs) { if (Env.maxConcurrentWs) {

24
lib/crypto.js Normal file
View file

@ -0,0 +1,24 @@
const Nacl = require('tweetnacl/nacl-fast');
const CPCrypto = module.exports;
const plugins = require('./plugin-manager');
CPCrypto.init = (cb) => {
const crypto = {};
crypto.open = (signedMsg, validateKey) => {
return Nacl.sign.open(signedMsg, validateKey);
};
crypto.detachedVerify = (signedBuffer, signatureBuffer, validateKey) => {
return Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer);
};
if (plugins.SODIUM && plugins.SODIUM.crypto) {
let c = plugins.SODIUM.crypto;
if (c.open) { crypto.open = c.open; }
if (c.detachedVerify) { crypto.detachedVerify = c.detachedVerify; }
}
// Make async because we might need it later with libsodium's promise
// libsodium.ready.then(() => {});
setTimeout(() => {
cb(void 0, crypto);
});
};

View file

@ -252,6 +252,7 @@ module.exports.create = function (config) {
curvePublic: Nacl.util.encodeBase64(curve.publicKey), curvePublic: Nacl.util.encodeBase64(curve.publicKey),
selfDestructTo: {}, selfDestructTo: {},
monitoring: {}
}; };
(function () { (function () {
@ -415,6 +416,7 @@ const BAD = [
'limits', 'limits',
'customLimits', 'customLimits',
'scheduleDecree', 'scheduleDecree',
'monitoring',
'httpServer', 'httpServer',

View file

@ -19,6 +19,9 @@ const BlobStore = require("./storage/blob");
const BlockStore = require("./storage/block"); const BlockStore = require("./storage/block");
const plugins = require("./plugin-manager"); const plugins = require("./plugin-manager");
const Prometheus = require('prom-client');
const Monitoring = require('./monitoring');
const DEFAULT_QUERY_TIMEOUT = 5000; const DEFAULT_QUERY_TIMEOUT = 5000;
const PID = process.pid; const PID = process.pid;
@ -66,6 +69,102 @@ Env.incrementBytesWritten = function () {};
const EVENTS = {}; const EVENTS = {};
// XXX Store in monitoring.js
const rssMetric = new Prometheus.Gauge({
name: `memory_rss`,
help: 'The amount of space occupied in the main memory device for the process.',
labelNames: ['pid', 'type']
});
const heapTotalMetric = new Prometheus.Gauge({
name: `memory_heap_total`,
help: "Total heap memory.",
labelNames: ['pid', 'type']
});
const heapUsedMetric = new Prometheus.Gauge({
name: `memory_heap_used`,
help: 'Used heap memory.',
labelNames: ['pid', 'type']
});
const externalMetric = new Prometheus.Gauge({
name: `memory_external`,
help: 'Memory usage of C++ objects bound to JavaScript objects managed by V8.',
labelNames: ['pid', 'type']
});
const arrayBufferMetric = new Prometheus.Gauge({
name: `memory_array_buffers`,
help: 'Memory allocated for ArrayBuffers and SharedArrayBuffers.',
labelNames: ['pid', 'type']
});
const cpuUserMetric = new Prometheus.Gauge({
name: `process_cpu_user_seconds_total`,
help: 'Total user CPU time spent in seconds during the configured interval.',
labelNames: ['pid', 'type']
});
const cpuSystemMetric = new Prometheus.Gauge({
name: `process_cpu_system_seconds_total`,
help: 'Total system CPU time spent in seconds during the configured interval.',
labelNames: ['pid', 'type']
});
const cpuTotalMetric = new Prometheus.Gauge({
name: `process_cpu_seconds_total`,
help: 'Total user and system CPU time spent in seconds during the configured interval',
labelNames: ['pid', 'type']
});
const cpuPercentMetric = new Prometheus.Gauge({
name: `process_cpu_percent`,
help: 'Total user and system CPU time spent divided by the interval duration',
labelNames: ['pid', 'type']
});
const wsMetric = new Prometheus.Gauge({
name: `active_websockets`,
help: 'Number of active websocket connections',
});
const regMetric = new Prometheus.Gauge({
name: `active_registered_users`,
help: 'Number of registered users online',
});
const chanMetric = new Prometheus.Gauge({
name: `active_channels`,
help: 'Number of active pads',
});
EVENTS.MONITORING = function (data) {
/*
{
main: {
rss: 1234
...
},
pid1: {
rss: 234
...
}
}
*/
Object.keys(data).forEach(pid => {
let val = data[pid];
let type = val.type;
rssMetric.set({pid, type}, val.mem?.rss || 0);
heapTotalMetric.set({pid, type}, val.mem?.heapTotal || 0);
heapUsedMetric.set({pid, type}, val.mem?.heapUsed || 0);
externalMetric.set({pid, type}, val.mem?.external || 0);
arrayBufferMetric.set({pid, type}, val.mem?.arrayBuffers || 0);
let userSeconds = (val.cpu?.user || 0) / 1000000;
let systemSeconds = (val.cpu?.system || 0) / 1000000;
cpuUserMetric.set({pid, type}, userSeconds);
cpuSystemMetric.set({pid, type}, systemSeconds);
let sum = userSeconds + systemSeconds;
let percent = sum / (Monitoring.interval/1000);
cpuTotalMetric.set({pid, type}, sum);
cpuPercentMetric.set({pid, type}, percent);
if (type === 'main') {
wsMetric.set(val.ws || 0);
regMetric.set(val.registered || 0);
chanMetric.set(val.channels || 0);
}
});
};
EVENTS.ENV_UPDATE = function (data /*, cb */) { EVENTS.ENV_UPDATE = function (data /*, cb */) {
try { try {
Env = JSON.parse(data); Env = JSON.parse(data);
@ -219,6 +318,13 @@ const wsProxy = createProxyMiddleware({
app.use('/cryptpad_websocket', wsProxy); app.use('/cryptpad_websocket', wsProxy);
app.get('/metrics', (req, res) => {
Prometheus.register.metrics().then((data) => {
res.set('Content-Type', Prometheus.register.contentType);
res.send(data);
});
});
app.use('/ssoauth', (req, res, next) => { app.use('/ssoauth', (req, res, next) => {
if (SSOUtils && req && req.body && req.body.SAMLResponse) { if (SSOUtils && req && req.body && req.body.SAMLResponse) {
req.method = 'GET'; req.method = 'GET';
@ -799,7 +905,14 @@ nThen(function (w) {
})); }));
}).nThen(function () { }).nThen(function () {
// TODO inform the parent process that this worker is ready // TODO inform the parent process that this worker is ready
setInterval(() => {
sendMessage({
command: 'MONITORING',
data: Monitoring.getData('http-worker')
}, () => {
// Done
});
}, Monitoring.interval);
}); });
process.on('uncaughtException', function (err) { process.on('uncaughtException', function (err) {

45
lib/monitoring.js Normal file
View file

@ -0,0 +1,45 @@
/*
globals process
*/
const VALUES = {};
VALUES.mem = () => {
return process.memoryUsage();
};
let oldCpu;
VALUES.cpu = () => {
if (!oldCpu) {
oldCpu = process.cpuUsage();
return {user:0,system:0};
}
let val = process.cpuUsage(oldCpu);
oldCpu = process.cpuUsage();
return val;
};
const applyToEnv = (Env, data) => {
if (!Env) { return; }
Env.monitoring[data.pid] = data;
};
const getData = (type) => {
const value = {
pid: process.pid,
type: type
};
Object.keys(VALUES).forEach(key => {
value[key] = VALUES[key]();
});
return value;
};
const remove = (Env, pid) => {
if (Env && Env.monitoring && pid && Env.monitoring[pid]) {
delete Env.monitoring[pid];
}
};
module.exports = {
interval: 5000,
applyToEnv,
getData,
remove
};

View file

@ -170,6 +170,15 @@ var rpc = function (Env, Server, userId, data, respond) {
var command = msg[1]; var command = msg[1];
/*
// TODO get data from lib/upload.js to be able to get the size of the uploaded file
if (command === 'UPLOAD_COMPLETE' || command === 'OWNED_UPLOAD_COMPLETE') {
let m = Env.monitoring = Env.monitoring || {};
let b = m.upload = m.upload || {};
let id = msg[2];
if (id) { b[id] = +new Date(); }
}
*/
if (command === 'UPLOAD') { if (command === 'UPLOAD') {
// UPLOAD is a special case that skips signature validation // UPLOAD is a special case that skips signature validation
// intentional fallthrough behaviour // intentional fallthrough behaviour

View file

@ -16,6 +16,8 @@ const Logger = require("../log");
const Tasks = require("../storage/tasks"); const Tasks = require("../storage/tasks");
const Nacl = require('tweetnacl/nacl-fast'); const Nacl = require('tweetnacl/nacl-fast');
const Eviction = require("../eviction"); const Eviction = require("../eviction");
const Monitoring = require('../monitoring');
const CPCrypto = require('../crypto');
const Env = { const Env = {
Log: {}, Log: {},
@ -56,6 +58,13 @@ const init = function (config, _cb) {
Env.archiveRetentionTime = config.archiveRetentionTime; Env.archiveRetentionTime = config.archiveRetentionTime;
Env.accountRetentionTime = config.accountRetentionTime; Env.accountRetentionTime = config.accountRetentionTime;
setInterval(() => {
process.send({
monitoring: true,
data: Monitoring.getData('db-worker')
});
}, Monitoring.interval);
nThen(function (w) { nThen(function (w) {
Store.create(config, w(function (err, _store) { Store.create(config, w(function (err, _store) {
if (err) { if (err) {
@ -101,6 +110,10 @@ const init = function (config, _cb) {
} }
Env.tasks = tasks; Env.tasks = tasks;
})); }));
}).nThen(function (w) {
CPCrypto.init(w(function (err, crypto) {
Env.crypto = crypto;
}));
}).nThen(function () { }).nThen(function () {
cb(); cb();
}); });
@ -680,7 +693,8 @@ COMMANDS.INLINE = function (data, cb) {
return void cb("E_BADKEY"); return void cb("E_BADKEY");
} }
// validate the message // validate the message
const validated = Nacl.sign.open(signedMsg, validateKey); //const validated = Nacl.sign.open(signedMsg, validateKey);
const validated = Env.crypto.open(signedMsg, validateKey);
if (!validated) { if (!validated) {
return void cb("FAILED"); return void cb("FAILED");
} }
@ -720,7 +734,8 @@ const checkDetachedSignature = function (signedMsg, signature, publicKey) {
throw new Error("INVALID_SIGNATURE_LENGTH"); throw new Error("INVALID_SIGNATURE_LENGTH");
} }
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) { //if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
if (Env.crypto.detachedVerify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
throw new Error("FAILED"); throw new Error("FAILED");
} }
}; };

View file

@ -9,6 +9,7 @@ const { fork } = require('child_process');
const Workers = module.exports; const Workers = module.exports;
const PID = process.pid; const PID = process.pid;
const Block = require("../storage/block"); const Block = require("../storage/block");
const Monitoring = require('../monitoring');
const DB_PATH = 'lib/workers/db-worker'; const DB_PATH = 'lib/workers/db-worker';
const MAX_JOBS = 16; const MAX_JOBS = 16;
@ -162,6 +163,13 @@ Workers.initialize = function (Env, config, _cb) {
if (res.log) { if (res.log) {
return void handleLog(res.log, res.label, res.info); return void handleLog(res.log, res.label, res.info);
} }
// handle monitoring data
if (res.monitoring) {
Monitoring.applyToEnv(Env, res.data);
return;
}
// but don't bother handling things addressed to other processes // but don't bother handling things addressed to other processes
// since it's basically guaranteed not to work // since it's basically guaranteed not to work
if (res.pid !== PID) { if (res.pid !== PID) {
@ -226,7 +234,9 @@ Workers.initialize = function (Env, config, _cb) {
handleResponse(state, res); handleResponse(state, res);
}); });
let pid = worker.pid;
var substituteWorker = Util.once(function () { var substituteWorker = Util.once(function () {
Monitoring.remove(Env, pid);
Env.Log.info("SUBSTITUTE_DB_WORKER", ''); Env.Log.info("SUBSTITUTE_DB_WORKER", '');
var idx = workers.indexOf(state); var idx = workers.indexOf(state);
if (idx !== -1) { if (idx !== -1) {
@ -263,9 +273,10 @@ Workers.initialize = function (Env, config, _cb) {
}; };
nThen(function (w) { nThen(function (w) {
var limit = Env.maxWorkers; var limit = Env.maxWorkers || OS.cpus().length;
var logged; var logged;
/*
OS.cpus().forEach(function (cpu, index) { OS.cpus().forEach(function (cpu, index) {
if (limit && index >= limit) { if (limit && index >= limit) {
if (!logged) { if (!logged) {
@ -281,6 +292,14 @@ Workers.initialize = function (Env, config, _cb) {
return void cb(err); return void cb(err);
})); }));
}); });
*/
for (let i = 0; i<limit; i++) {
initWorker(fork(DB_PATH), w(function (err) {
if (!err) { return; }
w.abort();
return void cb(err);
}));
}
}).nThen(function () { }).nThen(function () {
Env.computeIndex = function (Env, channel, cb) { Env.computeIndex = function (Env, channel, cb) {
Env.store.getWeakLock(channel, function (next) { Env.store.getWeakLock(channel, function (next) {

29
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"version": "2024.6.1", "version": "2024.6.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cryptpad", "name": "cryptpad",
"version": "2024.6.1", "version": "2024.6.0",
"license": "AGPL-3.0+", "license": "AGPL-3.0+",
"dependencies": { "dependencies": {
"@mcrowe/minibloom": "^0.2.0", "@mcrowe/minibloom": "^0.2.0",
@ -47,6 +47,7 @@
"open-sans-fontface": "^1.4.0", "open-sans-fontface": "^1.4.0",
"openid-client": "^5.4.2", "openid-client": "^5.4.2",
"pako": "^2.1.0", "pako": "^2.1.0",
"prom-client": "^14.2.0",
"prompt-confirm": "^2.0.4", "prompt-confirm": "^2.0.4",
"pull-stream": "^3.6.1", "pull-stream": "^3.6.1",
"require-css": "0.1.10", "require-css": "0.1.10",
@ -1202,6 +1203,11 @@
"node": ">= 0.6.0" "node": ">= 0.6.0"
} }
}, },
"node_modules/bintrees": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@ -3931,6 +3937,17 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/prom-client": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz",
"integrity": "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==",
"dependencies": {
"tdigest": "^0.1.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prompt-actions": { "node_modules/prompt-actions": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/prompt-actions/-/prompt-actions-3.0.2.tgz", "resolved": "https://registry.npmjs.org/prompt-actions/-/prompt-actions-3.0.2.tgz",
@ -5324,6 +5341,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tdigest": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
"dependencies": {
"bintrees": "1.0.2"
}
},
"node_modules/terminal-paginator": { "node_modules/terminal-paginator": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/terminal-paginator/-/terminal-paginator-2.0.2.tgz", "resolved": "https://registry.npmjs.org/terminal-paginator/-/terminal-paginator-2.0.2.tgz",

View file

@ -1,7 +1,7 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"description": "a collaborative office suite that is end-to-end encrypted and open-source", "description": "a collaborative office suite that is end-to-end encrypted and open-source",
"version": "2024.6.1", "version": "2024.6.0",
"license": "AGPL-3.0+", "license": "AGPL-3.0+",
"repository": { "repository": {
"type": "git", "type": "git",
@ -50,6 +50,7 @@
"open-sans-fontface": "^1.4.0", "open-sans-fontface": "^1.4.0",
"openid-client": "^5.4.2", "openid-client": "^5.4.2",
"pako": "^2.1.0", "pako": "^2.1.0",
"prom-client": "^14.2.0",
"prompt-confirm": "^2.0.4", "prompt-confirm": "^2.0.4",
"pull-stream": "^3.6.1", "pull-stream": "^3.6.1",
"require-css": "0.1.10", "require-css": "0.1.10",

61
scripts/testcrypto.js Normal file
View file

@ -0,0 +1,61 @@
let SodiumNative = require('sodium-native');
let Nacl = require('tweetnacl/nacl-fast');
let LibSodium = require('libsodium-wrappers');
let msgStr = "This is a test";
let keys = Nacl.sign.keyPair();
let pub = keys.publicKey;
let msg = Nacl.util.decodeUTF8(msgStr);
let signedMsg = Nacl.sign(msg, keys.secretKey);
let sig = signedMsg.subarray(0, 64);
LibSodium.ready.then(() => {
/*
console.log('tweetnacl open');
console.log(!!Nacl.sign.open(signedMsg, pub));
console.log('tweetnacl detached');
console.log(Nacl.sign.detached.verify(msg, sig, pub));
console.log('sodium-native open');
console.log(SodiumNative.crypto_sign_open(msg, signedMsg, pub));
console.log('sodium-native detached');
console.log(SodiumNative.crypto_sign_verify_detached(sig, msg, pub));
LibSodium.ready.then(() => {
console.log('libsodium open');
console.log(!!LibSodium.crypto_sign_open(signedMsg, pub));
console.log('libsodium detached');
console.log(LibSodium.crypto_sign_verify_detached(sig, msg, pub));
});
*/
const n = 10000;
let a;
console.log('start sodium-native');
a = +new Date();
for (var i = 0; i < n; i++) {
SodiumNative.crypto_sign_open(msg, signedMsg, pub);
SodiumNative.crypto_sign_verify_detached(sig, msg, pub);
}
console.log('end sodium-native ', (+new Date() - a), ' ms');
console.log('start libsodium');
a = +new Date();
for (var i = 0; i < n; i++) {
LibSodium.crypto_sign_open(signedMsg, pub);
LibSodium.crypto_sign_verify_detached(sig, msg, pub);
}
console.log('end libsodium ', (+new Date() - a), ' ms');
console.log('start tweetnacl');
a = +new Date();
for (var i = 0; i < n; i++) {
Nacl.sign.open(signedMsg, pub);
Nacl.sign.detached.verify(msg, sig, pub);
}
console.log('end tweetnacl ', (+new Date() - a), ' ms');
});

View file

@ -15,6 +15,7 @@ var config = require("./lib/load-config");
var Environment = require("./lib/env"); var Environment = require("./lib/env");
var Env = Environment.create(config); var Env = Environment.create(config);
var Default = require("./lib/defaults"); var Default = require("./lib/defaults");
var Monitoring = require('./lib/monitoring');
var app = Express(); var app = Express();
@ -49,6 +50,11 @@ COMMANDS.GET_PROFILING_DATA = function (msg, cb) {
cb(void 0, Env.bytesWritten); cb(void 0, Env.bytesWritten);
}; };
COMMANDS.MONITORING = function (msg, cb) {
Monitoring.applyToEnv(Env, msg.data);
cb();
};
nThen(function (w) { nThen(function (w) {
require("./lib/log").create(config, w(function (_log) { require("./lib/log").create(config, w(function (_log) {
Env.Log = _log; Env.Log = _log;
@ -93,6 +99,7 @@ nThen(function (w) {
var launchWorker = (online) => { var launchWorker = (online) => {
var worker = Cluster.fork(workerState); var worker = Cluster.fork(workerState);
var pid = worker.process.pid;
worker.on('online', () => { worker.on('online', () => {
online(); online();
}); });
@ -122,6 +129,7 @@ nThen(function (w) {
}); });
worker.on('exit', (code, signal) => { worker.on('exit', (code, signal) => {
Monitoring.remove(Env, pid);
if (!signal && code === 0) { return; } if (!signal && code === 0) { return; }
// relaunch http workers if they crash // relaunch http workers if they crash
Env.Log.error('HTTP_WORKER_EXIT', { Env.Log.error('HTTP_WORKER_EXIT', {
@ -163,6 +171,19 @@ nThen(function (w) {
broadcast('FLUSH_CACHE', Env.FRESH_KEY); broadcast('FLUSH_CACHE', Env.FRESH_KEY);
}, 250); }, 250);
setInterval(() => {
// Add main process data to monitoring
let monitoring = Monitoring.getData('main');
let Server = Env.Server;
let stats = Server.getSessionStats();
monitoring.ws = stats.total;
monitoring.channels = Server.getActiveChannelCount();
monitoring.registered = Object.keys(Env.netfluxUsers).length;
// Send updated values
Monitoring.applyToEnv(Env, monitoring);
broadcast('MONITORING', Env.monitoring);
}, Monitoring.interval);
Env.envUpdated.reg(throttledEnvChange); Env.envUpdated.reg(throttledEnvChange);
Env.cacheFlushed.reg(throttledCacheFlush); Env.cacheFlushed.reg(throttledCacheFlush);

View file

@ -2713,7 +2713,8 @@ define([
}, function (e, arr) { }, function (e, arr) {
pre.innerText = ''; pre.innerText = '';
let data = arr[0]; let data = arr[0];
pre.append(String(data.users)); pre.append(String(data.blocks));
pre.append(' (old value including teams: ' + String(data.users) + ')'); // XXX
}); });
}; };
onRefresh(); onRefresh();

View file

@ -106,7 +106,7 @@ define([
framework._.toolbar.$theme.append($showAuthorColors); framework._.toolbar.$theme.append($showAuthorColors);
markers.setButton($showAuthorColors); markers.setButton($showAuthorColors);
}; };
var mkPrintButton = function (framework, $content, $print) { var mkPrintButton = function (framework, $content) {
var $printButton = framework._.sfCommon.createButton('print', true); var $printButton = framework._.sfCommon.createButton('print', true);
$printButton.click(function () { $printButton.click(function () {
$print.html($content.html()); $print.html($content.html());
@ -115,8 +115,8 @@ define([
framework.feedback('PRINT_CODE'); framework.feedback('PRINT_CODE');
UI.clearTooltipsDelay(); UI.clearTooltipsDelay();
}); });
var $dropdownEntry = UIElements.getEntryFromButton($printButton); var $print = UIElements.getEntryFromButton($printButton);
framework._.toolbar.$drawer.append($dropdownEntry); framework._.toolbar.$drawer.append($print);
}; };
var mkMarkdownTb = function (editor, framework) { var mkMarkdownTb = function (editor, framework) {
var $codeMirrorContainer = $('#cp-app-code-container'); var $codeMirrorContainer = $('#cp-app-code-container');

View file

@ -811,11 +811,19 @@ define([
addTabListener(frame); addTabListener(frame);
listener = listenForKeys(function () { frame.addEventListener('keydown', function(e) {
// Only trigger OK if cancel is not focused if (e.keyCode === 13) {
if (document.activeElement === $cancel[0]) { if (document.activeElement === $ok[0]) {
return void $cancel.click(); $ok.click();
} else if (document.activeElement === $cancel[0]) {
$cancel.click();
}
} else if (e.keyCode === 27) {
$cancel.click();
} }
});
listener = listenForKeys(function () {
$ok.click(); $ok.click();
}, function () { }, function () {
$cancel.click(); $cancel.click();

View file

@ -919,10 +919,7 @@ define([
}; };
const getMyOOIndex = function() { const getMyOOIndex = function() {
const user = findUserByOOId(myOOId); return findUserByOOId(myOOId).index;
return user
? user.index
: content.ids.length; // Assign an unused id to read-only users
}; };
var getParticipants = function () { var getParticipants = function () {
@ -1481,10 +1478,6 @@ define([
send({ type: "message" }); send({ type: "message" });
break; break;
case "saveChanges": case "saveChanges":
if (readOnly) {
return;
}
// If we have unsaved data before reloading for a checkpoint... // If we have unsaved data before reloading for a checkpoint...
if (APP.onStrictSaveChanges) { if (APP.onStrictSaveChanges) {
delete APP.unsavedLocks; delete APP.unsavedLocks;

View file

@ -789,7 +789,8 @@ define([
ownedPads.forEach(function (c) { ownedPads.forEach(function (c) {
var w = waitFor(); var w = waitFor();
sem.take(function (give) { sem.take(function (_give) {
var give = _give();
var otherOwners = false; var otherOwners = false;
nThen(function (_w) { nThen(function (_w) {
// Don't check server metadata for blobs // Don't check server metadata for blobs

View file

@ -1055,7 +1055,7 @@ define([
var removeClient = function (ctx, cId) { var removeClient = function (ctx, cId) {
var idx = ctx.clients.indexOf(cId); var idx = ctx.clients.indexOf(cId);
if (idx !== -1) { ctx.clients.splice(idx, 1); } ctx.clients.splice(idx, 1);
Object.keys(ctx.calendars).forEach(function (id) { Object.keys(ctx.calendars).forEach(function (id) {
var cal = ctx.calendars[id]; var cal = ctx.calendars[id];

View file

@ -426,25 +426,5 @@
"settings_logoutEverywhereConfirm": "Сигурен ли си? Ще трябва да влезеш на всичките си устройства.", "settings_logoutEverywhereConfirm": "Сигурен ли си? Ще трябва да влезеш на всичките си устройства.",
"settings_driveDuplicateHint": "Когато преместите вашите документи в споделена папка, копие от тях се запазва във вашия CryptDrive, за да се гарантира, контрола ви върху него. Можете да скриете дублираните файлове. Само споделената версия ще бъде видима, освен ако не бъде изтрита, в този случай оригиналът ще се покаже на предишното си място.", "settings_driveDuplicateHint": "Когато преместите вашите документи в споделена папка, копие от тях се запазва във вашия CryptDrive, за да се гарантира, контрола ви върху него. Можете да скриете дублираните файлове. Само споделената версия ще бъде видима, освен ако не бъде изтрита, в този случай оригиналът ще се покаже на предишното си място.",
"settings_padWidthHint": "Превключване между режим на страница (по подразбиране), който ограничава ширината на текстовия редактор, и използване на цялата ширина на екрана.", "settings_padWidthHint": "Превключване между режим на страница (по подразбиране), който ограничава ширината на текстовия редактор, и използване на цялата ширина на екрана.",
"settings_padSpellcheckHint": "Тази опция ви позволява да активирате проверка на правописа в документи с форматиран текст. Правописните грешки ще бъдат подчертани в червено и ще трябва да задържите клавиша Ctrl или Meta, докато щраквате с десния бутон, за да видите опциите.", "settings_padSpellcheckHint": "Тази опция ви позволява да активирате проверка на правописа в документи с форматиран текст. Правописните грешки ще бъдат подчертани в червено и ще трябва да задържите клавиша Ctrl или Meta, докато щраквате с десния бутон, за да видите опциите."
"settings_padSpellcheckLabel": "Активиране на проверката на правописа в документи с форматиран текст",
"settings_padOpenLinkTitle": "Отваряне на връзки при първо щракване",
"settings_padOpenLinkHint": "С тази възможност можете да отваряте вградени връзки при щракване без изскачащ прозорец за показване",
"settings_padOpenLinkLabel": "Разрешаване на отварянето на директната връзка",
"settings_ownDriveTitle": "Актуализиране на профила",
"settings_ownDriveHint": "По-старите акаунти нямат достъп до най-новите функции поради технически причини. Безплатното обновяване ще активира текущите функции и ще подготви вашия CryptDrive за бъдещи обновявания.",
"settings_ownDriveButton": "Надграждане на профила ви",
"settings_ownDriveConfirm": "Надграждането на вашия акаунт може да отнеме известно време. Ще трябва да влезете отново през всичките си устройства. Сигурен ли сте?",
"settings_ownDrivePending": "Вашият профил се надгражда. Моля, не затваряйте и не презареждайте тази страница, докато процесът не приключи.",
"settings_changePasswordTitle": "Промяна на паролата",
"settings_changePasswordHint": "Променете паролата на профила си. Въведете текущата си парола и потвърдете новата парола, като я въведете два пъти.<br><b>Не можем да възстановим паролата ви, ако я забравите, така че бъдете много внимателни!</b>",
"settings_changePasswordButton": "Промяна на парола",
"settings_changePasswordCurrent": "Текуща парола",
"settings_changePasswordNew": "Нова парола",
"settings_changePasswordNewConfirm": "Потвърждаване на новата парола",
"settings_changePasswordConfirm": "Сигурни ли сте, че искате да промените паролата си? Ще трябва да влезете отново през всичките си устройства.",
"settings_changePasswordPending": "Вашата парола се обновява. Моля, не затваряйте и не презареждайте тази страница, докато процесът не приключи.",
"settings_cursorColorTitle": "Цвят на курсора",
"settings_changePasswordError": "Възникна неочаквана грешка. Ако не можете да влезете или да промените паролата си, свържете се с администраторите на CryptPad.",
"settings_changePasswordNewPasswordSameAsOld": "Вашата нова парола трябва да е различна от настоящата."
} }

View file

@ -607,8 +607,8 @@
"admin_activeSessionsHint": "Anzahl aktiver Websocket-Verbindungen (und verbundener IP-Adressen)", "admin_activeSessionsHint": "Anzahl aktiver Websocket-Verbindungen (und verbundener IP-Adressen)",
"admin_activePadsTitle": "Aktive Dokumente", "admin_activePadsTitle": "Aktive Dokumente",
"admin_activePadsHint": "Anzahl der Dokumente, die gerade angesehen oder bearbeitet werden", "admin_activePadsHint": "Anzahl der Dokumente, die gerade angesehen oder bearbeitet werden",
"admin_registeredTitle": "Nutzer und Team-Drives", "admin_registeredTitle": "Registrierte Nutzer",
"admin_registeredHint": "Anzahl aktiver Drives auf deiner Instanz", "admin_registeredHint": "Anzahl der auf deiner Instanz registrierten Nutzer",
"admin_updateLimitTitle": "Nutzer-Quotas aktualisieren", "admin_updateLimitTitle": "Nutzer-Quotas aktualisieren",
"admin_updateLimitHint": "Das Erzwingen einer Aktualisierung der Speicherbegrenzungen für Nutzer ist jederzeit möglich, aber nur im Fehlerfall notwendig", "admin_updateLimitHint": "Das Erzwingen einer Aktualisierung der Speicherbegrenzungen für Nutzer ist jederzeit möglich, aber nur im Fehlerfall notwendig",
"admin_updateLimitButton": "Quotas aktualisieren", "admin_updateLimitButton": "Quotas aktualisieren",

View file

@ -553,7 +553,7 @@
"password_info": "El documento que intenta abrir no existe o está protegido con una contraseña. Ingrese la contraseña correcta para acceder a su contenido.", "password_info": "El documento que intenta abrir no existe o está protegido con una contraseña. Ingrese la contraseña correcta para acceder a su contenido.",
"creation_newPadModalDescription": "Haz clic en una aplicación para crear un nuevo documento. Tú también puedes presionar <b>Tab</b> para seleccionar la aplicación y presiona <b>Enter</b> para confirmar.", "creation_newPadModalDescription": "Haz clic en una aplicación para crear un nuevo documento. Tú también puedes presionar <b>Tab</b> para seleccionar la aplicación y presiona <b>Enter</b> para confirmar.",
"toolbar_degraded": "Actualmente hay más de {0} editores en este documento. La lista de usuarios y el chat están desactivados para mejorar el rendimiento.", "toolbar_degraded": "Actualmente hay más de {0} editores en este documento. La lista de usuarios y el chat están desactivados para mejorar el rendimiento.",
"oo_lostEdits": "Lamentablemente, las ediciones recientes no guardadas no se pudieron recuperar después de sincronizar contenido nuevo.", "oo_lostEdits": "Lamentablemente, las ediciones recientes no guardadas no se pueden recuperar después de sincronizar el nuevo contenido.",
"properties_passwordError": "Se ha producido un error al intentar cambiar la contraseña. Por favor, inténtelo de nuevo.", "properties_passwordError": "Se ha producido un error al intentar cambiar la contraseña. Por favor, inténtelo de nuevo.",
"properties_passwordSame": "Las nuevas contraseñas deben diferir de la actual.", "properties_passwordSame": "Las nuevas contraseñas deben diferir de la actual.",
"properties_confirmChange": "¿Está seguro? Al cambiar la contraseña se eliminará su historial. Los usuarios que no tengan la nueva contraseña perderán el acceso a este documento", "properties_confirmChange": "¿Está seguro? Al cambiar la contraseña se eliminará su historial. Los usuarios que no tengan la nueva contraseña perderán el acceso a este documento",
@ -863,7 +863,7 @@
"form_open": "Abrir", "form_open": "Abrir",
"form_isPrivate": "Las respuestas son privadas", "form_isPrivate": "Las respuestas son privadas",
"form_isPublic": "Las respuestas son públicas", "form_isPublic": "Las respuestas son públicas",
"form_makePublicWarning": "¿Estás seguro de querer hacer públicas las respuestas a este formulario? Las respuestas pasadas y futuras serán visibles para los participantes. Esta acción no se puede deshacer.", "form_makePublicWarning": "¿Estás seguro/a que quieres hacer las respuestas a este formulario públicas? Las respuestas pasadas y futuras serán visibles por los participantes. Esto no se puede deshacer.",
"form_makePublic": "Publicar respuestas", "form_makePublic": "Publicar respuestas",
"form_invalidQuestion": "Pregunta {0}", "form_invalidQuestion": "Pregunta {0}",
"form_invalidWarning": "Hay errores en algunas respuestas:", "form_invalidWarning": "Hay errores en algunas respuestas:",
@ -875,7 +875,7 @@
"form_backButton": "Atrás", "form_backButton": "Atrás",
"form_viewButton": "Ver", "form_viewButton": "Ver",
"form_answerAnonymous": "Respuesta anónima en {0}", "form_answerAnonymous": "Respuesta anónima en {0}",
"form_showSummary": "Mostrar resumen", "form_showSummary": "Mostrar sumario",
"form_showIndividual": "Mostrar respuestas individuales", "form_showIndividual": "Mostrar respuestas individuales",
"form_editor": "Editor", "form_editor": "Editor",
"form_results_empty": "No hay respuestas", "form_results_empty": "No hay respuestas",
@ -887,49 +887,49 @@
"form_delete": "Eliminar", "form_delete": "Eliminar",
"form_reset": "Reiniciar", "form_reset": "Reiniciar",
"form_submit": "Enviar", "form_submit": "Enviar",
"form_maxOptions": "Máximo {0} respuesta(s)", "form_maxOptions": "respuesta(s) máximas {0}",
"form_description_default": "Tu texto aquí", "form_description_default": "Tu texto aquí",
"form_type_page": "Salto de página", "form_type_page": "Salto de página",
"form_type_md": "Descripción", "form_type_md": "Descripción",
"form_sort_hint": "Por favor arrastre estos ítems desde el más (1) al menos ({0}) preferido.", "form_sort_hint": "Por favor arrastre estos ítems desde el más (1) al menos ({0}) preferido.",
"form_type_sort": "Lista ordenada", "form_type_sort": "Lista ordenada",
"form_type_poll": "Encuesta", "form_type_poll": "Encuesta",
"form_type_multicheck": "Grilla de casillas de verificación", "form_type_multicheck": "Cuadrícula de casillas de verificación",
"form_type_checkbox": "Casilla de verificación", "form_type_checkbox": "Casilla de verificación",
"form_type_multiradio": "Grilla de opciones", "form_type_multiradio": "Escoge la red",
"form_type_radio": "Opción", "form_type_radio": "Opción",
"form_type_textarea": "Párrafo", "form_type_textarea": "Párrafo",
"form_type_input": "Texto", "form_type_input": "Texto",
"form_default": "¿Tu pregunta aquí?", "form_default": "¿Tu pregunta aquí?",
"form_text_number": "Número", "form_text_number": "Número",
"form_text_email": "Correo", "form_text_email": "Correo",
"form_text_url": "Enlace", "form_text_url": "Link",
"form_text_text": "Texto", "form_text_text": "Texto",
"form_textType": "Tipo de texto", "form_textType": "Tipo de texto",
"form_pollYourAnswers": "Tus respuestas", "form_pollYourAnswers": "Tus respuestas",
"form_pollTotal": "Total", "form_pollTotal": "Total",
"form_poll_switch": "Invertir ejes", "form_poll_switch": "Intercambio de ejes",
"form_poll_time": "Hora", "form_poll_time": "Tiempo",
"form_poll_day": "Día", "form_poll_day": "Día",
"form_poll_text": "Texto", "form_poll_text": "Texto",
"form_editType": "Tipo de opción", "form_editType": "Tipo de opción",
"form_editMaxLength": "Máximo de caracteres", "form_editMaxLength": "Máximos carácteres",
"form_editMax": "Máximo de opciones seleccionables", "form_editMax": "Máximas opciones seleccionables",
"form_editBlock": "Editar", "form_editBlock": "Editar",
"form_invalid": "Formulario inválido", "form_invalid": "Formulario inválido",
"share_formView": "Participante", "share_formView": "Participante",
"share_formAuditor": "Auditor", "share_formAuditor": "Auditor/a",
"share_formEdit": "Autor", "share_formEdit": "Autor/a",
"admin_supportPrivButton": "Mostrar llave", "admin_supportPrivButton": "Mostrar llave",
"admin_supportPrivHint": "Muestra la llave privada que otros administradores necesitarán para ver tickets de soporte. Se mostrará un formulario para ingresar esta clave en su panel de administración.", "admin_supportPrivHint": "Muestra la llave privada que otros/as administradores/as necesitarán para ver tickets de soporte. Una forma de ingresar esta llave será mostrada en su panel de administrador/a.",
"admin_supportInitGenerate": "Generar llaves de soporte", "admin_supportInitGenerate": "Generar llaves de soporte",
"admin_supportPrivTitle": "Llave privada del buzón de soporte", "admin_supportPrivTitle": "Llave privada del buzón de soporte",
"admin_emailHint": "Establece el correo de contacto para tu instancia aquí", "admin_emailHint": "Establece el correo de contacto para tu instancia aquí",
"admin_emailTitle": "Correo de contacto del administrador", "admin_emailTitle": "Correo de contacto del administrador/a",
"oo_importBin": "Presiona OK para importar el formato .bin interno de CryptPad.", "oo_importBin": "Presiona OK para importar el formato .bin interno de CryptPad.",
"oo_conversionSupport": "Tu navegador no puede manejar la conversión hacia y desde formatos de office. Sugerimos usar una versión reciente de Firefox o Chrome.", "oo_conversionSupport": "Tu navegador no puede manejar la conversión hacia y desde formatos de office. Sugerimos usar una versión reciente de Firefox o Chrome.",
"register_registrationIsClosed": "La registración está cerrada.", "register_registrationIsClosed": "La registración está cerrada.",
"mediatag_defaultImageName": "imagen", "mediatag_defaultImageName": "imágen",
"genericCopySuccess": "Copiado al portapapeles", "genericCopySuccess": "Copiado al portapapeles",
"toolbar_storeInDrive": "Guardar en CryptDrive", "toolbar_storeInDrive": "Guardar en CryptDrive",
"calendar_noNotification": "Ninguno", "calendar_noNotification": "Ninguno",
@ -957,9 +957,9 @@
"calendar_newEvent": "Nuevo evento", "calendar_newEvent": "Nuevo evento",
"calendar_import": "Añadir a mis calendarios", "calendar_import": "Añadir a mis calendarios",
"calendar_errorNoCalendar": "Ningún calendario editable seleccionado", "calendar_errorNoCalendar": "Ningún calendario editable seleccionado",
"calendar_deleteOwned": "Se mantendrá visible para otros usuarios con los que se haya compartido.", "calendar_deleteOwned": "Se mantendrá visible para otros/as usuarios/as con los que se haya compartido.",
"calendar_deleteTeamConfirm": "¿Estás seguro que quieres eliminar este calendario del equipo?", "calendar_deleteTeamConfirm": "¿Estás seguro/a que quieres eliminar este calendario del equipo?",
"calendar_deleteConfirm": "¿Estás seguro que quieres eliminar este calendario de tu cuenta?", "calendar_deleteConfirm": "¿Estás seguro/a que quieres eliminar este calendario de tu cuenta?",
"calendar_today": "Hoy", "calendar_today": "Hoy",
"calendar_month": "Mes", "calendar_month": "Mes",
"calendar_week": "Semana", "calendar_week": "Semana",
@ -972,73 +972,73 @@
"calendar_import_temp": "Importar este calendario", "calendar_import_temp": "Importar este calendario",
"pad_goToAnchor": "Ir al anclaje", "pad_goToAnchor": "Ir al anclaje",
"oo_cantMigrate": "Esta hoja excede el tamaño máximo de subida y es muy grande para ser migrada.", "oo_cantMigrate": "Esta hoja excede el tamaño máximo de subida y es muy grande para ser migrada.",
"footer_roadmap": "Plan de acción", "footer_roadmap": "Hoja de ruta",
"settings_deleteSubscription": "Administrar mi suscripción", "settings_deleteSubscription": "Manejar mi suscripción",
"settings_deleteContinue": "Eliminar mi cuenta", "settings_deleteContinue": "Eliminar mi cuenta",
"settings_deleteWarning": "Advertencia: actualmente estás suscrito a un plan premium (pagado o dado por otro usuario). Por favor cancela tu plan antes de eliminar tu cuenta, de otra forma tendrás que contactar al soporte para poder cancelar tu plan.", "settings_deleteWarning": "Advertencia: actualmente estás suscrito/a a un plan premium (pagado o dado por otro/a usuario/a). Por favor cancela tu plan antes de eliminar tu cuenta ya que no será posible sin contactar a soporte una vez tu cuenta esté eliminada.",
"broadcast_newCustom": "Mensaje de los administradores", "broadcast_newCustom": "Mensaje de los/as administradores/as",
"broadcast_preview": "Previsualizar notificación", "broadcast_preview": "Previsualizar notificación",
"broadcast_defaultLanguage": "Volver a este idioma", "broadcast_defaultLanguage": "Volver a este idioma",
"broadcast_translations": "Traducciones", "broadcast_translations": "Traducciones",
"admin_broadcastCancel": "Eliminar mensaje", "admin_broadcastCancel": "Eliminar mensaje",
"admin_broadcastActive": "Mensaje activo", "admin_broadcastActive": "Mensaje activo",
"admin_broadcastButton": "Enviar", "admin_broadcastButton": "Envíar",
"admin_broadcastHint": "Envía un mensaje a todos los usuarios en esta instancia. Todos los usuarios nuevos y existentes lo recibirán como una notificación. Previsualiza los mensajes antes de enviarlos con \"Previsualizar notificación\". Las notificaciones de previsualización tienen un ícono rojo y son visibles sólo para ti.", "admin_broadcastHint": "Envía un mensaje a todos/as los/as usuarios/as en esta instancia. Todos/as los/as usuarios/as nuevos/as y existentes lo recibirán como una notificación. Previsualiza los mensajes antes de enviarlos con \"Previsualizar notificación\". Las notificaciones de previsualización tienen un ícono rojo y son visibles sólo para ti.",
"admin_broadcastTitle": "Mensaje de difusión", "admin_broadcastTitle": "Mensaje de difusión",
"broadcast_surveyURL": "Enlace del cuestionario", "broadcast_surveyURL": "Link del cuestionario",
"admin_surveyActive": "Abrir cuestionario", "admin_surveyActive": "Abrir cuestionario",
"admin_surveyCancel": "Eliminar", "admin_surveyCancel": "Eliminar",
"admin_surveyButton": "Guardar cuestionario", "admin_surveyButton": "Guardar cuestionario",
"broadcast_newSurvey": "Un nuevo cuestionario está disponible. Presiona para abrir.", "broadcast_newSurvey": "Un nuevo cuestionario está disponible. Presiona para abrir.",
"admin_surveyHint": "Añade, actualiza o borra un enlace a un cuestionario externo. Los usuarios recibirán una notificación y el cuestionario se mantendrá disponible desde el menú del usuario.", "admin_surveyHint": "Añade, actualiza o borra un link a un cuestionario externo. Los/as usuarios/as recibirán una notificación y el cuestionario se mantendrá disponible desde el menú de usuario/a.",
"admin_surveyTitle": "Cuestionario", "admin_surveyTitle": "Cuestionario",
"broadcast_maintenance": "Un mantenimiento está planeado entre las <b>{0}</b> y <b>{1}</b>. CryptPad puede que no esté disponible durante ese tiempo.", "broadcast_maintenance": "Un mantenimiento está planeado entre las <b>{0}</b> y <b>{1}</b>. CryptPad puede no estar disponible durante ese tiempo.",
"broadcast_end": "Terminar", "broadcast_end": "Terminar",
"broadcast_start": "Empezar", "broadcast_start": "Empezar",
"admin_maintenanceCancel": "Cancelar mantenimiento", "admin_maintenanceCancel": "Cancelar mantenimiento",
"admin_maintenanceButton": "Planear mantenimiento", "admin_maintenanceButton": "Planear mantenimiento",
"admin_maintenanceHint": "Planear un mantenimiento en esta instancia y notificar a todos los usuarios. Limitado a un mantenimiento activo en un momento dado.", "admin_maintenanceHint": "Planear un mantenimiento en esta instancia y notificar a todos/as los/as usuarios/as. Limitado a un mantenimiento activo en un momento dado.",
"admin_maintenanceTitle": "Mantenimiento", "admin_maintenanceTitle": "Mantenimiento",
"admin_cat_broadcast": "Difusión", "admin_cat_broadcast": "Difusión",
"fm_cantUploadHere": "No se puede subir un archivo aquí", "fm_cantUploadHere": "No se puede subir un archivo aquí",
"importError": "Error al importar (formato incorrecto)", "importError": "Fallo al importar (formato incorrecto)",
"addOptionalPassword": "Añadir una contraseña (opcional)", "addOptionalPassword": "Añadir una contraseña (opcional)",
"settings_colortheme_custom": "Personalizar", "settings_colortheme_custom": "Personalizado",
"pad_settings_show": "Mostrar", "pad_settings_show": "Mostrar",
"pad_settings_hide": "Ocultar", "pad_settings_hide": "Esconder",
"pad_settings_comments": "Escoge si los comentarios deberían ser visibles u oculta por defecto.", "pad_settings_comments": "Escoge si los comentarios deberían ser visibles o escondidos por defecto.",
"pad_settings_outline": "Escoge si la tabla de contenidos debería ser visible u oculta por defecto.", "pad_settings_outline": "Escoge si la tabla de contemidos debería ser visible o escondida por defecto.",
"pad_settings_width_large": "Ancho total", "pad_settings_width_large": "Ancho total",
"pad_settings_width_small": "Modo de página", "pad_settings_width_small": "Modo de página",
"pad_settings_info": "Configuraciones por defecto para este documento. Estos serán aplicados cuando usuarios nuevos visiten este documento.", "pad_settings_info": "Configuraciones por defecto para este documento. Estos serán aplicados cuando usuarios/as nuevos/as visiten este documento.",
"pad_settings_title": "Configuración del documento", "pad_settings_title": "Configuración del documento",
"settings_colorthemeTitle": "Tema de color", "settings_colorthemeTitle": "Tema del color",
"settings_colorthemeHint": "Cambiar los colores de CryptPad en este dispositivo.", "settings_colorthemeHint": "Cambiar los colores en general de CryptPad en este dispositivo.",
"settings_colortheme_default": "Configuración predeterminada del sistema ({0})", "settings_colortheme_default": "Tema del sistema por defecto ({0})",
"settings_colortheme_light": "Claro", "settings_colortheme_light": "Blanco",
"settings_colortheme_dark": "Oscuro", "settings_colortheme_dark": "Negro",
"settings_cat_style": "Apariencia", "settings_cat_style": "Apariencia",
"admin_performancePercentHeading": "Porcentaje", "admin_performancePercentHeading": "Porcentaje",
"admin_performanceTimeHeading": "Tiempo (segundos)", "admin_performanceTimeHeading": "Tiempo (segundos)",
"admin_performanceKeyHeading": "Comando", "admin_performanceKeyHeading": "Comando",
"admin_performanceProfilingTitle": "Rendimiento", "admin_performanceProfilingTitle": "Rendimiento",
"admin_performanceProfilingHint": "Una visión general del tiempo total dedicado a ejecutar varios comandos del lado del servidor", "admin_performanceProfilingHint": "Una visión general del tiempo total gastado ejecutando varios comandos en el lado del servidor",
"admin_cat_performance": "Rendimiento", "admin_cat_performance": "Rendimiento",
"redo": "Rehacer", "redo": "Rehacer",
"undo": "Deshacer", "undo": "Deshacer",
"settings_cacheButton": "Borrar caché existente", "settings_cacheButton": "Limpiar caché existente",
"settings_cacheCheckbox": "Activar caché en este dispositivo", "settings_cacheCheckbox": "Activar caché en este dispositivo",
"settings_cacheHint": "CryptPad guarda partes de tus documentos en la memoria de tu navegador para poder ahorrar conexión y mejorar los tiempos de carga. Puedes desactivar el caché si tu dispositivo no tiene mucho espacio libre. Por razones de seguridad, el caché siempre es borrado cuando cierras sesión, pero puedes borrarlo manualmente si quieres recuperar espacio de almacenamiento en tu dispositivo.", "settings_cacheHint": "CryptPad guarda partes de tus documentos en la memoria de tu navegador para poder ahorrar en uso de la red y mejorar los tiempos de carga. Puedes desactivar el caché si tu dispositivo no tiene mucho espacio libre. Por razones de seguridad, el caché siempre es limpiado cuando cierras sesión, pero puedes limpiarlo manualmente si quieres recuperar espacio de almacenamiento en tu máquina.",
"settings_cacheTitle": "Caché", "settings_cacheTitle": "Caché",
"docs_link": "Documentación", "docs_link": "Documentación",
"creation_helperText": "Abrir en documentación", "creation_helperText": "Abrir en documentación",
"creation_expiresIn": "Destruir en", "creation_expiresIn": "Expira en",
"register_warning_note": "Debido a la naturaleza encriptada de CryptPad, los administradores no podrán recuperar los datos en caso de que olvides tu nombre de usuario y/o contraseña. Por favor, guárdalos en un lugar seguro.", "register_warning_note": "Debido a la naturaleza encriptada de CryptPad, los/as administradores/as de servicio no podrán recuperar los datos en caso de que olvides tu nombre de usuario/a y/o contraseña. Por favor guardalos en un lugar seguro.",
"register_notes": "<ul class=\"cp-notes-list\"><li>Tu contraseña es la llave secreta que encripta todos tus documentos. <span class=\"red\">Si la pierdes no hay forma que podamos recuperar tus datos.</span></li><li>Si estás usando una computadora compartida, <span class=\"red\">recuerda cerrar la sesión</span> cuando termines. Cerrar solo la ventana del navegador dejará tu cuenta expuesta. </li><li>Para quedarte con los documentos que creaste y/o guardaste sin iniciar sesión, marca \"Importar documentos de tu sesión de invitado\".</li></ul>", "register_notes": "<ul class=\"cp-notes-list\"><li>Tu contraseña es la llave secreta que encripta todos tus documentos. <span class=\"red\">Si la pierdes no hay forma que podamos recuperar tus datos.</span></li><li>Si estás usando una computadora compartida, <span class=\"red\">recuerda cerrar la sesión</span> cuando estés listo/a. Tan solo cerrar la ventana del navegador dejará a tu cuenta expuesta. </li><li>Para mantener los documentos que creaste y/o guardaste sin acceder, marca \"Importar documentos de tu sesión de invitado/a\".</li></ul>",
"register_notes_title": "Notas importantes", "register_notes_title": "Notas importantes",
"offlineError": "No se han podido sincronizar los datos más recientes, esta página no puede ser mostrada ahora. La carga continuará cuando tu conexión al servicio sea restaurada.", "offlineError": "No se han podido sincronizar los datos más recientes, esta página no puede ser mostrada ahora. La carga continuará cuando tu conexión al servicio sea restaurada.",
"share_noContactsOffline": "Actualmente estás fuera de línea. Los contactos no están disponibles.", "share_noContactsOffline": "Actualmente estás fuera de línea. Los contactos no están disponibles.",
"access_offline": "Actualmente estás fuera de línea. La administración de acceso no está disponible.", "access_offline": "Actualmente estás fuera de línea. Manejo de acceso no está disponible.",
"admin_support_first": "Creado en: ", "admin_support_first": "Creado en: ",
"admin_support_collapse": "Colapsar", "admin_support_collapse": "Colapsar",
"admin_support_open": "Mostrar", "admin_support_open": "Mostrar",
@ -1046,10 +1046,10 @@
"admin_support_answered": "Tickets respondidos:", "admin_support_answered": "Tickets respondidos:",
"admin_support_normal": "Tickets sin responder:", "admin_support_normal": "Tickets sin responder:",
"admin_support_premium": "Tickets premium:", "admin_support_premium": "Tickets premium:",
"contacts_confirmCancel": "¿Estás seguro que quieres cancelar tu solicitud de contacto con <b>{0}</b>?", "contacts_confirmCancel": "¿Estás seguro/a que quieres cancelar tu solicitud de contacto con <b>{0}</b>?",
"history_trimPrompt": "Este documento ha acumulado {0} de historial que puede ralentizar el tiempo de carga. Considera borrar el historial si no lo necesita.", "history_trimPrompt": "Este documento ha acumulado {0} de historial que puede ralentizar el tiempo de carga. Considere borrar el historial si no es necesario.",
"mediatag_loadButton": "Cargar adjunto", "mediatag_loadButton": "Cargar adjunto",
"settings_mediatagSizeHint": "Tamaño máximo en megabytes (MB) para cargar automáticamente elementos multimedia (imágenes, vídeos, pdf) dentro de los documentos. Elementos más grandes que el tamaño especificado pueden ser cargados manualmente. Usa \"-1\" para cargar siempre los elementos multimedia automáticamente.", "settings_mediatagSizeHint": "Tamaño máximo en megabytes (MB) para automáticamente cargar elementos multimedia (imágenes, vídeos, pdf) incrustados en documentos. Elementos más grandes que el tamaño especificado pueden ser cargados manualmente. Use \"-1\" para siempre cargar los elementos multimedia automáticamente.",
"settings_mediatagSizeTitle": "Límite automático de descarga", "settings_mediatagSizeTitle": "Límite automático de descarga",
"mediatag_notReady": "Por favor complete la descarga", "mediatag_notReady": "Por favor complete la descarga",
"pad_mediatagOpen": "Abrir archivo", "pad_mediatagOpen": "Abrir archivo",
@ -1067,33 +1067,33 @@
"admin_unarchiveButton": "Restaurar", "admin_unarchiveButton": "Restaurar",
"admin_unarchiveHint": "Restaurar un documento que fue previamente archivado", "admin_unarchiveHint": "Restaurar un documento que fue previamente archivado",
"admin_archiveButton": "Archivo", "admin_archiveButton": "Archivo",
"admin_archiveHint": "Poner un documento no disponible sin eliminarlo permanentemente. Será puesto en un directorio 'archivo' y eliminado luego de unos días (se puede configurar en el archivo de configuración del servidor).", "admin_archiveHint": "Hacer un documento no disponible sin eliminarlo permanentemente. Será puesto en un directorio 'archivo' y eliminado luego de unos días (configurable en el archivo de configuración del servidor).",
"errorPopupBlocked": "CryptPad necesita poder abrir nuevas pestañas para operar. Por favor permita las ventanas emergentes en la barra de direcciones de su navegador. Estas ventanas nunca van a ser usadas para mostrar anuncios.", "errorPopupBlocked": "CryptPad necesita poder abrir nuevas pestañas para operar. Por favor permita las ventanas emergentes en la barra de direcciones de su navegador. Estas ventanas nunca van a ser usadas para mostrarle anuncios.",
"unableToDisplay": "No se puede mostrar el documento. Por favor presione Esc para recargar la página. Si el problema persiste, por favor contacte al soporte.", "unableToDisplay": "No se puede mostrar este documento. Por favor presione Esc para recargar la página. Si el problema persiste, por favor contacte a soporte.",
"documentID": "Identificador del documento", "documentID": "Identificador del documento",
"error_unhelpfulScriptError": "Error de script: Vea la consola del navegador para detalles", "error_unhelpfulScriptError": "Error de script: Vea la consola del navegador para los detalles",
"tag_edit": "Editar", "tag_edit": "Editar",
"tag_add": "Añadir", "tag_add": "Añadir",
"loading_state_5": "Reconstruir documento", "loading_state_5": "Reconstruir documento",
"loading_state_4": "Cargar equipos", "loading_state_4": "Cargar equipos",
"loading_state_3": "Cargar carpetas compartidas", "loading_state_3": "Cargar carpetas compartidas",
"loading_state_2": "Actualizar contenido", "loading_state_2": "Actualizar el contenido",
"loading_state_1": "Cargar el disco", "loading_state_1": "Cargar el disco",
"loading_state_0": "Construir interfaz", "loading_state_0": "Construir interfaz",
"fm_shareFolderPassword": "Proteger esta carpeta con una contraseña (opcional)", "fm_shareFolderPassword": "Proteger esta carpeta con una contraseña (opcional)",
"access_destroyPad": "Destruir este documento o carpeta permanentemente", "access_destroyPad": "Destruir este documento o carpeta permanentemente",
"fm_deletedFolder": "Carpeta eliminada", "fm_deletedFolder": "Carpeta eliminada",
"admin_limitUser": "Llave pública del usuario", "admin_limitUser": "Llave pública del usuario/a",
"team_exportButton": "Descargar", "team_exportButton": "Descargar",
"team_exportHint": "Descargar todos los documentos en el disco de este equipo. Los documentos serán descargados en formatos que sean legibles por otras aplicaciones cuando el formato esté disponible. Cuando el formato no esté disponible, los documentos serán descargados en un formato legible por CryptPad.", "team_exportHint": "Descargar todos los documentos en el disco de este equipo. Los documentos serán descargados en formatos que sean legibles para otras aplicaciones cuando cierto formato esté disponible. Cuando cierto formato no esté disponible, los documentos serán descargados en un formato legible por CryptPad.",
"team_exportTitle": "Descargar disco del equipo", "team_exportTitle": "Descargar disco del equipo",
"admin_cat_quota": "Almacenamiento del usuario", "admin_cat_quota": "Almacenamiento del usuario/a",
"admin_invalLimit": "Valor de límite no válido", "admin_invalLimit": "Valor de límite inválido",
"admin_invalKey": "Llave pública inválida", "admin_invalKey": "Llave pública inválida",
"admin_limitSetNote": "Nota", "admin_limitSetNote": "Nota",
"admin_limitMB": "Límite (en MB)", "admin_limitMB": "Límite (en MB)",
"admin_setlimitTitle": "Aplicar un límite personalizado", "admin_setlimitTitle": "Aplicar un límite personalizado",
"admin_setlimitHint": "Establecer límites personalizados para usuarios usando su llave pública. Puedes actualizar o borrar un límite existente.", "admin_setlimitHint": "Establecer límites personalizados para usuarios/as usando su llave pública. Puedes actualizar o borrar un límite existente.",
"admin_limitNote": "Nota: {0}", "admin_limitNote": "Nota: {0}",
"admin_limitPlan": "Plan: {0}", "admin_limitPlan": "Plan: {0}",
"admin_getlimitsTitle": "Límites personalizados", "admin_getlimitsTitle": "Límites personalizados",
@ -1101,13 +1101,13 @@
"admin_limit": "Límite actual: {0}", "admin_limit": "Límite actual: {0}",
"admin_setlimitButton": "Establecer límite", "admin_setlimitButton": "Establecer límite",
"admin_defaultlimitTitle": "Límite de almacenamiento (MB)", "admin_defaultlimitTitle": "Límite de almacenamiento (MB)",
"admin_defaultlimitHint": "Límite de almacenamiento máximo para los CryptDrives (usuarios y equipos) cuando no se aplica ninguna regla", "admin_defaultlimitHint": "Límite de almacenamiento máximo para los CryptDrives (usuarios/as y equipos) cuando no hay una regla personalizada aplicada",
"admin_registrationTitle": "Cerrar registración", "admin_registrationTitle": "Cerrar registración",
"admin_registrationHint": "Los visitantes de la instancia no pueden crear cuentas. Las invitaciones pueden ser creadas por los administradores.", "admin_registrationHint": "No permitir a ningún usuario/a nuevo/a registrarse",
"snapshots_cantMake": "La captura no pudo ser creada. Has sido desconectado.", "snapshots_cantMake": "La instantánea no pudo ser creada. Estás desconectado/a.",
"snapshots_notFound": "Esta captura ya no existe debido a que el historial del documento ha sido borrado.", "snapshots_notFound": "Esta instantánea ya no existe debido a que el historial del documento ha sido borrado.",
"snapshot_error_exists": "Ya hay una captura de esta versión", "snapshot_error_exists": "Ya hay una instantánea de esta versión",
"snapshots_ooPickVersion": "Debes seleccionar una versión antes de crear una captura", "snapshots_ooPickVersion": "Debes seleccionar una versión antes de crear una instantánea",
"oo_version": "Versión: ", "oo_version": "Versión: ",
"oo_version_latest": "Último", "oo_version_latest": "Último",
"snapshots_delete": "Eliminar", "snapshots_delete": "Eliminar",
@ -1115,50 +1115,50 @@
"snapshots_close": "Cerrar", "snapshots_close": "Cerrar",
"snapshots_restore": "Restaurar", "snapshots_restore": "Restaurar",
"snapshots_open": "Abrir", "snapshots_open": "Abrir",
"snapshots_placeholder": "Título de la captura", "snapshots_placeholder": "Título de la instantánea",
"snapshots_new": "Nueva captura", "snapshots_new": "Nueva Instantánea",
"snapshots_button": "Capturas", "snapshots_button": "Instantáneas",
"snaphot_title": "Captura", "snaphot_title": "Instantánea",
"infobar_versionHash": "Actualmente estás viendo una versión antigua de este documento ({0}).", "infobar_versionHash": "Actualmente estás viendo una versión pasada de este documento ({0}).",
"history_restoreDriveDone": "CryptDrive restaurado", "history_restoreDriveDone": "CryptDrive restaurado",
"history_restoreDrivePrompt": "¿Estás seguro de que quieres reemplazar la versión actual del CryptDrive con la versión mostrada?", "history_restoreDrivePrompt": "¿Estás seguro/a de que quieres reemplazar la versión actual del CryptDrive con la versión mostrada?",
"history_restoreDriveTitle": "Restaurar la versión seleccionada del CryptDrive", "history_restoreDriveTitle": "Restaurar la versión seleccionada del CryptDrive",
"history_userNext": "Siguiente autor", "history_userNext": "Siguiente autor/a",
"history_fastNext": "Siguiente sesión de edición", "history_fastNext": "Siguiente sesión de edición",
"history_userPrev": "Autor previo", "history_userPrev": "Autor/a previo/a",
"history_fastPrev": "Sesión de edición previa", "history_fastPrev": "Sesión de edición previa",
"share_versionHash": "Estás a punto de compartir la versión del historial seleccionada del documento en el modo de solo lectura. Esto también <b>dará acceso de visión</b> para todas las versiones del documento.", "share_versionHash": "Estás a punto de compartir la versión del historial seleccionado del documento en el modo de solo lectura. Esto también <b>dará acceso de visión</b> para todas las versiones del documento.",
"history_shareTitle": "Compartir un enlace a esta versión", "history_shareTitle": "Compartir un link a esta versión",
"history_cantRestore": "Restauración fallida. Desconectado.", "history_cantRestore": "Restauración fallida. Estás desconectado/a.",
"history_close": "Cerrar", "history_close": "Cerrar",
"history_restore": "Restaurar", "history_restore": "Restaurar",
"share_bar": "Crear enlace", "share_bar": "Crear link",
"settings_cat_kanban": "Kanban", "settings_cat_kanban": "Kanban",
"settings_kanbanTagsOr": "O", "settings_kanbanTagsOr": "O",
"settings_kanbanTagsAnd": "Y", "settings_kanbanTagsAnd": "Y",
"settings_kanbanTagsHint": "Selecciona como quieres que actúe el filtro de etiquetas cuando se seleccionen múltiples etiquetas: mostrar solo tarjetas que contienen todas las etiquetas seleccionadas (Y) o mostrar tarjetas que contienen cualquiera de las etiquetas seleccionadas (O)", "settings_kanbanTagsHint": "Selecciona como quieres que actúe el filtro de etiquetas cuando se seleccionen múltiples etiquetas: solo mostrar tarjetas conteniendo todas las etiquetas seleccionadas (Y) o mostrar tarjetas conteniendo cualquiera de las etiquetas seleccionadas (O)",
"settings_kanbanTagsTitle": "Filtro de etiquetas", "settings_kanbanTagsTitle": "Filtro de etiquetas",
"pad_tocHide": "Resumen", "pad_tocHide": "Esquema",
"fm_noResult": "No se encontraron resultados", "fm_noResult": "No se encontraron resultados",
"fm_restricted": "No tienes acceso", "fm_restricted": "No tienes acceso",
"fm_emptyTrashOwned": "Tu basurero contiene documentos que te pertenecen. Puedes <b>borrarlos</b> solamente de tu disco, o <b>destruirlos</b> para todos los usuarios.", "fm_emptyTrashOwned": "Tu basurero contiene documentos que te pertenecen. Puedes <b>borrarlos</b> solamente de tu disco, o <b>detruirlos</b> para todos/as los/as usuarios/as.",
"support_formCategoryError": "Error: la categoría está vacía", "support_formCategoryError": "Error: la categoría está vacía",
"support_category": "Escoge una categoría", "support_category": "Escoge una categoría",
"oo_refresh": "Refrescar", "oo_refresh": "Refrescar",
"notification_folderSharedTeam": "{0} ha compartido una carpeta con el equipo {2}: <b>{1}</b>", "notification_folderSharedTeam": "{0} ha compartido una carpet con el equipo {2}: <b>{1}</b>",
"notification_fileSharedTeam": "{0} ha compartido un archivo con el equipo {2}: <b>{1}</b>", "notification_fileSharedTeam": "{0} ha compartido un archivo con el equipo {2}: <b>{1}</b>",
"notification_padSharedTeam": "{0} ha compartido un documento con el equipo {2}: <b>{1}</b>", "notification_padSharedTeam": "{0} ha compartido un documento con el equipo {2}: <b>{1}</b>",
"support_addAttachment": "Añadir adjunto", "support_addAttachment": "Añadir adjunto",
"support_attachments": "Adjuntos", "support_attachments": "Adjuntos",
"support_cat_all": "Todo", "support_cat_all": "Todo",
"support_cat_other": "Otros", "support_cat_other": "Otros",
"support_cat_bug": "Reporte de errores", "support_cat_bug": "Reporte de fallos",
"support_cat_data": "Pérdida de contenido", "support_cat_data": "Pérdida de contenido",
"support_cat_account": "Cuenta de usuario", "support_cat_account": "Cuenta de usuario/a",
"info_privacyFlavour": "<a>Política de privacidad</a> para esta instancia", "info_privacyFlavour": "<a>Política de privacidad</a> para esta instancia",
"user_about": "Acerca de CryptPad", "user_about": "Acerca de CryptPad",
"info_imprintFlavour": "<a>Información legal</a> acerca de los administradores de esta instancia", "info_imprintFlavour": "<a>Información legal</a> acerca de los/as administradores/as de esta instancia",
"settings_safeLinkDefault": "Los links seguros ahora están activados por defecto. Por favor usa <i></i><b>Compartir</b> menu para compartir links en lugar de la barra de direcciones de tu navegador.", "settings_safeLinkDefault": "Los links seguros ahora están activados por defecto. Por favor usa el menú <i></i><b>Compartir</b> para compartir links en lugar de la barra de direcciones de tu navegador.",
"support_languagesPreamble": "El equipo de soporte habla los siguientes idiomas:", "support_languagesPreamble": "El equipo de soporte habla los siguientes idiomas:",
"slide_textCol": "Color del texto", "slide_textCol": "Color del texto",
"slide_backCol": "Color de fondo", "slide_backCol": "Color de fondo",
@ -1169,20 +1169,20 @@
"toolbar_savetodrive": "Guardar como imagen", "toolbar_savetodrive": "Guardar como imagen",
"toolbar_insert": "Insertar", "toolbar_insert": "Insertar",
"toolbar_theme": "Tema", "toolbar_theme": "Tema",
"todo_move": "Tu lista de cosas por hacer ahora está en el kanban <b>{0}</b> en tu disco.", "todo_move": "Tu lista de cosas por hacer ahora está en el kanban <b>{0}</b> en tu Disco.",
"fm_sort": "Ordenar", "fm_sort": "Ordenar",
"comments_error": "No se puede añadir comentario aquí", "comments_error": "No se puede comentar aquí",
"settings_padNotifCheckbox": "Desactivar las notificaciones de comentarios", "settings_padNotifCheckbox": "Desactivar las notificaciones de comentarios",
"settings_padNotifHint": "Ignorar notificaciones cuando alguien responda a uno de tus comentarios", "settings_padNotifHint": "Ignorar las notificaciones cuando alguien responda a uno de tus comentarios",
"comments_comment": "Comentar", "comments_comment": "Comentar",
"comments_resolve": "Resolver", "comments_resolve": "Resolver",
"comments_reply": "Responder", "comments_reply": "Responder",
"comments_submit": "Enviar", "comments_submit": "Envíar",
"comments_edited": "Editado", "comments_edited": "Editado",
"comments_deleted": "Comentario eliminado por su autor", "comments_deleted": "Comentario eliminado por su autor/a",
"mentions_notification": "{0} te ha mencionado en <b>{1}</b>", "mentions_notification": "{0} te ha mencionado en <b>{1}</b>",
"unknownPad": "Documento desconocido", "unknownPad": "Documento desconocido",
"comments_notification": "Respuestas a tu comentario \"{0}\" en <b>{1}</b>", "comments_notification": "Responde a tu comentario \"{0}\" en <b>{1}</b>",
"cba_title": "Colores del autor", "cba_title": "Colores del autor",
"oo_login": "Por favor accede o regístrate para mejorar el rendimiento de las hojas de cálculo.", "oo_login": "Por favor accede o regístrate para mejorar el rendimiento de las hojas de cálculo.",
"cba_hide": "Esconder los colores del autor", "cba_hide": "Esconder los colores del autor",
@ -1209,9 +1209,9 @@
"kanban_body": "Contenido", "kanban_body": "Contenido",
"kanban_title": "Título", "kanban_title": "Título",
"teams": "Equipos", "teams": "Equipos",
"allow_text": "Usar una lista de acceso significa que solo los usuarios seleccionados y propietarios podrán acceder a este documento.", "allow_text": "Usar una lista de acceso significa que solo los/as usuarios/as seleccionados/as y propietarios/as podrán acceder a este documento.",
"logoutEverywhere": "Cerrar sesión en todos lados", "logoutEverywhere": "Cerrar sesión en todos lados",
"owner_text": "El o los propietarios de un documento son los únicos usuarios autorizados a: añadir o quitar propietarios, restringir acceso al documento con una lista de acceso, o eliminar el documento.", "owner_text": "El/los/as propietario/a/s de un documento son los/as únicos/as usuarios/as autorizados/as para: añadir/expulsar propietarios/as, restringir acceso al documento con una lista de acceso, o eliminar el documento.",
"access_muteRequests": "Silenciar solicitudes de acceso para este documento", "access_muteRequests": "Silenciar solicitudes de acceso para este documento",
"allow_label": "Lista de acceso: {0}", "allow_label": "Lista de acceso: {0}",
"allow_disabled": "desactivado", "allow_disabled": "desactivado",
@ -1249,32 +1249,32 @@
"oo_sheetMigration_complete": "Versión actualizada disponible, presione OK para recargar.", "oo_sheetMigration_complete": "Versión actualizada disponible, presione OK para recargar.",
"admin_consentToContactTitle": "Consentir a contacto", "admin_consentToContactTitle": "Consentir a contacto",
"admin_checkupButton": "Ejecutar diagnóstico", "admin_checkupButton": "Ejecutar diagnóstico",
"admin_checkupHint": "CryptPad incluye una página que diagnostica automáticamente problemas comunes de configuración y sugiere cómo corregirlos si es necesario.", "admin_checkupHint": "CryptoPad incluye una página que automáticamente diagnostica errores comunes y sugiere cómo corregirlos si fuera necesario.",
"admin_checkupTitle": "Validar la configuración de la instancia", "admin_checkupTitle": "Valide la configuración de la instancia",
"admin_updateAvailableButton": "Ver notas de la versión", "admin_updateAvailableButton": "Ver las notas de lanzamiento",
"admin_updateAvailableHint": "Una nueva versión de CryptPad está disponible", "admin_updateAvailableHint": "Una versión nueva de CryptPad está disponible",
"admin_updateAvailableTitle": "Nuevas versiones", "admin_updateAvailableTitle": "Nuevas versiones",
"admin_cat_network": "Red", "admin_cat_network": "Red",
"mdToolbar_embed": "Incrustar archivo", "mdToolbar_embed": "Incrustar archivo",
"restrictedLoginPrompt": "No estás autorizado a acceder a este documento. <a>Accede</a> si crees que tu cuenta debería tener acceso.", "restrictedLoginPrompt": "No estás autorizado/a a acceder a este documento. <a>Accede</a> si crees que tu cuenta debería poder accederlo.",
"copyToClipboard": "Copiar al portapapeles", "copyToClipboard": "Copiar al portapapeles",
"settings_driveRedirect": "Redirígeme automáticamente", "settings_driveRedirect": "Redirigirme automáticamente",
"settings_driveRedirectHint": "La redirección automática desde la página de inicio al drive cuando se inicia sesión ya no está habilitada por defecto. El comportamiento anterior puede habilitarse a continuación.", "settings_driveRedirectHint": "La redirección automática desde la página de inicio al disco cuando ya accedió ya no está activada por defecto. El comportamiento de legado puede ser activado abajo.",
"settings_driveRedirectTitle": "Redirección a la página de inicio", "settings_driveRedirectTitle": "Redirección a la página de inicio",
"form_page": "Página {0}/{1}", "form_page": "Página {0}/{1}",
"form_clear": "Limpiar", "form_clear": "Limpiar",
"form_addMultipleHint": "Añadir múltiples fechas y horas", "form_addMultipleHint": "Añadir múltiples fechas y tiempos",
"form_addMultiple": "Añadir todo", "form_addMultiple": "Añadir todo",
"form_anonymous_blocked": "Las respuestas de invitados están bloqueadas para este formulario. Debes <a href=\"/login/\">iniciar sesión</a> o <a href=\"/register/\">registrarte</a> para enviar respuestas.", "form_anonymous_blocked": "Las respuestas de invitados/a están bloqueadas para este formulario. Debes <a href=\"/login/\">acceder</a> o <a href=\"/register/\">registrarte</a> para enviar respuestas.",
"form_add_item": "Añadir elemento", "form_add_item": "Añadir ítem",
"form_add_option": "Añadir opción", "form_add_option": "Añadir opción",
"form_newItem": "Nuevo elemento", "form_newItem": "Nuevo ítem",
"form_newOption": "Nueva opción", "form_newOption": "Nueva opción",
"form_defaultItem": "Elemento {0}", "form_defaultItem": "Ítem {0}",
"form_defaultOption": "Opción {0}", "form_defaultOption": "Opción {0}",
"form_anonymous_off": "Bloqueado", "form_anonymous_off": "Bloqueado",
"form_anonymous_on": "Permitido", "form_anonymous_on": "Permitido",
"form_anonymous": "Acceso de invitado (sin sesión iniciada)", "form_anonymous": "Acceso de invitado/a (sin acceder)",
"oo_sheetMigration_loading": "Actualizando tu documento a la última versión. Porfavor espera aproximadamente 1 minuto.", "oo_sheetMigration_loading": "Actualizando tu documento a la última versión. Porfavor espera aproximadamente 1 minuto.",
"oo_exportInProgress": "Exportación en progreso", "oo_exportInProgress": "Exportación en progreso",
"oo_importInProgress": "Importación en progreso", "oo_importInProgress": "Importación en progreso",
@ -1453,50 +1453,50 @@
"toolbar_expand": "Expandir barra de herramientas", "toolbar_expand": "Expandir barra de herramientas",
"toolbar_collapse": "Colapsar barra de herramientas", "toolbar_collapse": "Colapsar barra de herramientas",
"support_premiumLink": "Ver opciones de suscripción", "support_premiumLink": "Ver opciones de suscripción",
"support_premiumPriority": "Los usuarios premium ayudan a respaldar las mejoras en la usabilidad de CryptPad y se benefician de respuestas prioritarias a sus solicitudes de soporte.", "support_premiumPriority": "Los/as usuarios/as premium ayudan a financiar mejoras a la usabilidad de CryptPad y se benefician de respuestas priorizadas a sus tickets de soporte.",
"form_totalResponses": "Total de respuestas: {0}", "form_totalResponses": "Respuestas totales: {0}",
"ui_expand": "Expandir", "ui_expand": "Expandir",
"ui_collapse": "Colapsar", "ui_collapse": "Colapsar",
"fm_link_invalid": "URL no válida", "fm_link_invalid": "URL inválida",
"fm_link_warning": "Advertencia: la URL excede los 200 caracteres", "fm_link_warning": "Advertencia: la URL excede los 200 carácteres",
"form_anonName": "Tu nombre", "form_anonName": "Tu nombre",
"notification_linkShared": "{0} ha compartido un enlace contigo: <b>{1}</b>", "notification_linkShared": "{0} ha compartido un link contigo: <b>{1}</b>",
"fm_link_name_placeholder": "Mi enlace", "fm_link_name_placeholder": "Mí link",
"fm_link_url": "URL", "fm_link_url": "URL",
"fm_link_name": "Nombre del Enlace", "fm_link_name": "Nombre del link",
"fm_link_type": "Enlace", "fm_link_type": "Link",
"fm_link_new": "Nuevo enlace", "fm_link_new": "Nuevo link",
"notification_openLink": "Has recibido un enlace <b>{0}</b> de {1}:", "notification_openLink": "Has recibido un link <b>{0}</b> de {1}:",
"form_exportCSV": "Exportar a CSV", "form_exportCSV": "Exportar a CSV",
"team_leaveOwner": "Por favor, reduce privilegios de propietario antes de salir del equipo. Ten en cuenta que los equipos deben tener al menos un propietario; añade uno antes de proceder si actualmente eres el único.", "team_leaveOwner": "Por favor degradate del rol de propietario/a antes de dejar el equipo. Ten en cuenta que los equipos deben tener al menos un/a propietario/a, por favor añade uno/a antes de proceder si actualmente eres el único/a propietario/a.",
"admin_instancePurposeHint": "¿Porqué usas esta instancia? Tu respuesta será usada para ayudar al plan de desarrollo si la telemetría está activada.", "admin_instancePurposeHint": "¿Porqué diriges esta instancia? Tu respuesta será usada para informar al plan de desarrollo si su telemetría esta activada.",
"admin_purpose_business": "Para una empresa u organización comercial", "admin_purpose_business": "Para una empresa u organización comercial",
"admin_purpose_public": "Para proveer un servicio gratuito al público", "admin_purpose_public": "Para proveer un servicio gratuito al público",
"admin_purpose_education": "Para una escuela o universidad", "admin_purpose_education": "Para una escuela, colegio o universidad",
"admin_purpose_org": "Para una organización sin fines de lucro o grupo de defensa", "admin_purpose_org": "Para una organización sin ánimos de lucro o un grupo de defensa",
"admin_purpose_personal": "Para mí, familia, o amigos", "admin_purpose_personal": "Para mí, familia, o amigos/as",
"admin_purpose_experiment": "Para probar la plataforma o desarrollar nuevas funciones", "admin_purpose_experiment": "Para probar la plataforma o desarrollar nuevas funciones",
"admin_purpose_noanswer": "Prefiero no decirlo", "admin_purpose_noanswer": "Prefiero no decirlo",
"admin_instancePurposeTitle": "Propósito de la instancia", "admin_instancePurposeTitle": "Propósito de la instancia",
"resources_learnWhy": "Entérate por qué fue bloqueada", "resources_learnWhy": "Conoce por qué fue bloqueado",
"resources_openInNewTab": "Abrir en una nueva pestaña", "resources_openInNewTab": "Abrirlo en una nueva pestaña",
"resources_imageBlocked": "CryptPad bloqueó una imagen remota", "resources_imageBlocked": "CryptPad bloqueó una imágen remota",
"fc_open_formro": "Abrir (como participante)", "fc_open_formro": "Abrir (como participante)",
"form_poll_hint": "<i></i>: Si, <i></i>: No, <i></i>: Aceptar", "form_poll_hint": "<i></i>: Si, <i></i>: No, <i></i>: Aceptable",
"admin_provideAggregateStatisticsLabel": "Proveer estadísticas adicionales", "admin_provideAggregateStatisticsLabel": "Proveer estadísticas adicionales",
"admin_provideAggregateStatisticsHint": "Puedes optar por incluir mediciones de uso adicionales para los desarrolladores, tal como el número aproximado de usuarios diarios y registrados en tu instancia.", "admin_provideAggregateStatisticsHint": "Puedes optar por incluir mediciones de uso adicionales para los/as desarrolladores/as, tal como el número aproximado de usuarios/as diarios y registrados/as para tu instancia.",
"admin_provideAggregateStatisticsTitle": "Estadísticas adicionales", "admin_provideAggregateStatisticsTitle": "Agregación estadística",
"admin_blockDailyCheckLabel": "Desactivar telemetría del servidor", "admin_blockDailyCheckLabel": "Desactivar telemetría del servidor",
"admin_blockDailyCheckHint": "Las instancias de CryptPad envían un mensaje al servidor de los desarrolladores una vez al día al iniciarse. Esto les permite llevar un registro de cuántos servidores se están ejecutando y qué versiones del software. Puedes optar por no participar en esta medición a continuación. El contenido de este mensaje se puede encontrar en el registro del servidor de la aplicación para tu revisión.", "admin_blockDailyCheckHint": "Las instancias de CryptPad envian un mensaje al servidor de los/as desarrolladores/as cuando son lanzadas y una vez por día a partir de entonces. Esto les permite mantenerse al tanto de cuantos servidores están funcionando con que versiones del software. Puedes salirte de esta medición abajo. Los contenidos de este mensaje pueden ser encontrados en el registro de la aplicación del servidor para tu revisión.",
"admin_blockDailyCheckTitle": "Telemetría del servidor", "admin_blockDailyCheckTitle": "Telemetría del servidor",
"admin_removeDonateButtonLabel": "No promocionar campañas de crowdfunding", "admin_removeDonateButtonLabel": "No publicar las campañas de financiación colectiva",
"admin_removeDonateButtonHint": "El desarrollo de CryptPad está parcialmente financiado por subvenciones públicas y donaciones. Publicitar nuestros esfuerzos de crowdfunding en tu instancia ayuda a los desarrolladores a continuar mejorando la plataforma para todos, pero puedes desactivar estos avisos si los consideras inapropiados.", "admin_removeDonateButtonHint": "El desarrollo de CryptPad es financiado parcialmente por subvenciones públicas y donaciones. Publicar nuestros esfuerzos de financiación colectiva en tu instancia ayuda a los/as desarrolladores/as a continuar mejorando la plataforma para todos/as, pero puedes desactivar estos abisos si los encuentras inapropiados.",
"admin_removeDonateButtonTitle": "Participación Crowdfunding", "admin_removeDonateButtonTitle": "Participación en la financiación colectiva",
"admin_listMyInstanceLabel": "Listar esta instancia", "admin_listMyInstanceLabel": "Listar esta instancia",
"admin_listMyInstanceHint": "Si tu instancia es adecuada para el uso público puedes aceptar que sea enlistada en los directorios de la red. La telemetría del servidor debe estar activada para que esto tenga efecto.", "admin_listMyInstanceHint": "Si tu instancia es adecuada para el uso público puedes consentir a que sea enlistada en los directorios de la red. La telemetría del servidor debe estar activada para que esto tenga algún efecto.",
"admin_listMyInstanceTitle": "Listar mi instancia en directorios públicos", "admin_listMyInstanceTitle": "Listar mi instancia en los directorios públicos",
"admin_consentToContactLabel": "Acepto", "admin_consentToContactLabel": "Consiento",
"admin_consentToContactHint": "La telemetría del servidor incluye el correo electrónico de contacto del administrador para que los desarrolladores puedan notificarte sobre problemas graves con el software o la configuración. Nunca será compartido, vendido ni utilizado con fines de marketing. Acepta para ser contactado si deseas ser informado de problemas críticos en tu servidor.", "admin_consentToContactHint": "La telemetría del servidor incluye el correo de contacto del administrador/a para que así los/as desarrolladores/as puedan notificarte de problemas serios con el software o tu configuración. Nunca será compartido, vendido, o usado por razones de marketing. Consiente al contacto si te gustaría estar informado/a de problemas críticos en tu servidor.",
"calendar_nth_5": "quinto", "calendar_nth_5": "quinto",
"calendar_nth_last": "último", "calendar_nth_last": "último",
"calendar_rec_monthly_nth": "Cada {0} {1} del mes", "calendar_rec_monthly_nth": "Cada {0} {1} del mes",

File diff suppressed because it is too large Load diff

View file

@ -609,8 +609,8 @@
"admin_activeSessionsHint": "Nombre de connexions websocket actives (et adresses IP uniques connectées)", "admin_activeSessionsHint": "Nombre de connexions websocket actives (et adresses IP uniques connectées)",
"admin_activePadsTitle": "Documents actifs", "admin_activePadsTitle": "Documents actifs",
"admin_activePadsHint": "Nombre de documents uniques actuellement ouverts (lus ou modifiés)", "admin_activePadsHint": "Nombre de documents uniques actuellement ouverts (lus ou modifiés)",
"admin_registeredTitle": "Drives utilisateur·ices et équipes", "admin_registeredTitle": "Utilisateur·ices enregistré·es",
"admin_registeredHint": "Nombre de drives actifs sur votre instance", "admin_registeredHint": "Nombre d'utilisateur·ices enregistré·es sur votre instance",
"admin_updateLimitTitle": "Mettre à jour les quotas", "admin_updateLimitTitle": "Mettre à jour les quotas",
"admin_updateLimitHint": "Forcer la mise à jour des limites de stockage des utilisateur·ices peut être effectué à tout moment, mais cela n'est utile que lorsqu'une erreur survient", "admin_updateLimitHint": "Forcer la mise à jour des limites de stockage des utilisateur·ices peut être effectué à tout moment, mais cela n'est utile que lorsqu'une erreur survient",
"admin_updateLimitButton": "Mettre à jour les quotas", "admin_updateLimitButton": "Mettre à jour les quotas",
@ -1249,7 +1249,7 @@
"form_makePublic": "Publier les réponses", "form_makePublic": "Publier les réponses",
"form_invalidQuestion": "Questions {0}", "form_invalidQuestion": "Questions {0}",
"form_invalidWarning": "Certaines réponses contiennent des erreurs :", "form_invalidWarning": "Certaines réponses contiennent des erreurs :",
"form_input_ph_url": "https://example.net", "form_input_ph_url": "https://example.net/exemple",
"form_input_ph_email": "courriel_exemple@example.net", "form_input_ph_email": "courriel_exemple@example.net",
"form_notAnswered": "<b>{0}</b> réponses vides", "form_notAnswered": "<b>{0}</b> réponses vides",
"form_answerWarning": "Identité non confirmée", "form_answerWarning": "Identité non confirmée",

View file

@ -647,8 +647,8 @@
"fm_expirablePad": "Kedaluwarsa: {0}", "fm_expirablePad": "Kedaluwarsa: {0}",
"admin_activePadsTitle": "Dokumen aktif", "admin_activePadsTitle": "Dokumen aktif",
"admin_activePadsHint": "Jumlah dokumen unik yang saat ini ditampilkan atau disunting", "admin_activePadsHint": "Jumlah dokumen unik yang saat ini ditampilkan atau disunting",
"admin_registeredTitle": "Pengguna dan drive tim", "admin_registeredTitle": "Pengguna terdaftar",
"admin_registeredHint": "Jumlah drive aktif di server Anda", "admin_registeredHint": "Jumlah pengguna terdaftar di server Anda",
"admin_updateLimitButton": "Perbarui kuota", "admin_updateLimitButton": "Perbarui kuota",
"admin_updateLimitDone": "Pembaruan berhasil", "admin_updateLimitDone": "Pembaruan berhasil",
"admin_flushCacheDone": "Tembolok berhasil dibersihkan", "admin_flushCacheDone": "Tembolok berhasil dibersihkan",

View file

@ -1026,12 +1026,12 @@
"unableToDisplay": "Impossibile visualizzare il documento. Premi Esc per ricaricare la pagina. Se il problema persiste, contatta l'assistenza.", "unableToDisplay": "Impossibile visualizzare il documento. Premi Esc per ricaricare la pagina. Se il problema persiste, contatta l'assistenza.",
"documentID": "Codice identificativo del documento", "documentID": "Codice identificativo del documento",
"error_unhelpfulScriptError": "Errore di script: vedi la console del browser per i dettagli", "error_unhelpfulScriptError": "Errore di script: vedi la console del browser per i dettagli",
"loading_state_5": "Ricostruzione documento", "loading_state_5": "Ricostruzione del documento",
"loading_state_4": "Caricamento gruppi", "loading_state_4": "Caricamento dei gruppi",
"loading_state_3": "Caricamento cartelle condivise", "loading_state_3": "Caricamento delle cartelle condivise",
"loading_state_2": "Aggiornamento contenuto", "loading_state_2": "Aggiornamento del contenuto",
"loading_state_1": "Caricamento drive", "loading_state_1": "Caricamento del drive",
"loading_state_0": "Creazione interfaccia", "loading_state_0": "Creazione dell'interfaccia",
"fm_shareFolderPassword": "Proteggi questa cartella con una password (facoltativo)", "fm_shareFolderPassword": "Proteggi questa cartella con una password (facoltativo)",
"access_destroyPad": "Distruggi questo documento o questa cartella in modo permanente", "access_destroyPad": "Distruggi questo documento o questa cartella in modo permanente",
"fm_deletedFolder": "Cartella eliminata", "fm_deletedFolder": "Cartella eliminata",

View file

@ -628,8 +628,8 @@
"admin_activeSessionsHint": "Number of active websocket connections (and unique IP addresses connected)", "admin_activeSessionsHint": "Number of active websocket connections (and unique IP addresses connected)",
"admin_activePadsTitle": "Active documents", "admin_activePadsTitle": "Active documents",
"admin_activePadsHint": "Number of unique documents currently being viewed or edited", "admin_activePadsHint": "Number of unique documents currently being viewed or edited",
"admin_registeredTitle": "User and team drives", "admin_registeredTitle": "Registered users",
"admin_registeredHint": "Number of active drives on your instance", "admin_registeredHint": "Number of users registered on your instance",
"admin_updateLimitTitle": "Update user quotas", "admin_updateLimitTitle": "Update user quotas",
"admin_updateLimitHint": "Forcing an update of user storage limits can be done any time, but is only necessary in the event of an error", "admin_updateLimitHint": "Forcing an update of user storage limits can be done any time, but is only necessary in the event of an error",
"admin_updateLimitButton": "Update quotas", "admin_updateLimitButton": "Update quotas",

View file

@ -6,12 +6,12 @@
"whiteboard": "Whiteboard", "whiteboard": "Whiteboard",
"drive": "CryptDrive", "drive": "CryptDrive",
"slide": "Markdown bilder", "slide": "Markdown bilder",
"poll": "Omröstning", "poll": "Röstning",
"code": "Kod", "code": "Kod",
"todo": "Att-göra", "todo": "Att-göra",
"teams": "Teams", "teams": "Teams",
"sheet": "Kalkylark", "sheet": "Kalkylark",
"pad": "Rich text", "pad": "Rik text",
"kanban": "Kanban", "kanban": "Kanban",
"presentation": "Presentation", "presentation": "Presentation",
"doc": "Dokument", "doc": "Dokument",
@ -137,11 +137,11 @@
"chainpadError": "Ett kritiskt fel har uppstått när ditt innehåll uppdaterades. Denna sida är i skrivskyddat läge för att säkerställa att du inte förlorar ditt arbete.<br>Tryck Esc för att fortsätta visa detta dokument, eller ladda om och försök redigera igen.", "chainpadError": "Ett kritiskt fel har uppstått när ditt innehåll uppdaterades. Denna sida är i skrivskyddat läge för att säkerställa att du inte förlorar ditt arbete.<br>Tryck Esc för att fortsätta visa detta dokument, eller ladda om och försök redigera igen.",
"inactiveError": "Det här dokumentet har tagits bort på grund av inaktivitet. Vänligen tryck Esc för att skapa ett nytt dokument.", "inactiveError": "Det här dokumentet har tagits bort på grund av inaktivitet. Vänligen tryck Esc för att skapa ett nytt dokument.",
"deletedError": "Det här dokumentet har tagits bort och är inte längre tillgängligt.", "deletedError": "Det här dokumentet har tagits bort och är inte längre tillgängligt.",
"expiredError": "Det här dokument har nått sitt utgångsdatum och är inte längre tillgängligt.", "expiredError": "Det här dokument har nått sitt makuleringsdatum och är inte längre tillgängligt.",
"padNotPinnedVariable": "Detta dokument kommer gå ut efter {4} dagars inaktivitet, {0}logga in{1} eller {2}registrera dig{3} för att bevara det.", "padNotPinnedVariable": "Detta dokument kommer gå ut efter {4} dagars inaktivitet, {0}logga in{1} eller {2}registrera dig{3} för att bevara det.",
"padNotPinned": "Det här dokument kommer automatiskt gå ut efter 3 månader av inaktivitet, {0}logga in{1} eller {2}registrera dig{3} för att bevara det.", "padNotPinned": "Det här dokument kommer automatiskt gå ut efter 3 månader av inaktivitet, {0}logga in{1} eller {2}registrera dig{3} för att bevara det.",
"onLogout": "Du är utloggad, {0}klicka här{1} för att logga in<br>eller tryck Esc för att öppna ditt dokument i skrivskyddat läge.", "onLogout": "Du är utloggad, {0}klicka här{1} för att logga in<br>eller tryck Esc för att öppna ditt dokument i skrivskyddat läge.",
"typeError": "Det här dokumentet är inte kompatibelt med den valda applikationen", "typeError": "Det här dokument är inte kompatibelt med vald applikationen",
"kanban_working": "Pågående", "kanban_working": "Pågående",
"kanban_done": "Färdigt", "kanban_done": "Färdigt",
"kanban_todo": "Att göra", "kanban_todo": "Att göra",
@ -480,7 +480,7 @@
"settings_changePasswordCurrent": "Nuvarande lösenord", "settings_changePasswordCurrent": "Nuvarande lösenord",
"settings_changePasswordNew": "Nytt lösenord", "settings_changePasswordNew": "Nytt lösenord",
"features_f_core": "Vanliga funktioner", "features_f_core": "Vanliga funktioner",
"creation_expiration": "Förfallodatum", "creation_expiration": "Makuleringsdatum",
"share_linkAccess": "Behörighetsinställningar", "share_linkAccess": "Behörighetsinställningar",
"share_linkOpen": "Öppna länk", "share_linkOpen": "Öppna länk",
"share_linkCopy": "Kopiera länk", "share_linkCopy": "Kopiera länk",
@ -755,7 +755,7 @@
"admin_support_closed": "Avslutade ärenden:", "admin_support_closed": "Avslutade ärenden:",
"form_makeAnon": "Anonymisera svaren", "form_makeAnon": "Anonymisera svaren",
"team_exportButton": "Ladda ned", "team_exportButton": "Ladda ned",
"creation_expiresIn": "Förfaller om", "creation_expiresIn": "Makulera om",
"footer_website": "Projektets webbplats", "footer_website": "Projektets webbplats",
"form_condition_isnot": "är inte", "form_condition_isnot": "är inte",
"creation_expire": "Utgående dokument", "creation_expire": "Utgående dokument",
@ -986,7 +986,7 @@
"history_restoreDrivePrompt": "Är du säker på att du vill ersätta den nuvarande versionen av CryptDrive med den här versionen?", "history_restoreDrivePrompt": "Är du säker på att du vill ersätta den nuvarande versionen av CryptDrive med den här versionen?",
"infobar_versionHash": "Du visar för närvarande en tidigare version av detta dokument ({0}).", "infobar_versionHash": "Du visar för närvarande en tidigare version av detta dokument ({0}).",
"oo_deletedVersion": "Denna version finns inte längre i historien.", "oo_deletedVersion": "Denna version finns inte längre i historien.",
"admin_registrationHint": "Besökare till instansen har inte möjlighet att skapa konton. Inbjudningar kan skapas av administratörer.", "admin_registrationHint": "Tillåt inga nya användare att registrera sig",
"admin_defaultlimitTitle": "Lagringsgräns (MB)", "admin_defaultlimitTitle": "Lagringsgräns (MB)",
"error_unhelpfulScriptError": "Skriptfel: Se webbläsarkonsolen för mer information", "error_unhelpfulScriptError": "Skriptfel: Se webbläsarkonsolen för mer information",
"pad_settings_comments": "Välj om kommentarerna ska vara synliga eller dolda som standard.", "pad_settings_comments": "Välj om kommentarerna ska vara synliga eller dolda som standard.",
@ -1122,7 +1122,7 @@
"fm_info_root": "Skapa så många mappar här som du vill för att sortera dina filer.", "fm_info_root": "Skapa så många mappar här som du vill för att sortera dina filer.",
"fm_info_template": "Dessa dokument lagras som mallar. De kan återanvändas när du skapar nya dokument.", "fm_info_template": "Dessa dokument lagras som mallar. De kan återanvändas när du skapar nya dokument.",
"fm_info_trash": "Töm papperskorgen för att frigöra utrymme i din CryptDrive.", "fm_info_trash": "Töm papperskorgen för att frigöra utrymme i din CryptDrive.",
"admin_flushCacheHint": "Tvinga alla användare att ladda ner de senaste tillgångarna efter ändring i anpassningar eller inställningar. Detta undviker omstart av servern men tvingar varje aktiv användare att återansluta, använd sparsamt.", "admin_flushCacheHint": "Tvinga användare att ladda ner de senaste tillgångarna på klientsidan (endast om din server är i uppdaterat läge)",
"contact_admin": "Kontakta administratörerna för: {0}", "contact_admin": "Kontakta administratörerna för: {0}",
"contact_adminHint": "För eventuella problem relaterade till ditt konto, lagringsgräns eller tillgänglighet av tjänsten.\n", "contact_adminHint": "För eventuella problem relaterade till ditt konto, lagringsgräns eller tillgänglighet av tjänsten.\n",
"owner_addConfirm": "Delägare kommer att kunna ändra innehållet och ta bort dig som ägare. Är du säker?", "owner_addConfirm": "Delägare kommer att kunna ändra innehållet och ta bort dig som ägare. Är du säker?",
@ -1334,7 +1334,7 @@
"settings_importConfirm": "Är du säker på att du vill importera de senaste dokumenten från den här webbläsaren till ditt användarkontos CryptDrive?", "settings_importConfirm": "Är du säker på att du vill importera de senaste dokumenten från den här webbläsaren till ditt användarkontos CryptDrive?",
"settings_changePasswordHint": "Ändra ditt kontos lösenord. Ange ditt nuvarande lösenord och bekräfta det nya lösenordet genom att skriva det två gånger.<br><b>Vi kan inte återställa ditt lösenord om du glömmer det, så var mycket försiktig!</b>", "settings_changePasswordHint": "Ändra ditt kontos lösenord. Ange ditt nuvarande lösenord och bekräfta det nya lösenordet genom att skriva det två gånger.<br><b>Vi kan inte återställa ditt lösenord om du glömmer det, så var mycket försiktig!</b>",
"creation_newPadModalDescription": "Klicka på en app för att skapa ett nytt dokument. Du kan också trycka på <b>Tabb</b> för att välja appen och trycka på <b>Retur</b> för att bekräfta.", "creation_newPadModalDescription": "Klicka på en app för att skapa ett nytt dokument. Du kan också trycka på <b>Tabb</b> för att välja appen och trycka på <b>Retur</b> för att bekräfta.",
"settings_autostoreTitle": "Dokument lagring i CryptDrive", "settings_autostoreTitle": "Pad lagring i CryptDrive",
"settings_changePasswordNewConfirm": "Bekräfta nytt lösenord", "settings_changePasswordNewConfirm": "Bekräfta nytt lösenord",
"properties_addPassword": "Lägg till ett lösenord", "properties_addPassword": "Lägg till ett lösenord",
"admin_supportInitHint": "Du kan konfigurera en supportbrevlåda för att ge användare av din CryptPad-instans ett sätt att kontakta dig säkert om de har problem med sitt konto.", "admin_supportInitHint": "Du kan konfigurera en supportbrevlåda för att ge användare av din CryptPad-instans ett sätt att kontakta dig säkert om de har problem med sitt konto.",
@ -1665,20 +1665,5 @@
"access_passwordUsed": "Lösenordet har använts tidigare för det här dokumentet. Det kan inte återanvändas.", "access_passwordUsed": "Lösenordet har använts tidigare för det här dokumentet. Det kan inte återanvändas.",
"status": "Status-sida", "status": "Status-sida",
"admin_diskUsageWarning": "Använd med försiktighet! Beroende på storleken på de data som lagras på instansen kan generering av denna rapport använda allt tillgängligt minne på servern och leda till en krasch.", "admin_diskUsageWarning": "Använd med försiktighet! Beroende på storleken på de data som lagras på instansen kan generering av denna rapport använda allt tillgängligt minne på servern och leda till en krasch.",
"dph_pad_pw": "Det här dokumentet skyddas med ett nytt lösenord", "dph_pad_pw": "Det här dokumentet skyddas med ett nytt lösenord"
"calendar_rec_change_first": "Flyttar den första återkommande händelsen till en annan kalender. Samtliga återkommande händelser kommer också att flyttas.",
"admin_forcemfaTitle": "Obligatorisk Två-Faktor Autentisering",
"calendar_desc": "Beskrivning",
"calendar_description": "Beskrivning:{0}{1}",
"sso_login_description": "Logga in med",
"sso_register_description": "Registrera med",
"ssoauth_form_hint_login": "Var vänlig ange ditt CryptPad lösenord",
"duplicate": "Duplicera",
"kanban_showTags": "Se alla taggar",
"kanban_hideTags": "Se färre taggar",
"ssoauth_header": "CryptPad Lösenord",
"admin_forcemfaHint": "Alla användare av den här instansen kommer att uppmanas att ställa in tvåfaktorsautentisering för att logga in på sitt konto. Notera att existerande användare inte kommer att kunna behålla sina konton utan att ställa in en TOTP-lösning.",
"ssoauth_form_hint_register": "Lägg till ett CryptPad-lösenord för extra säkerhet eller lämna tomt och fortsätt. Om du inte lägger till ett lösenord kommer nycklarna som skyddar din data att vara tillgänglig för instansadministratörerna.",
"calendar_rec_change": "Flyttar en återkommande händelse till en annan kalender. Du kan bara applicera förändringen till denna eller alla återkommande händelser.",
"admin_channelPlaceholder": "Förfallna dokument platshållare"
} }

18
www/loadtest/index.html Normal file
View file

@ -0,0 +1,18 @@
<!--
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!DOCTYPE html>
<html>
<head>
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script async data-bootload="/loadtest/main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
</head>
<body class="html cp-page-load">
<noscript></noscript>

431
www/loadtest/main.js Normal file
View file

@ -0,0 +1,431 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/api/config',
'jquery',
'netflux-client',
'/common/hyperscript.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-interface.js',
'/common/outer/network-config.js',
'/components/nthen/index.js',
'/components/saferphore/index.js',
'/components/tweetnacl/nacl-fast.min.js',
'less!/customize/src/less2/pages/page-load.less',
'css!/components/components-font-awesome/css/font-awesome.min.css',
], function (Config, $, Netflux, h, Hash, Util, UI, NetConfig, nThen, Saferphore) {
const wsUrl = NetConfig.getWebsocketURL();
const nacl = window.nacl;
let makeNetwork = function (cb) {
Netflux.connect(wsUrl).then(function (network) {
cb(null, network);
}, function (err) {
cb(err);
});
};
let Env = {
users: {},
channels: {},
queries: 0,
lag: [],
errors: 0
};
let startSendDataEvt = Util.mkEvent(true);
let hk;
let edPublic = "gH12mjdXc1hGsVMtCJeoGTkBQRA21V0VOEGphoddmPM=";
let edPrivate = "5V0tO8q1wKr62KIJadYdXaXvgG8f6FQtS6XHYrHYLzGAfXaaN1dzWEaxUy0Il6gZOQFBEDbVXRU4QamGh12Y8w==";
let hash = "/2/undefined/edit/UCrOzk5XEOP7qi"; // missing 10 characters
let makeHash = (id) => {
let l = String(id).length;
let add = 10 - l;
let str = String(id);
for(let i=0; i<add; i++) {
str = 'x' + str;
}
let _hash = hash + str + '/';
return _hash;
};
let getMsg = isCp => {
let base = nacl.util.encodeBase64(nacl.randomBytes(30));
let repeat = isCp ? 300 : 5;
let str = base;
for (let i = 0; i < repeat; i++) {
str += base;
}
return str;
};
let signMsg = (isCp, secret) => {
let msg = getMsg(isCp);
let signKey = nacl.util.decodeBase64(secret.keys.signKey);
let signed = nacl.util.encodeBase64(nacl.sign(nacl.util.decodeUTF8(msg), signKey));
if (!isCp) { return signed; }
let id = msg.slice(0,8);
return `cp|${id}|${signed}`;
};
let makeData = function (id, cb) {
let user = Env.users[id];
if (!user || !user.wc || !user.secret || !user.isEmpty) {
return void setTimeout(cb);
}
let n = nThen;
for (let i = 1; i<=130; i++) {
n = n(w => {
let m = signMsg(!(i%50), user.secret);
user.wc.bcast(m).then(w());
}).nThen;
}
n(() => {
cb();
});
};
let clearDataCmd = max => {
let cmd = 'Run the following commands to clear all the data\n';
cmd += 'rm ';
for (let i=0; i<max; i++) {
let hash = makeHash(i);
let secret = Hash.getSecrets('pad', hash);
let chan = secret.channel;
cmd += `${chan.slice(0,2)}/${chan}* `;
}
console.error(cmd);
};
let joinChan = (user, id, cb) => {
if (!user || !user.network) { return; }
let network = user.network;
let hash = makeHash(id);
let secret = Hash.getSecrets('pad', hash);
if (!user.hash && !user.secret) {
user.hash = hash;
user.secret = secret;
}
user.isEmpty = true; // Only used with the makeData button
let chan = Env.channels[secret.channel] = Env.channels[secret.channel] || {
secret: secret,
};
let n = 0;
network.on('message', (msg, sender) => {
if (sender !== hk) { return; }
let parsed = JSON.parse(msg);
if (parsed.state === 1 && parsed.channel === secret.channel) {
chan.total = n; // %50 to know if we should make a cp
return void cb();
}
let m = parsed[4];
if (parsed[3] !== secret.channel) { return; }
if (!m) { return; }
n++;
user.isEmpty = false;
});
network.join(secret.channel).then(wc => {
user.wc = wc;
if (!hk) {
wc.members.forEach(function (p) {
if (p.length === 16) { hk = p; }
});
}
let cfg = {
metadata: {
validateKey: secret.keys.validateKey,
owners: [edPublic]
}
};
let msg = ['GET_HISTORY', wc.id, cfg];
network.sendto(hk, JSON.stringify(msg));
});
};
// TODO
// Connect many websockets and have them run tasks
// * [x] JOIN with 10 users per pad
// * [x] GET_HISTORY
// * [x] SEND content at random intervals
// * UPLOAD random blobs
// * RPC commands?
let setRandomInterval = f => {
let delay = (Env.delay - 300)*2;
let rdm = 300 + Math.floor(delay * Math.random());
if (Env.stopPatch) { return; }
setTimeout(function () {
f();
setRandomInterval(f);
}, rdm);
};
let startOneUser = function (i, init, cb) {
let network;
let myPads = [i];
let me;
nThen(w => {
makeNetwork(w((err, _network) => {
if (err) {
w.abort();
return void console.error(err);
}
network = _network;
me = Env.users[i] = {
network: network,
myPads
};
}));
}).nThen(w => {
joinChan(me, i, w());
}).nThen(w => {
if (!init) { return; }
makeData(i, w());
}).nThen(w => {
if (init) { return; }
console.warn(i, me.secret.channel);
let min = Math.max(Env.offset, i-5); // XXX 5 users per pad
for (let j = min; j<i; j++) {
myPads.push(j);
joinChan(me, j, w());
}
}).nThen(w => {
if (init) { return; }
myPads.forEach(function (id) {
let channel = (Env.users[id] && Env.users[id].secret) ? Env.users[id].secret.channel : null;
if (channel==null) {
console.log("Channel " + id + " is null");
} else {
let wc = me.network.webChannels.find(obj => {
return obj.id === channel;
});
let chanObj = Env.channels[channel] || {};
// Only fill the chan if it is not originally empty
if (Env.users[id].isEmpty) { return; }
startSendDataEvt.reg(function () {
setRandomInterval(function () {
let i = chanObj.total || 0;
let m = signMsg(!(i%50), chanObj.secret);
console.log('Send patch', channel, i%50);
chanObj.total = i+1;
Env.incQueries();
let t = +new Date();
wc.bcast(m).then(() => {
let now = +new Date();
Env.lag.push((now - t));
}, err => {
Env.errors++;
console.error(err);
});
});
});
}
});
}).nThen(w => {
// TODO
// RPC commands? Upload blob?
}).nThen(w => {
cb();
});
};
let start = function (cb) {
clearDataCmd(Env.numberUsers);
var sem = Saferphore.create(20);
let max = Env.numberUsers + Env.offset;
nThen(w => {
for (let i=Env.offset; i<max; i++) {
let done = w();
sem.take(function(give) {
console.log('loading user ', i);
startOneUser(i, false, () => {
setTimeout(give(() => {
done();
}));
console.log('loaded user ', i);
});
});
}
}).nThen(() => {
cb();
});
};
let makeAllData = function (cb) {
var sem = Saferphore.create(10);
let max = Env.numberUsers + Env.offset;
nThen(w => {
for (let i=Env.offset; i<max; i++) {
let done = w();
sem.take(function(give) {
console.log('loading user ', i);
startOneUser(i, true, () => {
setTimeout(give(() => {
done();
}));
console.log('loaded user ', i);
});
});
}
}).nThen(() => {
cb();
});
};
$(function () {
let input = h('input', {type:'number',value:100,min:1, step:1});
let label = h('label', [
h('span', 'Number of users'),
input
]);
let inputOff = h('input', {type:'number',value:0,min:0, step:1});
let labelOff = h('label', [
h('span', 'User offset'),
inputOff
]);
let inputFreq = h('input', {type:'number',value:800,min:300, step:1});
let labelFreq = h('label', [
h('span', 'Average time between patches (ms) per user per channel'),
inputFreq
]);
let inputMax = h('input', {type:'number',value:0,min:0, step:1});
let labelMax = h('label', [
h('span', 'Max queries (0 for infinite)'),
inputMax
]);
let queries = h('span');
let freq = h('span');
let freqr = h('span');
let time = h('span');
let lag = h('span');
let errors = h('span');
let res = h('div', [
queries,
h('br'),
time,
h('br'),
freq,
h('br'),
freqr,
h('br'),
lag,
h('br'),
errors
]);
let button = h('button.btn.btn-primary', 'Start load testing');
let buttonPatch = h('button.btn.btn-primary', {style:'display:none;'}, 'Start sending patches');
let buttonStopPatch = h('button.btn.btn-danger-alt', {style:'display:none;'}, 'STOP sending patches');
let buttonData = h('button.btn', 'Create data');
var spinner = UI.makeSpinner();
let content = h('div', [
h('div.form', [
label,
labelOff,
labelFreq,
labelMax,
h('nav', [button, buttonPatch, buttonStopPatch, buttonData, spinner.spinner]),
res
])
]);
Env.incQueries = () => {
Env.queries++;
if (Env.maxQ && Env.queries >= Env.maxQ) {
Env.stopPatch = true;
$(buttonStopPatch).click();
}
};
let started = false;
$(button).click(() => {
if (started) { return; }
spinner.spin();
started = true;
//$(button).remove();
let users = Env.numberUsers = Number($(input).val());
Env.offset = Number($(inputOff).val()) || 0;
Env.delay = Number($(inputFreq).val()) || 800;
Env.maxQ = Number($(inputMax).val()) || 0;
if (typeof(users) !== "number" || !users) {
return void console.error('Not a valid number');
}
$(buttonData).remove();
start(() => {
spinner.done();
started = false;
UI.log('READY: you can now start sending patches');
$(buttonPatch).show();
});
});
let qIt, fIt;
let last = {};
$(buttonPatch).click(() => {
startSendDataEvt.fire();
$(buttonPatch).remove();
$(buttonStopPatch).show();
Env.start = +new Date();
last.t = +new Date();
last.q = 0;
qIt = setInterval(() => {
$(queries).text('Queries: '+Env.queries);
let q = Env.queries;
let now = +new Date();
let diffTime = (now-Env.start)/1000;
let f = Math.floor(q/diffTime);
const average = Math.round((Env.lag.length && Env.lag.reduce((a, b) => a + b, 0) / Env.lag.length)) || 0;
$(freq).text('Queries/s (all): '+f);
$(time).text('Time: '+Math.floor(diffTime)+'s');
$(lag).text('Avg response time: '+average+'ms');
$(errors).text('Errors: '+Env.errors);
Env.lag = [];
}, 200);
fIt = setInterval(() => {
let q = Env.queries;
let now = +new Date();
let fr = Math.floor(1000*(Env.queries-last.q)/(now-last.t));
last.t = +new Date();
last.q = q;
$(freqr).text('Queries/s (recent): '+fr);
}, 1000);
});
$(buttonStopPatch).click(() => {
Env.stopPatch = true;
clearInterval(qIt);
clearInterval(fIt);
$(buttonStopPatch).remove();
});
let startedData = false;
$(buttonData).click(() => {
if (startedData) { return; }
startedData = true;
spinner.spin();
let users = Env.numberUsers = Number($(input).val());
Env.offset = Number($(inputOff).val()) || 0;
Env.delay = Number($(inputFreq).val()) || 800;
Env.maxQ = Number($(inputMax).val()) || 0;
if (typeof(users) !== "number" || !users) {
return void console.error('Not a valid number');
}
$(button).remove();
$(buttonData).remove();
makeAllData(() => {
spinner.done();
UI.log('DONE');
});
});
$('body').append(content);
});
});