Merge branch 'query-performance'

This commit is contained in:
Kailash Nadh 2024-01-27 15:56:09 +05:30
commit bb1492b882
60 changed files with 587 additions and 136 deletions

View file

@ -15,6 +15,7 @@ import (
"time"
"github.com/Masterminds/sprig/v3"
"github.com/gdgvda/cron"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/types"
"github.com/knadh/goyesql/v2"
@ -28,6 +29,7 @@ import (
"github.com/knadh/listmonk/internal/bounce"
"github.com/knadh/listmonk/internal/bounce/mailbox"
"github.com/knadh/listmonk/internal/captcha"
"github.com/knadh/listmonk/internal/core"
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/internal/media"
@ -494,7 +496,7 @@ func initTxTemplates(m *manager.Manager, app *App) {
}
// initImporter initializes the bulk subscriber importer.
func initImporter(q *models.Queries, db *sqlx.DB, app *App) *subimporter.Importer {
func initImporter(q *models.Queries, db *sqlx.DB, core *core.Core, app *App) *subimporter.Importer {
return subimporter.New(
subimporter.Options{
DomainBlocklist: app.constants.Privacy.DomainBlocklist,
@ -502,6 +504,9 @@ func initImporter(q *models.Queries, db *sqlx.DB, app *App) *subimporter.Importe
BlocklistStmt: q.UpsertBlocklistSubscriber.Stmt,
UpdateListDateStmt: q.UpdateListsDate.Stmt,
NotifCB: func(subject string, data interface{}) error {
// Refresh cached subscriber counts and stats.
core.RefreshMatViews(true)
app.sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data)
return nil
},
@ -803,6 +808,22 @@ func initCaptcha() *captcha.Captcha {
})
}
func initCron(core *core.Core) {
c := cron.New()
_, err := c.Add(ko.MustString("app.cache_slow_queries_interval"), func() {
lo.Println("refreshing slow query cache")
_ = core.RefreshMatViews(true)
lo.Println("done refreshing slow query cache")
})
if err != nil {
lo.Printf("error initializing slow cache query cron: %v", err)
return
}
c.Start()
lo.Printf("IMPORTANT: database slow query caching is enabled. Aggregate numbers and stats will not be realtime. Next refresh at: %v", c.Entries()[0].Next)
}
func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) chan bool {
// The blocking signal handler that main() waits on.
out := make(chan bool)

View file

@ -191,6 +191,7 @@ func main() {
cOpt := &core.Opt{
Constants: core.Constants{
SendOptinConfirmation: app.constants.SendOptinConfirmation,
CacheSlowQueries: ko.Bool("app.cache_slow_queries"),
},
Queries: queries,
DB: db,
@ -208,7 +209,7 @@ func main() {
app.queries = queries
app.manager = initCampaignManager(app.queries, app.constants, app)
app.importer = initImporter(app.queries, db, app)
app.importer = initImporter(app.queries, db, app.core, app)
app.notifTpls = initNotifTemplates("/email-templates/*.html", fs, app.i18n, app.constants)
initTxTemplates(app.manager, app)
@ -233,6 +234,11 @@ func main() {
// Load system information.
app.about = initAbout(queries, db)
// Start cronjobs.
if cOpt.Constants.CacheSlowQueries {
initCron(app.core)
}
// Start the campaign workers. The campaign batches (fetch from DB, push out
// messages) get processed at the specified interval.
go app.manager.Run()

View file

@ -11,6 +11,7 @@ import (
"time"
"unicode/utf8"
"github.com/gdgvda/cron"
"github.com/gofrs/uuid"
"github.com/jmoiron/sqlx/types"
"github.com/knadh/koanf/parsers/json"
@ -207,6 +208,13 @@ func handleUpdateSettings(c echo.Context) error {
}
set.DomainBlocklist = doms
// Validate slow query caching cron.
if set.CacheSlowQueries {
if _, err := cron.ParseStandard(set.CacheSlowQueriesInterval); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidData")+": slow query cron: "+err.Error())
}
}
// Update the settings in the DB.
if err := app.core.UpdateSettings(set); err != nil {
return err

View file

@ -2,6 +2,7 @@ package main
import (
"fmt"
"log"
"strings"
"github.com/jmoiron/sqlx"
@ -18,7 +19,7 @@ import (
// of logic to be performed before executing upgrades. fn is idempotent.
type migFunc struct {
version string
fn func(*sqlx.DB, stuffbin.FileSystem, *koanf.Koanf) error
fn func(*sqlx.DB, stuffbin.FileSystem, *koanf.Koanf, *log.Logger) error
}
// migList is the list of available migList ordered by the semver.
@ -69,7 +70,7 @@ func upgrade(db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) {
// Execute migrations in succession.
for _, m := range toRun {
lo.Printf("running migration %s", m.version)
if err := m.fn(db, fs, ko); err != nil {
if err := m.fn(db, fs, ko, lo); err != nil {
lo.Fatalf("error running migration %s: %v", m.version, err)
}

View file

@ -147,7 +147,7 @@ describe('Subscribers', () => {
// Get the ID from the header and proceed to fill the form.
let id = 0;
cy.get('[data-cy=id]').then(($el) => {
id = $el.text();
id = parseInt($el.text());
cy.get('input[name=email]').clear().type(email);
cy.get('input[name=name]').clear().type(name);
@ -162,9 +162,11 @@ describe('Subscribers', () => {
});
// Confirm the edits on the table.
cy.wait(250);
cy.wait(500);
cy.log(rows);
cy.get('tbody tr').each(($el) => {
cy.wrap($el).find('td[data-id]').invoke('attr', 'data-id').then((id) => {
cy.wrap($el).find('td[data-id]').invoke('attr', 'data-id').then((idStr) => {
const id = parseInt(idStr);
cy.wrap($el).find('td[data-label=E-mail]').contains(rows[id].email.toLowerCase());
cy.wrap($el).find('td[data-label=Name]').contains(rows[id].name);
cy.wrap($el).find('td[data-label=Status]').contains(rows[id].status, { matchCase: false });

View file

@ -183,7 +183,7 @@
</div>
<div class="column has-text-right">
<a href="https://listmonk.app/docs/templating/#template-expressions" target="_blank" rel="noopener noreferer">
<b-icon icon="code" /> Templating reference</a>
<b-icon icon="code" /> {{ $t('campaigns.templatingRef') }}</a>
<span v-if="canEdit && form.content.contentType !== 'plain'" class="is-size-6 has-text-grey ml-6">
<a v-if="form.altbody === null" href="#" @click.prevent="onAddAltBody">
<b-icon icon="text" size="is-small" /> {{ $t('campaigns.addAltText') }}
@ -193,7 +193,6 @@
{{ $t('campaigns.removeAltText') }}
</a>
</span>
</a>
</div>
</div>

View file

@ -137,6 +137,13 @@
</div>
</div>
</div><!-- tile block -->
<p v-if="settings['app.cache_slow_queries']" class="has-text-grey">
*{{ $t('globals.messages.slowQueriesCached') }}
<a href="https://listmonk.app/docs/performance/query-caching" target="_blank" rel="noopener noreferer"
class="has-text-grey">
<b-icon icon="link-variant" /> {{ $t('globals.buttons.learnMore') }}
</a>
</p>
</section>
</section>
</template>
@ -144,6 +151,7 @@
<script>
import dayjs from 'dayjs';
import Vue from 'vue';
import { mapState } from 'vuex';
import { colors } from '../constants';
import Chart from '../components/Chart.vue';
@ -188,6 +196,7 @@ export default Vue.extend({
},
computed: {
...mapState(['settings']),
dayjs() {
return dayjs;
},

View file

@ -142,6 +142,14 @@
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="600" @close="onFormClose">
<list-form :data="curItem" :is-editing="isEditing" @finished="formFinished" />
</b-modal>
<p v-if="settings['app.cache_slow_queries']" class="has-text-grey">
*{{ $t('globals.messages.slowQueriesCached') }}
<a href="https://listmonk.app/docs/performance/query-caching" target="_blank" rel="noopener noreferer"
class="has-text-grey">
<b-icon icon="link-variant" /> {{ $t('globals.buttons.learnMore') }}
</a>
</p>
</section>
</template>

View file

@ -51,6 +51,30 @@
</div>
</div>
</div><!-- sliding window -->
<div>
<hr />
<div class="columns">
<div class="column is-4">
<b-field :label="$t('settings.performance.cacheSlowQueries')"
:message="$t('settings.performance.cacheSlowQueriesHelp')">
<b-switch v-model="data['app.cache_slow_queries']" name="app.cache_slow_queries" />
</b-field>
</div>
<div class="column is-4" :class="{ disabled: !data['app.cache_slow_queries'] }">
<b-field :label="$t('settings.maintenance.cron')">
<b-input v-model="data['app.cache_slow_queries_interval']" :disabled="!data['app.cache_slow_queries']"
placeholder="0 3 * * *" />
</b-field>
</div>
<div class="column">
<br /><br />
<a href="https://listmonk.app/docs/performance/query-caching" target="_blank" rel="noopener noreferer">
<b-icon icon="link-variant" /> {{ $t('globals.buttons.learnMore') }}
</a>
</div>
</div>
</div>
</div>
</template>

1
go.mod
View file

@ -39,6 +39,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gdgvda/cron v0.2.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect

2
go.sum
View file

@ -18,6 +18,8 @@ github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gdgvda/cron v0.2.0 h1:oX8qdLZq4tC5StnCsZsTNs2BIzaRjcjmPZ4o+BArKX4=
github.com/gdgvda/cron v0.2.0/go.mod h1:VEwidZXB255kESB5DcUGRWTYZS8KkOBYD1YBn8Wiyx8=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "No s'ha trobat {name} ",
"globals.messages.passwordChange": "Introduïu un valor per canviar",
"globals.messages.passwordChangeFull": "Buida i torna a introduir la contrasenya completa a '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" actualitzat",
"globals.months.1": "gen.",
"globals.months.10": "oct.",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Usuari",
"settings.mailserver.waitTimeout": "Espera el timeout",
"settings.mailserver.waitTimeoutHelp": "Temps per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la del grup (s per segon, m per minut).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Proveïdor",
"settings.media.s3.bucket": "Contenidor",
"settings.media.s3.bucketPath": "Ruta del contenidor",
@ -473,6 +475,8 @@
"settings.needsRestart": "La configuració ha canviat. Posa en pausa totes les campanyes en curs i reinicia l'aplicació",
"settings.performance.batchSize": "Mida del lot",
"settings.performance.batchSizeHelp": "El nombre de subscriptors que cal extreure de la base de dades en una sola iteració. Cada iteració extreu subscriptors de la base de dades, els envia missatges i després passa a la següent iteració per extreure el següent lot. Idealment, hauria de ser superior al rendiment màxim possible (concurrency * message_rate).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Concurrència",
"settings.performance.concurrencyHelp": "Màxim treballador concurrent (fils) que intentarà enviar missatges simultàniament.",
"settings.performance.maxErrThreshold": "Llindar d'error màxim",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} nebyl nalezen",
"globals.messages.passwordChange": "Zadejte hodnotu ke změně",
"globals.messages.passwordChangeFull": "Vymazat a zadat úplné heslo znovu v '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" aktualizován",
"globals.months.1": "Led",
"globals.months.10": "Říj",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Jméno uživatele",
"settings.mailserver.waitTimeout": "Časový limit čekání",
"settings.mailserver.waitTimeoutHelp": "Doba čekání na novou aktivitu na připojení před uzavřením a odebráním z fondu (s - sekundy, m - minuty).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Poskytovatel",
"settings.media.s3.bucket": "Sektor",
"settings.media.s3.bucketPath": "Cesta sektoru",
@ -473,6 +475,8 @@
"settings.needsRestart": "Nastavení změněno. Pozastavte všechny spuštěné kampaně a restartujte aplikaci",
"settings.performance.batchSize": "Velikost dávky",
"settings.performance.batchSizeHelp": "Počet odběratelů ke stažení z databáze v jednotlivé iteraci. Každá iterace stáhne odběratele z databáze, odešle jim zprávy a pak se přesune na další iteraci, aby stáhla další dávku. Ideálně by měl být vyšší než je maximální dosažitelná propustnost (souběžnost * četnost_zpráv).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Souběžnost",
"settings.performance.concurrencyHelp": "Maximální počet souběžných modulů worker (podprocesů), které se pokusí současně odeslat zprávy.",
"settings.performance.maxErrThreshold": "Maximální prahová hodnota chyb",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "Heb ddod o hyd i {enw]",
"globals.messages.passwordChange": "Rhoi gwerth i'w newid",
"globals.messages.passwordChangeFull": "Clirio ac ailgyflwyno'r cyfrinair llawn yn '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "Wedi diweddaru “{name}”",
"globals.months.1": "Ion",
"globals.months.10": "Hyd",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Enw defnyddiwr",
"settings.mailserver.waitTimeout": "Terfyn amser aros",
"settings.mailserver.waitTimeoutHelp": "Amser aros ar gyfer gweithgaredd newydd ar gysylltiad cyn ei gau a'i ddileu o'r gronfa (e ar gyfer eiliad",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Darparwr",
"settings.media.s3.bucket": "Bwced",
"settings.media.s3.bucketPath": "Llwybr bwced",
@ -473,6 +475,8 @@
"settings.needsRestart": "Wedi newid y gosodiadau. Rhewi'r holl ymgyrchoedd byw ac ailgychwyn yr ap",
"settings.performance.batchSize": "Maint y swp",
"settings.performance.batchSizeHelp": "Nifer y tanysgrifwyr y mae modd eu tynnu o'r gronfa ddata ar yr un pryd. Bydd pob iteriad yn tynnu tanysgrifwyr o'r gronfa ddata",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Cydamseru",
"settings.performance.concurrencyHelp": "Uchafswm nifer y gweithwyr (llinynnau) a fydd yn ceisio anfon negeseuon yr un pryd.",
"settings.performance.maxErrThreshold": "Uchafswm nifer y gwallau",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} ikke fundet",
"globals.messages.passwordChange": "Indtast en værdi, der skal ændres",
"globals.messages.passwordChangeFull": "Ryd og indtast den fulde adgangskode igen i '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" opdateret",
"globals.months.1": "Jan",
"globals.months.10": "Okt",
@ -429,6 +430,7 @@
"settings.mailserver.username": "Brugernavn",
"settings.mailserver.waitTimeout": "Ventetid timeout",
"settings.mailserver.waitTimeoutHelp": "Tid til at vente på ny aktivitet på en forbindelse, før du lukker den og fjerner den fra poolen (s for sekund, m for minut).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Udbyder",
"settings.media.s3.bucket": "Spand",
"settings.media.s3.bucketPath": "Spand sti",
@ -469,6 +471,8 @@
"settings.needsRestart": "Indstillinger ændret. Sæt alle kørende kampagner på pause, og genstart appen",
"settings.performance.batchSize": "Batch størrelse",
"settings.performance.batchSizeHelp": "Antallet af abonnenter, der skal trækkes fra databasen i en enkelt iteration. Hver iteration trækker abonnenter fra databasen, sender meddelelser til dem og går derefter videre til den næste iteration for at trække den næste batch. Dette bør ideelt set være højere end den maksimalt opnåelige gennemstrømning (samtidighed * message_rate).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Samtidighed",
"settings.performance.concurrencyHelp": "Maksimalt antal samtidige arbejdere (tråde), der forsøger at sende meddelelser samtidigt.",
"settings.performance.maxErrThreshold": "Maksimal fejltærskel",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} nicht gefunden",
"globals.messages.passwordChange": "Gib dein Passwort für die Änderung ein",
"globals.messages.passwordChangeFull": "Löschen und das vollständige Passwort in '{name}' erneut eingeben.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" aktualisiert",
"globals.months.1": "Jan",
"globals.months.10": "Okt",
@ -429,6 +430,7 @@
"settings.mailserver.username": "Benutzername",
"settings.mailserver.waitTimeout": "Maximale Wartezeit",
"settings.mailserver.waitTimeoutHelp": "Wartezeit auf neue Aktivität bevor eine Verbindung geschlossen und aus dem Pool entfernt wird. (s für Sekunden, m für Minuten).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Anbieter",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Bucket Pfad",
@ -469,6 +471,8 @@
"settings.needsRestart": "Einstellungen geändert. Pausiere alle laufenden Kampagnen und starte die App (Listmonk) neu",
"settings.performance.batchSize": "Durchlaufgröße",
"settings.performance.batchSizeHelp": "Die Anzahl an Abonnenten, die in einem Durchlauf verarbeitet werden. Jeder Durchlauf holt die angegebene Anzahl an Abonnenten und schickt die Nachrichten. Idealerweise sollte dies höher sein als der maximal erreichbare Durchsatz (Anzahl Threads * Nachrichtenrate).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Anzahl Threads",
"settings.performance.concurrencyHelp": "Maximale Anzahl an Threads, welche versuchen Nachrichten versenden.",
"settings.performance.maxErrThreshold": "Maximale Anzahl Fehler",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "Το {name} δεν βρέθηκε",
"globals.messages.passwordChange": "Εισάγετε νέο περιεχόμενο για αλλαγή",
"globals.messages.passwordChangeFull": "Εκκαθάριση και επανεισαγωγή του συνθηματικού στο '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "Το \"{name}\" ενημερώθηκε",
"globals.months.1": "Ιαν",
"globals.months.10": "Οκτ",
@ -429,6 +430,7 @@
"settings.mailserver.username": "Όνομα χρήστη",
"settings.mailserver.waitTimeout": "Χρονικό όριο αναμονής",
"settings.mailserver.waitTimeoutHelp": "Χρόνος αναμονής για νέα δραστηριότητα σε μια σύνδεση πριν από το κλείσιμό της και την αφαίρεσή της από τη δεξαμενή (s για το δευτερόλεπτο, m για το λεπτό).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Πάροχος",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Διαδρομή του bucket",
@ -469,6 +471,8 @@
"settings.needsRestart": "Οι ρυθμίσεις άλλαξαν. Διακόψτε όλες τις τρέχουσες καμπάνιες και επανεκκινήστε την εφαρμογή",
"settings.performance.batchSize": "Μέγεθος παρτίδας",
"settings.performance.batchSizeHelp": "Ο αριθμός των συνδρομητών που θα αντληθούν από τη βάση δεδομένων σε κάθε επανάληψη. Κάθε επανάληψη αντλεί συνδρομητές από τη βάση δεδομένων, στέλνει μηνύματα σε αυτούς και στη συνέχεια μεταβαίνει στην επόμενη επανάληψη για να αντλήσει την επόμενη παρτίδα. Αυτός ο αριθμός θα πρέπει ιδανικά να είναι υψηλότερος από τη μέγιστη επιτεύξιμη απόδοση (παραλληλισμός * ρυθμός μηνυμάτων).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Παραλληλισμός",
"settings.performance.concurrencyHelp": "Μέγιστος αριθμός νημάτων που θα προσπαθήσει να στείλει μηνύματα ταυτόχρονα.",
"settings.performance.maxErrThreshold": "Μέγιστο όριο σφάλματος",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} not found",
"globals.messages.passwordChange": "Enter a value to change",
"globals.messages.passwordChangeFull": "Clear and re-enter the full password in '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" updated",
"globals.months.1": "Jan",
"globals.months.10": "Oct",
@ -429,6 +430,7 @@
"settings.mailserver.username": "Username",
"settings.mailserver.waitTimeout": "Wait timeout",
"settings.mailserver.waitTimeoutHelp": "Time to wait for new activity on a connection before closing it and removing it from the pool (s for second, m for minute).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Provider",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Bucket path",
@ -469,6 +471,8 @@
"settings.needsRestart": "Settings changed. Pause all running campaigns and restart the app",
"settings.performance.batchSize": "Batch size",
"settings.performance.batchSizeHelp": "The number of subscribers to pull from the database in a single iteration. Each iteration pulls subscribers from the database, sends messages to them, and then moves on to the next iteration to pull the next batch. This should ideally be higher than the maximum achievable throughput (concurrency * message_rate).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Only enable this on large databases that have slowed down significantly. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Concurrency",
"settings.performance.concurrencyHelp": "Maximum concurrent worker (threads) that will attempt to send messages simultaneously.",
"settings.performance.maxErrThreshold": "Maximum error threshold",

View file

@ -190,6 +190,7 @@
"globals.messages.notFound": "{name} no encontrado",
"globals.messages.passwordChange": "Ingresar una contraseña para cambiar",
"globals.messages.passwordChangeFull": "Borre y vuelva a ingresar la contraseña completa en '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" actualizado",
"globals.months.1": "Enero",
"globals.months.10": "Octubre",
@ -434,6 +435,7 @@
"settings.mailserver.username": "Nombre de usuario",
"settings.mailserver.waitTimeout": "Tiempo máximo de espera",
"settings.mailserver.waitTimeoutHelp": "Tiempo máximo de espera de nueva actividad en una conexión antes de cerrarla y retirarla del pool de conexiones (s para segundos, m para minutos).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Proveedor",
"settings.media.s3.bucket": "Bucket/contenedor",
"settings.media.s3.bucketPath": "Ruta de bucket",
@ -474,6 +476,8 @@
"settings.needsRestart": "Configuración cambiada. Pause todas las campañas y reinicie la aplicación.",
"settings.performance.batchSize": "Tamaño del lote",
"settings.performance.batchSizeHelp": "Número de suscriptores a extraer de la base de datos en cada iteración individul. Cada iteración extrae suscriptores de la base de datos, envía mensajes a ellos y luego avanza a la siguiente iteración para obtener el siguiente lote. Este número idealmente debería ser mayor que el máximo rendimiento alcanzable (concurrencia * tasa de envíos)",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Concurrencia",
"settings.performance.concurrencyHelp": "Número máximo de hilos que intentarán enviar mensajes de forma simultánea.",
"settings.performance.maxErrThreshold": "Umbral máximo de errores.",

View file

@ -190,6 +190,7 @@
"globals.messages.notFound": "{name} ei löytynyt",
"globals.messages.passwordChange": "Syötä arvoa muuttaaksesi",
"globals.messages.passwordChangeFull": "Tyhjennä ja kirjoita uudelleen täysi salasana kohdassa '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" päivitetty",
"globals.months.1": "Tammi",
"globals.months.10": "Loka",
@ -434,6 +435,7 @@
"settings.mailserver.username": "Käyttäjätunnus",
"settings.mailserver.waitTimeout": "Odota aikakatkaisu",
"settings.mailserver.waitTimeoutHelp": "Odota uusia toimintoja yhteydellä ennen kuin suljetaan ja poistetaan alta (s sekunteja, m minuutteja).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Tarjoaja",
"settings.media.s3.bucket": "Säilö",
"settings.media.s3.bucketPath": "Säilön polku",
@ -474,6 +476,8 @@
"settings.needsRestart": "Asetukset muutettu. Tauko kaikissa käynnissä olevissa kampanjoissa ja käynnistä sovellus uudelleen",
"settings.performance.batchSize": "Erän koko",
"settings.performance.batchSizeHelp": "Tilaajien määrä kannasta, jotka haetaan yhdellä noutokerroilla. Jokaisella noudolla tilaajia haetaan kannasta, lähetetään viesti ja siirrytään seuraavaan noudon erään. Joten tämän arvon tulisi olla suurempi kuin maksimaalinen suorituskyky (monisäikeisyys * viestinopeus).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Monisuoritus",
"settings.performance.concurrencyHelp": "Samanaikaisten työntekijöiden (säikeiden) enimmäismäärä, jotka yrittävät lähettää viestejä samanaikaisesti.",
"settings.performance.maxErrThreshold": "Enimmäisvirhekynnys",

View file

@ -190,6 +190,7 @@
"globals.messages.notFound": "{name} introuvable",
"globals.messages.passwordChange": "Entrez un nouveau mot de passe pour en changer",
"globals.messages.passwordChangeFull": "Effacer et saisir à nouveau le mot de passe complet dans '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "Mise à jour de \"{name}\"",
"globals.months.1": "jan.",
"globals.months.10": "oct.",
@ -434,6 +435,7 @@
"settings.mailserver.username": "Nom d'utilisateur",
"settings.mailserver.waitTimeout": "Délai d'attente",
"settings.mailserver.waitTimeoutHelp": "Temps d'attente d'une nouvelle activité sur une connexion avant sa fermeture et sa suppression du pool (s pour seconde, m pour minute)",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Fournisseur",
"settings.media.s3.bucket": "Compartiment",
"settings.media.s3.bucketPath": "Chemin du compartiment",
@ -474,6 +476,8 @@
"settings.needsRestart": "Certains paramètres ont été modifiés. Mettez toutes les campagnes actives en pause et redémarrez l'application.",
"settings.performance.batchSize": "Taille du lot",
"settings.performance.batchSizeHelp": "Le nombre d'abonné·es à extraire de la base de données en une seule itération. Chaque itération extrait les abonné·es de la base de données, leur envoie les messages, puis passe à l'itération suivante pour extraire le lot suivant. Idéalement cette valeur devrait être supérieure au débit maximum possible (Nb de threads * débit).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Nombre de threads",
"settings.performance.concurrencyHelp": "Nombre de workers (threads) concurrents maximum qui enverrons les messages simultanément.",
"settings.performance.maxErrThreshold": "Seuil maximum d'erreurs",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} לא נמצא",
"globals.messages.passwordChange": "הזן ערך לשינוי",
"globals.messages.passwordChangeFull": "נא לנקות ולהזין שוב את הסיסמה המלאה ב־'{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" עודכן",
"globals.months.1": "ינואר",
"globals.months.10": "אוקטובר",
@ -429,6 +430,7 @@
"settings.mailserver.username": "שם משתמש",
"settings.mailserver.waitTimeout": "זמן המתנה",
"settings.mailserver.waitTimeoutHelp": "זמן המתנה לפענוח פעילות נוספת בחיבור לפני סגירתו והסרתו מהקופסה (s לשנייה, m לדקה).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "ספק",
"settings.media.s3.bucket": "דלור סלון",
"settings.media.s3.bucketPath": "נתיב דלור סלון",
@ -469,6 +471,8 @@
"settings.needsRestart": "השינויים בהגדרות יחדו עם השהיית קמפיינים נכונים חדשים והפעל את אפליקציית ההפעלה.",
"settings.performance.batchSize": "מס יחידות בפסה",
"settings.performance.batchSizeHelp": "מס המנויים לשימוש מגרסת מסד הנתונים בשלב יחיד בלבד. שלב במסד הנתונים מושלם כולל מנויים מהמסד, שליחת הודעות אליהם והמשכת השלב המוסכמת לשלב הבא למשל מנויים נוספים ממסד הנתונים. הערך המומלץ מעלה מכותרת הרמות הנישפות המרבית (תנועה * קצב הודעות).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "דרגת תוחלת",
"settings.performance.concurrencyHelp": "שלב הפועל ביותר המטפלים מזמן אחד שירבים לשלח הודעות בתקופה יחידה.",
"settings.performance.maxErrThreshold": "רמת ה-שגיא המרבית",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} nem található",
"globals.messages.passwordChange": "Adja meg az új jelszót",
"globals.messages.passwordChangeFull": "Tisztítsa meg és írja be újra a teljes jelszót a(z) '{name}'-ben.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" frissítve",
"globals.months.1": "Jan",
"globals.months.10": "Okt",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Név",
"settings.mailserver.waitTimeout": "Várakozás",
"settings.mailserver.waitTimeoutHelp": "Kapcsolat életben tartása a megadott ideig. (s: másodperc, m: perc)",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Tárhely",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Eléréséi út",
@ -473,6 +475,8 @@
"settings.needsRestart": "A beállítások megváltoztak. Szüneteltesse az összes kampányt, és indítsa újra az alkalmazást.",
"settings.performance.batchSize": "Kötegméret",
"settings.performance.batchSizeHelp": "Az adatbázisból egy kötegben lehívandó tagok száma. Az üzenetek kiküldése kötegegen történik. Ideális esetben nagyobb, mint a számított átviteli sebesség ('Egyidejűség' × 'Üzenet / másodperc').",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Egyidejűség",
"settings.performance.concurrencyHelp": "Legfeljebb ennyi üzenetet próbál meg a rendszer egyszerre kiküldeni.",
"settings.performance.maxErrThreshold": "Hibaküszöb",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} introvabile",
"globals.messages.passwordChange": "Inserisci un valore da modificare",
"globals.messages.passwordChangeFull": "Cancella e reinserisci la password completa in '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" aggiornato",
"globals.months.1": "Gen",
"globals.months.10": "Ott",
@ -429,6 +430,7 @@
"settings.mailserver.username": "Nome utente",
"settings.mailserver.waitTimeout": "Tempo d'attesa",
"settings.mailserver.waitTimeoutHelp": "Tempo di attesa per una nuova attività su una connessione prima che venga chiusa e rimossa dal pool (s per secondo, m per minuto).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Fornitore",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Percorso del bucket",
@ -469,6 +471,8 @@
"settings.needsRestart": "Impostazione cambiata. Pausare tutte le campagne e riavviare l'applicazione",
"settings.performance.batchSize": "Dimensione del lotto",
"settings.performance.batchSizeHelp": "Numero di iscritti da estrarre dal database in una sola iterazione. Ogni iterazione estrae gli iscritti dal database, invia loro i messaggi, poi passa all'iterazione seguente per estrarre il lotto successivo. Idealmente questo valore dovrebbe essere superiore alla velocità massima possibile (Concorrenza x Frequenza del messaggio).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Simultanei",
"settings.performance.concurrencyHelp": "Numero di worker (threads) simultanei massimo che invieranno i messaggi contemporaneamente.",
"settings.performance.maxErrThreshold": "Soglia massima di errore",

View file

@ -190,6 +190,7 @@
"globals.messages.notFound": "{name} が見つかりません。",
"globals.messages.passwordChange": "変更するには値を入力",
"globals.messages.passwordChangeFull": "'{name}’でパスワードをクリアして再入力してください。",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" 更新済み",
"globals.months.1": "1月",
"globals.months.10": "10月",
@ -434,6 +435,7 @@
"settings.mailserver.username": "ユーザーネーム",
"settings.mailserver.waitTimeout": "タイムアウト待機",
"settings.mailserver.waitTimeoutHelp": "接続を閉じてプールから削除する前に、接続の新しいアクティビティの待機をする時間 (秒はs,分はm)",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "プロバイダー",
"settings.media.s3.bucket": "バケット",
"settings.media.s3.bucketPath": "バケットパス",
@ -474,6 +476,8 @@
"settings.needsRestart": "設定が変更されました。実行中の全てのキャンペーンを停止し、アプリをリスタートさせてください。",
"settings.performance.batchSize": "バッチサイズ",
"settings.performance.batchSizeHelp": "一回のイテレーションでデータベースから取得する加入者の数。各イテレーションではデータベースから加入者を取り出し、メッセージを送信した後、次のバッチを取り出すためのイテレーションに進みます。理想として達成可能な最大スループット (並行性 * メッセージ_レート)よりも高くなければなりません.",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "並行性",
"settings.performance.concurrencyHelp": "同時にメッセージを送信しようとする並行ワーカー(スレッド)の最大数。",
"settings.performance.maxErrThreshold": "最大エラーしきい値",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} കണ്ടെത്തിയില്ല",
"globals.messages.passwordChange": "മാറ്റം വരുത്തേണ്ട വില രേഖപ്പെടുത്തുക",
"globals.messages.passwordChangeFull": "'{name}' എന്നില്‍ നിന്ന് പൂര്‍ണ്ണമായി പാസ്‌വേഡ്‌ മാറ്റുക.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" പുതുക്കി",
"globals.months.1": "ജനുവരി",
"globals.months.10": "ഒക്ടോബർ",
@ -433,6 +434,7 @@
"settings.mailserver.username": "ഉപഭോക്തൃ നാമം",
"settings.mailserver.waitTimeout": "കാത്തുനിൽക്കുന്നതിനുള്ള സമയപരിധി",
"settings.mailserver.waitTimeoutHelp": "പൂളിൽ നിന്നും കണക്ഷൻ വിച്ഛേദിയ്ക്കുന്നതിനുമുമ്പ് പുതിയ പ്രവർത്തനത്തിനായി കാത്തുനിൽക്കുന്നതിനുള്ള സമയപരിധി(s സെക്കന്റിന്, m മിനുട്ടിന്).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "ദാതാവ്",
"settings.media.s3.bucket": "ബക്കറ്റ്",
"settings.media.s3.bucketPath": "ബക്കറ്റിലേക്കുള്ള പാത്ത്",
@ -473,6 +475,8 @@
"settings.needsRestart": "ക്രമീകരണങ്ങൾ മാറ്റി. പ്രവർത്തിക്കുന്ന എല്ലാ കാമ്പെയ്‌നുകളും താൽക്കാലികമായി നിർത്തി ആപ്പ് പുനരാരംഭിക്കുക",
"settings.performance.batchSize": "ബാച്ചിന്റെ വലിപ്പം",
"settings.performance.batchSizeHelp": "ഒരാവർത്തനത്തിൽ എത്ര വരിക്കാരെ ഡാറ്റാബേസിൽ നിന്നും എടുക്കണം. ഓരോ തവണയും വരിക്കാരെ ഡാറ്റാബേസിൽ നിന്നും എടുക്കുകയും അടുത്ത ആവർത്തനത്തിൽ അടുത്ത ബാച്ചിനെ എടുക്കുകയും അങ്ങനെ തുടരുകയും ചെയ്യും. ഈ മൂല്യം പരമാവധി ത്രൂപുട്ടിനേക്കാളും (concurrency * message_rate) കൂടുതലാകുന്നതാണ് നല്ലത്.",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "കൺകറൻസി",
"settings.performance.concurrencyHelp": "ഒരുമിച്ച് സന്ദേശമയക്കാൻ ശ്രമിക്കുന്നതിനുള്ള പരമാവധി സമാന്തര ജോലിക്കാർ (ത്രെഡുകൾ).",
"settings.performance.maxErrThreshold": "പിശകുണ്ടാകാവുന്നതിന്റെ പരമാവധി പരിധി",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} niet gevonden",
"globals.messages.passwordChange": "Geef een nieuw wachtwoord in",
"globals.messages.passwordChangeFull": "Wis en voer het volledige wachtwoord opnieuw in bij '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" geüpdatet",
"globals.months.1": "Jan",
"globals.months.10": "Okt",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Gebruikersnaam",
"settings.mailserver.waitTimeout": "Wachttijd",
"settings.mailserver.waitTimeoutHelp": "Hoe lang op nieuwe activeit gewacht moet worden voor een verbinding wordt gesloten en van de pool wordt verwijderd (s voor seconden, m voor minuten). ",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Provider",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Bucket pad",
@ -473,6 +475,8 @@
"settings.needsRestart": "Instellingen veranderd. Pauzeer alle lopende campagnes en herstart de app",
"settings.performance.batchSize": "Batchgrootte",
"settings.performance.batchSizeHelp": "Het aantal abonnees om per iteratie uit de database te lezen. Elke iteratie leest abonnees uit de database, verzend berichten naar hen, en gaat dan verder naar de volgende iteratie met de volgende batch. Dit aantal zou hoger moeten zijn dan de maximale doorvoer (Gelijktijdig * Berichtensnelheid).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Gelijktijdig",
"settings.performance.concurrencyHelp": "Maximum aantal workers (threads) die gelijktijdig proberen berichten te versturen.",
"settings.performance.maxErrThreshold": "Maximum aantal fouten",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} nie znaleziono",
"globals.messages.passwordChange": "Podaj wartość do zmiany",
"globals.messages.passwordChangeFull": "Wyczyść i ponownie wprowadź pełne hasło w '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" zaktualizowano",
"globals.months.1": "Sty",
"globals.months.10": "Paź",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Nazwa użytkownika",
"settings.mailserver.waitTimeout": "Czas oczekiwania",
"settings.mailserver.waitTimeoutHelp": "Czas czekania na nową aktywność na połączeniu przed jej zamknięciem i usunięciem z puli (s dla sekund, m dla minut).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Dostawca",
"settings.media.s3.bucket": "Komora (Bucket)",
"settings.media.s3.bucketPath": "Ścieżka komory (Bucket path)",
@ -473,6 +475,8 @@
"settings.needsRestart": "Ustawienia zmienione. Zatrzymaj wszystkie aktywne kampanie i uruchom ponownie aplikację",
"settings.performance.batchSize": "Rozmiar paczki",
"settings.performance.batchSizeHelp": "Liczba subskrybentów do pobrania z bazy danych przy jednej iteracji. Każda iteracja pobiera subskrybentów z bazy danych, wysyła do nich wiadomości, a następnie przechodzi do następnej iteracji. W idealnym przypadku powinno to być większe niż maksymalna przepustowość (liczba wątków * prędkość wysyłania wiadomości)",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Wielowątkowość",
"settings.performance.concurrencyHelp": "Maksymalna liczba jednoczesnych workerów (wątków), która będzie wysyłała wiadomości jednocześnie.",
"settings.performance.maxErrThreshold": "Maksymalny prób błędu",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} não encontrado",
"globals.messages.passwordChange": "Digite um valor para alterar",
"globals.messages.passwordChangeFull": "Limpe e insira novamente a senha completa em '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\"atualizado",
"globals.months.1": "Jan",
"globals.months.10": "Out",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Usuário",
"settings.mailserver.waitTimeout": "Tempo limite de espera",
"settings.mailserver.waitTimeoutHelp": "Tempo para esperar por uma nova atividade em uma conexão antes de fechá-la e removê-la do pool (s parar segundo, m para minuto).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Provedor",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Caminho do bucket",
@ -473,6 +475,8 @@
"settings.needsRestart": "Configurações alteradas. Pause todas as campanhas em execução e reiniciar o aplicativo",
"settings.performance.batchSize": "Tamanho do lote",
"settings.performance.batchSizeHelp": "O número de inscritos para puxar do banco de dados em uma única iteração. Cada iteração puxa assinantes da base de dados, envia mensagens para eles, e então passa para a próxima iteração para puxar o próximo lote. O ideal é que isso seja mais alto do que o máximo possível de transferência (concorrência * taxa de mensagem).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Concorrência",
"settings.performance.concurrencyHelp": "Máximo de trabalhador simultâneo (threads) que tentará enviar mensagens simultaneamente.",
"settings.performance.maxErrThreshold": "Limite máximo de erros",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} não encontrado",
"globals.messages.passwordChange": "Insere um valor para alterar",
"globals.messages.passwordChangeFull": "Limpe e digite novamente a senha completa em '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" atualizado",
"globals.months.1": "Jan",
"globals.months.10": "Out",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Nome de utilizador",
"settings.mailserver.waitTimeout": "Tempo limite de espera",
"settings.mailserver.waitTimeoutHelp": "Tempo a esperar por nova atividade numa conexão antes de a fechar e removê-la da pool (s para segundo, m para minuto).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Fornecedor",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Caminho do bucket",
@ -473,6 +475,8 @@
"settings.needsRestart": "Definições alteradas. Pause todas as campanhas em curso e reinicie a aplicação",
"settings.performance.batchSize": "Tamanho do lote",
"settings.performance.batchSizeHelp": "O número de subscritores para ir buscar à base de dados numa só iteração. Cada iteração vai buscar subscritores à base de dados, envia-lhe mensagens, e depois segue para a nova iteração para ir buscar o lote seguinte. Isto deve idealmente ser maior do que a máxima taxa de transferência alcançável (simultaneidade * taxa de mensagens).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Simultaneidade",
"settings.performance.concurrencyHelp": "Número máximo de workers (threads) concurrentes que irão tentar enviar as mensagens simultaneamente.",
"settings.performance.maxErrThreshold": "Limite máximo de erros",

View file

@ -190,6 +190,7 @@
"globals.messages.notFound": "{name} nu a fost găsit",
"globals.messages.passwordChange": "Introducerea unei valori de modificat",
"globals.messages.passwordChangeFull": "Ștergeți și reintroduceți parola completă în '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" actualizat",
"globals.months.1": "Ian",
"globals.months.10": "Oct",
@ -434,6 +435,7 @@
"settings.mailserver.username": "Nume de utilizator",
"settings.mailserver.waitTimeout": "Așteptați timeout-ul",
"settings.mailserver.waitTimeoutHelp": "E timpul să așteptați o nouă activitate pe o conexiune înainte de a o închide și de a o scoate din piscină (s pentru a doua, m pentru minut).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Prestator",
"settings.media.s3.bucket": "Găleată",
"settings.media.s3.bucketPath": "Calea cu găleată",
@ -474,6 +476,8 @@
"settings.needsRestart": "Setările s-au schimbat. Întrerupe toate campaniile care rulează și reporniți aplicația",
"settings.performance.batchSize": "Mărimea lotului",
"settings.performance.batchSizeHelp": "Numărul de abonați care pot fi extrași din baza de date într-o singură iterație. Fiecare iterație atrage abonații din baza de date, le trimite mesaje și apoi trece la următoarea iterație pentru a extrage următorul lot. Acest lucru ar trebui să fie în mod ideal mai mare decât debitul maxim realizabil (concurență * rată_mesaj).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Concurență",
"settings.performance.concurrencyHelp": "Lucrător simultan maxim (fire) care va încerca să trimită mesaje simultan.",
"settings.performance.maxErrThreshold": "Pragul maxim de eroare",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} не найдено",
"globals.messages.passwordChange": "Введите значение для изменения",
"globals.messages.passwordChangeFull": "Очистите и повторно введите полный пароль в '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" обновлено",
"globals.months.1": "Янв",
"globals.months.10": "Окт",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Имя пользователя",
"settings.mailserver.waitTimeout": "Таймаут ожидания",
"settings.mailserver.waitTimeoutHelp": "Время ожидания новой активности в соединении перед тем, как закрыть и удалить его из пула (s, m соттветственно секунды и минуты)",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Провайдер",
"settings.media.s3.bucket": "Бакет",
"settings.media.s3.bucketPath": "Путь bucket",
@ -473,6 +475,8 @@
"settings.needsRestart": "Параметры изменены. Приостановите все запущенные кампании и перезапустите приложение",
"settings.performance.batchSize": "Размер партии",
"settings.performance.batchSizeHelp": "Количество подписчиков, которые нужно извлечь из базы данных за одну итерацию. Каждая итерация извлекает подписчиков из базы данных, отправляет им сообщения, а затем переходит к следующей итерации, чтобы получить следующую партию. В идеале это должно быть выше максимально достижимой пропускной способности (concurrency * message_rate). ",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Параллельное выполнение",
"settings.performance.concurrencyHelp": "Максимальное число одновременно работающих процессов, которые будут пытаться одновременно отправить сообщения.",
"settings.performance.maxErrThreshold": "Порог максимального числа ошибок",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} not found",
"globals.messages.passwordChange": "Enter a value to change",
"globals.messages.passwordChangeFull": "Clear and re-enter the full password in '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" updated",
"globals.months.1": "Jan",
"globals.months.10": "Oct",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Username",
"settings.mailserver.waitTimeout": "Wait timeout",
"settings.mailserver.waitTimeoutHelp": "Time to wait for new activity on a connection before closing it and removing it from the pool (s for second, m for minute).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Provider",
"settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Bucket path",
@ -473,6 +475,8 @@
"settings.needsRestart": "Settings changed. Pause all running campaigns and restart the app",
"settings.performance.batchSize": "Batch size",
"settings.performance.batchSizeHelp": "The number of subscribers to pull from the database in a single iteration. Each iteration pulls subscribers from the database, sends messages to them, and then moves on to the next iteration to pull the next batch. This should ideally be higher than the maximum achievable throughput (concurrency * message_rate).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Concurrency",
"settings.performance.concurrencyHelp": "Maximum concurrent worker (threads) that will attempt to send messages simultaneously.",
"settings.performance.maxErrThreshold": "Maximum error threshold",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} sa nenašlo",
"globals.messages.passwordChange": "Zadajte zmenenú hodnotu",
"globals.messages.passwordChangeFull": "Zadajte celé heslo v '{name}' znova.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" upravené",
"globals.months.1": "Jan",
"globals.months.10": "Okt",
@ -433,6 +434,7 @@
"settings.mailserver.username": "Meno používateľa",
"settings.mailserver.waitTimeout": "Časový limit čakania",
"settings.mailserver.waitTimeoutHelp": "Doba čakania na novú aktivitu na pripojení pred uzavretím a odobratí z poolu (s - sekundy, m - minuty).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Poskytovateľ",
"settings.media.s3.bucket": "Sekcia",
"settings.media.s3.bucketPath": "Cesta bucketu",
@ -473,6 +475,8 @@
"settings.needsRestart": "Nastavenia zmenené. Pozastavte všetky spustené kampane a reštartuje aplikáciu",
"settings.performance.batchSize": "Veľkosť dávky",
"settings.performance.batchSizeHelp": "Počet odberateľov na stiahnutie z databázy v jednej iterácii. Každá iterácia stiahne odberateľov z databáze, odošle im správy a potom se presunie na dalšiu iteráciu, aby stiahla dalšiu dávku. Ideálne by mala byť vyššia než je maximálne dosiahnuteľná priepustnosť (súbežnosť * počet správ).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Súbežnosť",
"settings.performance.concurrencyHelp": "Maximálny počet súbežných procesov, ktoré se súčasne odosielajú správy.",
"settings.performance.maxErrThreshold": "Maximálna prahová hodnota chýb",

View file

@ -190,6 +190,7 @@
"globals.messages.notFound": "{name} bulunamadı",
"globals.messages.passwordChange": "Değiştirmek için değer gir",
"globals.messages.passwordChangeFull": "'{name}' içinde parolayı temizleyin ve yeniden girin.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" güncellendi",
"globals.months.1": "Oca",
"globals.months.10": "Eki",
@ -434,6 +435,7 @@
"settings.mailserver.username": "Kullanıcı adı",
"settings.mailserver.waitTimeout": "Bekleme süresi aşımı",
"settings.mailserver.waitTimeoutHelp": "Bir bağlantıdaki yeni etkinliği kapatmadan ve havuzdan kaldırmadan önce bekleme süresi (saniye için s, dakika için m). ",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Sağlayıcı",
"settings.media.s3.bucket": "Kova",
"settings.media.s3.bucketPath": "Bucket yolu",
@ -474,6 +476,8 @@
"settings.needsRestart": "Ayarlar değişti. Çalışan tüm kampanyaları durdur ve uygulamayı yeniden başlat.",
"settings.performance.batchSize": "Batch büyüklüğü",
"settings.performance.batchSizeHelp": "Veritabanından tek bir yinelemede çekilecek abone sayısı. Her yineleme, aboneleri veritabanından çeker, onlara mesajlar gönderir ve ardından bir sonraki grubu çekmek için bir sonraki yinelemeye geçer. Bu, ideal olarak elde edilebilecek maksimum iş hacminden (eşzamanlılık * ileti_ hızı) daha yüksek olmalıdır.",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Çoklu bağlantı",
"settings.performance.concurrencyHelp": "Aynı anda ileti göndermeyi deneyecek maksimum eşzamanlı worker (thread) sayısı.",
"settings.performance.maxErrThreshold": "Maksimum hata eşiği",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} не знайдено",
"globals.messages.passwordChange": "Щоб змінити, введіть нове значення",
"globals.messages.passwordChangeFull": "Зітріть і введіть заново повний пароль у '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "«{name}» оновлено",
"globals.months.1": "січ",
"globals.months.10": "жов",
@ -429,6 +430,7 @@
"settings.mailserver.username": "Логін",
"settings.mailserver.waitTimeout": "Час очікування",
"settings.mailserver.waitTimeoutHelp": "Скільки чекати нові дані, перш ніж закрити з'єднання й вилучити його з черги (s — секунди, m — хвилини).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Провайдер",
"settings.media.s3.bucket": "Сховище",
"settings.media.s3.bucketPath": "Шлях до сховища",
@ -469,6 +471,8 @@
"settings.needsRestart": "Налаштування змінено. Призупиніть усі запущені кампанії й перезапустіть програму",
"settings.performance.batchSize": "Обсяг вибірки",
"settings.performance.batchSizeHelp": "Скільком підписни_цям надсилати листи протягом одного запуску. В ідеалі значення має бути більшим, ніж добуток конкурентності й пропускної здатності.",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Конкурентність",
"settings.performance.concurrencyHelp": "Максимум потоків, які намагаються надсилати листи водночас.",
"settings.performance.maxErrThreshold": "Поріг помилок",

View file

@ -190,6 +190,7 @@
"globals.messages.notFound": "{name} không tìm thấy",
"globals.messages.passwordChange": "Nhập một giá trị để thay đổi",
"globals.messages.passwordChangeFull": "Xóa và nhập lại mật khẩu đầy đủ trong '{name}'.",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" đã cập nhật",
"globals.months.1": "Tháng 1",
"globals.months.10": "Tháng 10",
@ -434,6 +435,7 @@
"settings.mailserver.username": "Tài khoản",
"settings.mailserver.waitTimeout": "Chờ hết thời gian",
"settings.mailserver.waitTimeoutHelp": "Thời gian chờ hoạt động mới trên một kết nối trước khi đóng và xóa nó khỏi nhóm (s cho giây, m cho phút).",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "Các nhà cung cấp",
"settings.media.s3.bucket": "Gầu múc",
"settings.media.s3.bucketPath": "Đường nhóm",
@ -474,6 +476,8 @@
"settings.needsRestart": "Đã thay đổi cài đặt. Tạm dừng tất cả các chiến dịch đang chạy và khởi động lại ứng dụng",
"settings.performance.batchSize": "Kích thước lô",
"settings.performance.batchSizeHelp": "Số lượng người đăng ký để lấy từ cơ sở dữ liệu trong một lần lặp lại. Mỗi lần lặp lại kéo người đăng ký từ cơ sở dữ liệu, gửi tin nhắn cho họ, sau đó chuyển sang lần lặp tiếp theo để kéo đợt tiếp theo. Điều này lý tưởng là phải cao hơn thông lượng tối đa có thể đạt được (đồng thời * message_rate).",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "Đồng thời",
"settings.performance.concurrencyHelp": "Công nhân đồng thời tối đa (luồng) sẽ cố gắng gửi tin nhắn đồng thời.",
"settings.performance.maxErrThreshold": "Ngưỡng lỗi tối đa",

View file

@ -189,6 +189,7 @@
"globals.messages.notFound": "{name} 未找到",
"globals.messages.passwordChange": "输入要更改的值",
"globals.messages.passwordChangeFull": "在“{name}”中清除并重新输入完整密码。",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "“{name}”已更新",
"globals.months.1": "一月",
"globals.months.10": "十月",
@ -433,6 +434,7 @@
"settings.mailserver.username": "用户名",
"settings.mailserver.waitTimeout": "等待超时",
"settings.mailserver.waitTimeoutHelp": "在关闭连接并将其从池中删除之前等待连接上的新活动的时间s 表示秒m 表示分钟)。",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "提供者",
"settings.media.s3.bucket": "存储桶",
"settings.media.s3.bucketPath": "存储桶路径",
@ -473,6 +475,8 @@
"settings.needsRestart": "设置已更改。暂停所有正在运行的广告系列并重新启动应用",
"settings.performance.batchSize": "批量大小",
"settings.performance.batchSizeHelp": "在单次迭代中从数据库中提取的订阅者数量。每次迭代都会从数据库中提取订阅者,向他们发送消息,然后继续进行下一次迭代以提取下一批。理想情况下,这应该高于可实现的最大吞吐量(并发 * message_rate。",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "并发",
"settings.performance.concurrencyHelp": "将尝试同时发送消息的最大并发工作线程(线程)。",
"settings.performance.maxErrThreshold": "最大误差阈值",

View file

@ -190,6 +190,7 @@
"globals.messages.notFound": "{name} 未找到",
"globals.messages.passwordChange": "輸入要更改的值",
"globals.messages.passwordChangeFull": "在 '{name}' 中清除並重新輸入完整密碼。",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "“{name}”已更新",
"globals.months.1": "一月",
"globals.months.10": "十月",
@ -434,6 +435,7 @@
"settings.mailserver.username": "用戶名",
"settings.mailserver.waitTimeout": "等待超時",
"settings.mailserver.waitTimeoutHelp": "在關閉連接並將其從池中刪除之前等待連接上的新活動的時間s 表示秒m 表示分鐘)。",
"settings.maintenance.cron": "Cron interval",
"settings.media.provider": "提供者",
"settings.media.s3.bucket": "存儲桶",
"settings.media.s3.bucketPath": "存儲桶路徑",
@ -474,6 +476,8 @@
"settings.needsRestart": "設置已更改。暫停所有正在運行的廣告系列並重新啟動應用",
"settings.performance.batchSize": "批量大小",
"settings.performance.batchSizeHelp": "在單次迭代中從數據庫中提取的訂閱者數量。每次迭代都會從數據庫中提取訂閱者,向他們發送消息,然後繼續進行下一次迭代以提取下一批。理想情況下,這應該高於可實現的最大吞吐量(並發* message_rate。",
"settings.performance.cacheSlowQueries": "Cache slow database queries",
"settings.performance.cacheSlowQueriesHelp": "Enable this on large databases that have slowed down. Caches list subscriber counts, dashboard statistics etc.",
"settings.performance.concurrency": "並發",
"settings.performance.concurrencyHelp": "將嘗試同時發送消息的最大並發工作線程(線程)。",
"settings.performance.maxErrThreshold": "最大誤差閾值",

View file

@ -59,7 +59,7 @@ func (c *Core) GetBounce(id int) (models.Bounce, error) {
// RecordBounce records a new bounce.
func (c *Core) RecordBounce(b models.Bounce) error {
action, ok := c.constants.BounceActions[b.Type]
action, ok := c.consts.BounceActions[b.Type]
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("globals.messages.invalidData")+": "+b.Type)
}

View file

@ -20,17 +20,21 @@ import (
const (
SortAsc = "asc"
SortDesc = "desc"
matDashboardCharts = "mat_dashboard_charts"
matDashboardCounts = "mat_dashboard_counts"
matListSubStats = "mat_list_subscriber_stats"
)
// Core represents the listmonk core with all shared, global functions.
type Core struct {
h *Hooks
constants Constants
i18n *i18n.I18n
db *sqlx.DB
q *models.Queries
log *log.Logger
consts Constants
i18n *i18n.I18n
db *sqlx.DB
q *models.Queries
log *log.Logger
}
// Constants represents constant config.
@ -40,6 +44,7 @@ type Constants struct {
Count int
Action string
}
CacheSlowQueries bool
}
// Hooks contains external function hooks that are required by the core package.
@ -67,15 +72,49 @@ var (
// New returns a new instance of the core.
func New(o *Opt, h *Hooks) *Core {
return &Core{
h: h,
constants: o.Constants,
i18n: o.I18n,
db: o.DB,
q: o.Queries,
log: o.Log,
h: h,
consts: o.Constants,
i18n: o.I18n,
db: o.DB,
q: o.Queries,
log: o.Log,
}
}
// RefreshMatViews refreshes all materialized views.
func (c *Core) RefreshMatViews(concurrent bool) error {
for _, v := range []string{matDashboardCharts, matDashboardCounts, matListSubStats} {
_ = c.RefreshMatView(v, true)
}
return nil
}
// RefreshMatView refreshes a Postgres materialized view.
func (c *Core) RefreshMatView(name string, concurrent bool) error {
q := "REFRESH MATERIALIZED VIEW %s %s"
if concurrent {
q = fmt.Sprintf(q, "CONCURRENTLY", name)
} else {
q = fmt.Sprintf(q, "", name)
}
if _, err := c.db.Exec(q); err != nil {
c.log.Printf("error refreshing materialized view: %s: %v", name, err)
return err
}
return nil
}
// refreshCache refreshes a Postgres materialized view if caching is disabled.
func (c *Core) refreshCache(name string, concurrent bool) error {
if c.consts.CacheSlowQueries {
return nil
}
return c.RefreshMatView(name, concurrent)
}
// Given an error, pqErrMsg will try to return pq error details
// if it's a pq error.
func pqErrMsg(err error) string {

View file

@ -9,6 +9,8 @@ import (
// GetDashboardCharts returns chart data points to render on the dashboard.
func (c *Core) GetDashboardCharts() (types.JSONText, error) {
_ = c.refreshCache(matDashboardCharts, false)
var out types.JSONText
if err := c.q.GetDashboardCharts.Get(&out); err != nil {
return nil, echo.NewHTTPError(http.StatusInternalServerError,
@ -20,6 +22,8 @@ func (c *Core) GetDashboardCharts() (types.JSONText, error) {
// GetDashboardCounts returns stats counts to show on the dashboard.
func (c *Core) GetDashboardCounts() (types.JSONText, error) {
_ = c.refreshCache(matDashboardCounts, false)
var out types.JSONText
if err := c.q.GetDashboardCounts.Get(&out); err != nil {
return nil, echo.NewHTTPError(http.StatusInternalServerError,

View file

@ -37,14 +37,16 @@ func (c *Core) GetLists(typ string) ([]models.List, error) {
// QueryLists gets multiple lists based on multiple query params. Along with the paginated and sliced
// results, the total number of lists in the DB is returned.
func (c *Core) QueryLists(searchStr, typ, optin string, tags []string, orderBy, order string, offset, limit int) ([]models.List, int, error) {
out := []models.List{}
queryStr, stmt := makeSearchQuery(searchStr, orderBy, order, c.q.QueryLists, listQuerySortFields)
_ = c.refreshCache(matListSubStats, false)
if tags == nil {
tags = []string{}
}
var (
out = []models.List{}
queryStr, stmt = makeSearchQuery(searchStr, orderBy, order, c.q.QueryLists, listQuerySortFields)
)
if err := c.db.Select(&out, stmt, 0, "", queryStr, typ, optin, pq.StringArray(tags), offset, limit); err != nil {
c.log.Printf("error fetching lists: %v", err)
return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,

View file

@ -88,19 +88,9 @@ func (c *Core) QuerySubscribers(query string, listIDs []int, subStatus string, o
// Create a readonly transaction that just does COUNT() to obtain the count of results
// and to ensure that the arbitrary query is indeed readonly.
stmt := fmt.Sprintf(c.q.QuerySubscribersCount, cond)
tx, err := c.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
total, err := c.getSubscriberCount(cond, subStatus, listIDs)
if err != nil {
c.log.Printf("error preparing subscriber query: %v", err)
return nil, 0, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
}
defer tx.Rollback()
// Execute the readonly query and get the count of results.
total := 0
if err := tx.Get(&total, stmt, pq.Array(listIDs), subStatus); err != nil {
return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
return nil, 0, err
}
// No results.
@ -110,8 +100,17 @@ func (c *Core) QuerySubscribers(query string, listIDs []int, subStatus string, o
// Run the query again and fetch the actual data. stmt is the raw SQL query.
var out models.Subscribers
stmt := fmt.Sprintf(c.q.QuerySubscribersCount, cond)
stmt = strings.ReplaceAll(c.q.QuerySubscribers, "%query%", cond)
stmt = strings.ReplaceAll(stmt, "%order%", orderBy+" "+order)
tx, err := c.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
if err != nil {
c.log.Printf("error preparing subscriber query: %v", err)
return nil, 0, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
}
defer tx.Rollback()
if err := tx.Select(&out, stmt, pq.Array(listIDs), subStatus, offset, limit); err != nil {
return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
@ -289,7 +288,7 @@ func (c *Core) InsertSubscriber(sub models.Subscriber, listIDs []int, listUUIDs
}
hasOptin := false
if !preconfirm && c.constants.SendOptinConfirmation {
if !preconfirm && c.consts.SendOptinConfirmation {
// Send a confirmation e-mail (if there are any double opt-in lists).
num, _ := c.h.SendOptinConfirmation(out, listIDs)
hasOptin = num > 0
@ -374,7 +373,7 @@ func (c *Core) UpdateSubscriberWithLists(id int, sub models.Subscriber, listIDs
}
hasOptin := false
if !preconfirm && c.constants.SendOptinConfirmation {
if !preconfirm && c.consts.SendOptinConfirmation {
// Send a confirmation e-mail (if there are any double opt-in lists).
num, _ := c.h.SendOptinConfirmation(out, listIDs)
hasOptin = num > 0
@ -502,3 +501,37 @@ func (c *Core) DeleteBlocklistedSubscribers() (int, error) {
n, _ := res.RowsAffected()
return int(n), nil
}
func (c *Core) getSubscriberCount(cond, subStatus string, listIDs []int) (int, error) {
// If there's no condition, it's a "get all" call which can probably be optionally pulled from cache.
if cond == "" {
_ = c.refreshCache(matListSubStats, false)
total := 0
if err := c.q.QuerySubscribersCountAll.Get(&total, pq.Array(listIDs), subStatus); err != nil {
return 0, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
}
return total, nil
}
// Create a readonly transaction that just does COUNT() to obtain the count of results
// and to ensure that the arbitrary query is indeed readonly.
stmt := fmt.Sprintf(c.q.QuerySubscribersCount, cond)
tx, err := c.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
if err != nil {
c.log.Printf("error preparing subscriber query: %v", err)
return 0, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
}
defer tx.Rollback()
// Execute the readonly query and get the count of results.
total := 0
if err := tx.Get(&total, stmt, pq.Array(listIDs), subStatus); err != nil {
return 0, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
}
return total, nil
}

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V0_4_0 performs the DB migrations for v.0.4.0.
func V0_4_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V0_4_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
_, err := db.Exec(`
DO $$
BEGIN

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V0_7_0 performs the DB migrations for v.0.7.0.
func V0_7_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V0_7_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
// Check if the subscriber_status.blocklisted enum value exists. If not,
// it has to be created (for the change from blacklisted -> blocklisted).
var bl bool

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V0_8_0 performs the DB migrations for v.0.8.0.
func V0_8_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V0_8_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
_, err := db.Exec(`
INSERT INTO settings (key, value) VALUES ('privacy.individual_tracking', 'false')
ON CONFLICT DO NOTHING;

View file

@ -2,6 +2,7 @@ package migrations
import (
"fmt"
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
@ -9,7 +10,7 @@ import (
)
// V0_9_0 performs the DB migrations for v.0.9.0.
func V0_9_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V0_9_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
if _, err := db.Exec(`
INSERT INTO settings (key, value) VALUES
('app.lang', '"en"'),

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V1_0_0 performs the DB migrations for v.1.0.0.
func V1_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V1_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
if _, err := db.Exec(`ALTER TYPE content_type ADD VALUE IF NOT EXISTS 'markdown'`); err != nil {
return err
}

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V2_0_0 performs the DB migrations for v.1.0.0.
func V2_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V2_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
if _, err := db.Exec(`
DO $$
BEGIN

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V2_1_0 performs the DB migrations for v.2.1.0.
func V2_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V2_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
// Insert appearance related settings.
if _, err := db.Exec(`
INSERT INTO settings (key, value) VALUES

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V2_2_0 performs the DB migrations for v.2.2.0.
func V2_2_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V2_2_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
if _, err := db.Exec(`
DO $$
BEGIN

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V2_2_0 performs the DB migrations for v.2.3.0.
func V2_3_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V2_3_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
if _, err := db.Exec(`ALTER TABLE media ADD COLUMN IF NOT EXISTS "meta" JSONB NOT NULL DEFAULT '{}'`); err != nil {
return err
}

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V2_4_0 performs the DB migrations.
func V2_4_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V2_4_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
// Insert new preference settings.
if _, err := db.Exec(`
INSERT INTO settings (key, value) VALUES

View file

@ -1,13 +1,15 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V2_5_0 performs the DB migrations.
func V2_5_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V2_5_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
// Insert new preference settings.
if _, err := db.Exec(`
INSERT INTO settings (key, value) VALUES

View file

@ -1,15 +1,25 @@
package migrations
import (
"log"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V3_0_0 performs the DB migrations.
func V3_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
func V3_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
lo.Println("IMPORTANT: this upgrade might take a while if you have a large database. Please be patient ...")
// Insert new preference settings.
if _, err := db.Exec(`INSERT INTO settings (key, value) VALUES ('bounce.postmark', '{"enabled": false, "username": "", "password": ""}') ON CONFLICT DO NOTHING;`); err != nil {
if _, err := db.Exec(`
INSERT INTO settings (key, value) VALUES
('bounce.postmark', '{"enabled": false, "username": "", "password": ""}'),
('app.cache_slow_queries', 'false'),
('app.cache_slow_queries_interval', '"0 3 * * *"')
ON CONFLICT DO NOTHING;
`); err != nil {
return err
}
@ -22,5 +32,106 @@ func V3_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
return err
}
// Add indexes that make sorting faster on large tables.
if _, err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_subs_created_at ON subscribers(created_at);
CREATE INDEX IF NOT EXISTS idx_subs_updated_at ON subscribers(updated_at);
CREATE INDEX IF NOT EXISTS idx_camps_status ON campaigns(status);
CREATE INDEX IF NOT EXISTS idx_camps_name ON campaigns(name);
CREATE INDEX IF NOT EXISTS idx_camps_created_at ON campaigns(created_at);
CREATE INDEX IF NOT EXISTS idx_camps_updated_at ON campaigns(updated_at);
CREATE INDEX IF NOT EXISTS idx_lists_type ON lists(type);
CREATE INDEX IF NOT EXISTS idx_lists_optin ON lists(optin);
CREATE INDEX IF NOT EXISTS idx_lists_name ON lists(name);
CREATE INDEX IF NOT EXISTS idx_lists_created_at ON lists(created_at);
CREATE INDEX IF NOT EXISTS idx_lists_updated_at ON lists(updated_at);
`); err != nil {
return err
}
// Create materialized views for slow aggregate queries.
if _, err := db.Exec(`
-- dashboard stats
CREATE MATERIALIZED VIEW IF NOT EXISTS mat_dashboard_counts AS
WITH subs AS (
SELECT COUNT(*) AS num, status FROM subscribers GROUP BY status
)
SELECT NOW() AS updated_at,
JSON_BUILD_OBJECT(
'subscribers', JSON_BUILD_OBJECT(
'total', (SELECT SUM(num) FROM subs),
'blocklisted', (SELECT num FROM subs WHERE status='blocklisted'),
'orphans', (
SELECT COUNT(id) FROM subscribers
LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
WHERE subscriber_lists.subscriber_id IS NULL
)
),
'lists', JSON_BUILD_OBJECT(
'total', (SELECT COUNT(*) FROM lists),
'private', (SELECT COUNT(*) FROM lists WHERE type='private'),
'public', (SELECT COUNT(*) FROM lists WHERE type='public'),
'optin_single', (SELECT COUNT(*) FROM lists WHERE optin='single'),
'optin_double', (SELECT COUNT(*) FROM lists WHERE optin='double')
),
'campaigns', JSON_BUILD_OBJECT(
'total', (SELECT COUNT(*) FROM campaigns),
'by_status', (
SELECT JSON_OBJECT_AGG (status, num) FROM
(SELECT status, COUNT(*) AS num FROM campaigns GROUP BY status) r
)
),
'messages', (SELECT SUM(sent) AS messages FROM campaigns)
) AS data;
CREATE UNIQUE INDEX IF NOT EXISTS mat_dashboard_stats_idx ON mat_dashboard_counts (updated_at);
CREATE MATERIALIZED VIEW IF NOT EXISTS mat_dashboard_charts AS
WITH clicks AS (
SELECT JSON_AGG(ROW_TO_JSON(row))
FROM (
WITH viewDates AS (
SELECT TIMEZONE('UTC', created_at)::DATE AS to_date,
TIMEZONE('UTC', created_at)::DATE - INTERVAL '30 DAY' AS from_date
FROM link_clicks ORDER BY id DESC LIMIT 1
)
SELECT COUNT(*) AS count, created_at::DATE as date FROM link_clicks
-- use > between < to force the use of the date index.
WHERE TIMEZONE('UTC', created_at)::DATE BETWEEN (SELECT from_date FROM viewDates) AND (SELECT to_date FROM viewDates)
GROUP by date ORDER BY date
) row
),
views AS (
SELECT JSON_AGG(ROW_TO_JSON(row))
FROM (
WITH viewDates AS (
SELECT TIMEZONE('UTC', created_at)::DATE AS to_date,
TIMEZONE('UTC', created_at)::DATE - INTERVAL '30 DAY' AS from_date
FROM campaign_views ORDER BY id DESC LIMIT 1
)
SELECT COUNT(*) AS count, created_at::DATE as date FROM campaign_views
-- use > between < to force the use of the date index.
WHERE TIMEZONE('UTC', created_at)::DATE BETWEEN (SELECT from_date FROM viewDates) AND (SELECT to_date FROM viewDates)
GROUP by date ORDER BY date
) row
)
SELECT NOW() AS updated_at, JSON_BUILD_OBJECT('link_clicks', COALESCE((SELECT * FROM clicks), '[]'),
'campaign_views', COALESCE((SELECT * FROM views), '[]')
) AS data;
CREATE UNIQUE INDEX IF NOT EXISTS mat_dashboard_charts_idx ON mat_dashboard_charts (updated_at);
-- subscriber counts stats for lists
CREATE MATERIALIZED VIEW IF NOT EXISTS mat_list_subscriber_stats AS
SELECT NOW() AS updated_at, lists.id AS list_id, subscriber_lists.status, COUNT(*) AS subscriber_count FROM lists
LEFT JOIN subscriber_lists ON (subscriber_lists.list_id = lists.id)
GROUP BY lists.id, subscriber_lists.status
UNION ALL
SELECT NOW() AS updated_at, 0 AS list_id, NULL AS status, COUNT(*) AS subscriber_count FROM subscribers;
CREATE UNIQUE INDEX IF NOT EXISTS mat_list_subscriber_stats_idx ON mat_list_subscriber_stats (list_id, status);
`); err != nil {
return err
}
return nil
}

View file

@ -37,15 +37,16 @@ type Queries struct {
ExportSubscriberData *sqlx.Stmt `query:"export-subscriber-data"`
// Non-prepared arbitrary subscriber queries.
QuerySubscribers string `query:"query-subscribers"`
QuerySubscribersCount string `query:"query-subscribers-count"`
QuerySubscribersForExport string `query:"query-subscribers-for-export"`
QuerySubscribersTpl string `query:"query-subscribers-template"`
DeleteSubscribersByQuery string `query:"delete-subscribers-by-query"`
AddSubscribersToListsByQuery string `query:"add-subscribers-to-lists-by-query"`
BlocklistSubscribersByQuery string `query:"blocklist-subscribers-by-query"`
DeleteSubscriptionsByQuery string `query:"delete-subscriptions-by-query"`
UnsubscribeSubscribersFromListsByQuery string `query:"unsubscribe-subscribers-from-lists-by-query"`
QuerySubscribers string `query:"query-subscribers"`
QuerySubscribersCount string `query:"query-subscribers-count"`
QuerySubscribersCountAll *sqlx.Stmt `query:"query-subscribers-count-all"`
QuerySubscribersForExport string `query:"query-subscribers-for-export"`
QuerySubscribersTpl string `query:"query-subscribers-template"`
DeleteSubscribersByQuery string `query:"delete-subscribers-by-query"`
AddSubscribersToListsByQuery string `query:"add-subscribers-to-lists-by-query"`
BlocklistSubscribersByQuery string `query:"blocklist-subscribers-by-query"`
DeleteSubscriptionsByQuery string `query:"delete-subscriptions-by-query"`
UnsubscribeSubscribersFromListsByQuery string `query:"unsubscribe-subscribers-from-lists-by-query"`
CreateList *sqlx.Stmt `query:"create-list"`
QueryLists string `query:"query-lists"`

View file

@ -15,10 +15,12 @@ type Settings struct {
CheckUpdates bool `json:"app.check_updates"`
AppLang string `json:"app.lang"`
AppBatchSize int `json:"app.batch_size"`
AppConcurrency int `json:"app.concurrency"`
AppMaxSendErrors int `json:"app.max_send_errors"`
AppMessageRate int `json:"app.message_rate"`
AppBatchSize int `json:"app.batch_size"`
AppConcurrency int `json:"app.concurrency"`
AppMaxSendErrors int `json:"app.max_send_errors"`
AppMessageRate int `json:"app.message_rate"`
CacheSlowQueries bool `json:"app.cache_slow_queries"`
CacheSlowQueriesInterval string `json:"app.cache_slow_queries_interval"`
AppMessageSlidingWindow bool `json:"app.message_sliding_window"`
AppMessageSlidingWindowDuration string `json:"app.message_sliding_window_duration"`

View file

@ -324,6 +324,12 @@ SELECT COUNT(*) AS total FROM subscribers
)
WHERE (CARDINALITY($1) = 0 OR subscriber_lists.list_id = ANY($1::INT[])) %s;
-- name: query-subscribers-count-all
-- Cached query for getting the "all" subscriber count without arbitrary conditions.
SELECT SUM(subscriber_count) AS total FROM mat_list_subscriber_stats
WHERE list_id = ANY(CASE WHEN CARDINALITY($1::INT[]) > 0 THEN $1 ELSE '{0}' END)
AND ($2 = '' OR status = $2::subscription_status);
-- name: query-subscribers-for-export
-- raw: true
-- Unprepared statement for issuring arbitrary WHERE conditions for
@ -422,18 +428,15 @@ WITH ls AS (
AND (CARDINALITY($6::VARCHAR(100)[]) = 0 OR $6 <@ tags)
OFFSET $7 LIMIT (CASE WHEN $8 < 1 THEN NULL ELSE $8 END)
),
counts AS (
SELECT list_id, JSON_OBJECT_AGG(status, num) AS subscriber_statuses, SUM(num) AS subscriber_count
FROM (
SELECT list_id, status, COUNT(*) as num
FROM subscriber_lists
WHERE ($1 = 0 OR list_id = $1)
GROUP BY list_id, status
) AS subquery GROUP BY list_id
statuses AS (
SELECT
list_id,
COALESCE(JSONB_OBJECT_AGG(status, subscriber_count) FILTER (WHERE status IS NOT NULL), '{}') AS subscriber_statuses
FROM mat_list_subscriber_stats
GROUP BY list_id
)
SELECT ls.*, subscriber_statuses FROM ls
LEFT JOIN counts ON (counts.list_id = ls.id) ORDER BY %order%;
SELECT ls.*, COALESCE(ss.subscriber_statuses, '{}') AS subscriber_statuses
FROM ls LEFT JOIN statuses ss ON (ls.id = ss.list_id) ORDER BY %order%;
-- name: get-lists-by-optin
-- Can have a list of IDs or a list of UUIDs.
@ -960,71 +963,13 @@ INSERT INTO link_clicks (campaign_id, subscriber_id, link_id) VALUES(
) RETURNING (SELECT url FROM link);
-- name: get-dashboard-charts
WITH clicks AS (
SELECT JSON_AGG(ROW_TO_JSON(row))
FROM (
WITH viewDates AS (
SELECT TIMEZONE('UTC', created_at)::DATE AS to_date,
TIMEZONE('UTC', created_at)::DATE - INTERVAL '30 DAY' AS from_date
FROM link_clicks ORDER BY id DESC LIMIT 1
)
SELECT COUNT(*) AS count, created_at::DATE as date FROM link_clicks
-- use > between < to force the use of the date index.
WHERE TIMEZONE('UTC', created_at)::DATE BETWEEN (SELECT from_date FROM viewDates) AND (SELECT to_date FROM viewDates)
GROUP by date ORDER BY date
) row
),
views AS (
SELECT JSON_AGG(ROW_TO_JSON(row))
FROM (
WITH viewDates AS (
SELECT TIMEZONE('UTC', created_at)::DATE AS to_date,
TIMEZONE('UTC', created_at)::DATE - INTERVAL '30 DAY' AS from_date
FROM campaign_views ORDER BY id DESC LIMIT 1
)
SELECT COUNT(*) AS count, created_at::DATE as date FROM campaign_views
-- use > between < to force the use of the date index.
WHERE TIMEZONE('UTC', created_at)::DATE BETWEEN (SELECT from_date FROM viewDates) AND (SELECT to_date FROM viewDates)
GROUP by date ORDER BY date
) row
)
SELECT JSON_BUILD_OBJECT('link_clicks', COALESCE((SELECT * FROM clicks), '[]'),
'campaign_views', COALESCE((SELECT * FROM views), '[]'));
SELECT data FROM mat_dashboard_charts;
-- name: get-dashboard-counts
WITH subs AS (
SELECT COUNT(*) AS num, status FROM subscribers GROUP BY status
)
SELECT JSON_BUILD_OBJECT('subscribers', JSON_BUILD_OBJECT(
'total', (SELECT SUM(num) FROM subs),
'blocklisted', (SELECT num FROM subs WHERE status='blocklisted'),
'orphans', (
SELECT COUNT(id) FROM subscribers
LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
WHERE subscriber_lists.subscriber_id IS NULL
)
),
'lists', JSON_BUILD_OBJECT(
'total', (SELECT COUNT(*) FROM lists),
'private', (SELECT COUNT(*) FROM lists WHERE type='private'),
'public', (SELECT COUNT(*) FROM lists WHERE type='public'),
'optin_single', (SELECT COUNT(*) FROM lists WHERE optin='single'),
'optin_double', (SELECT COUNT(*) FROM lists WHERE optin='double')
),
'campaigns', JSON_BUILD_OBJECT(
'total', (SELECT COUNT(*) FROM campaigns),
'by_status', (
SELECT JSON_OBJECT_AGG (status, num) FROM
(SELECT status, COUNT(*) AS num FROM campaigns GROUP BY status) r
)
),
'messages', (SELECT SUM(sent) AS messages FROM campaigns));
SELECT data FROM mat_dashboard_counts;
-- name: get-settings
SELECT JSON_OBJECT_AGG(key, value) AS settings
FROM (
SELECT * FROM settings ORDER BY key
) t;
SELECT JSON_OBJECT_AGG(key, value) AS settings FROM (SELECT * FROM settings ORDER BY key) t;
-- name: update-settings
UPDATE settings AS s SET value = c.value

View file

@ -23,6 +23,8 @@ CREATE TABLE subscribers (
);
DROP INDEX IF EXISTS idx_subs_email; CREATE UNIQUE INDEX idx_subs_email ON subscribers(LOWER(email));
DROP INDEX IF EXISTS idx_subs_status; CREATE INDEX idx_subs_status ON subscribers(status);
DROP INDEX IF EXISTS idx_subs_created_at; CREATE INDEX idx_subs_created_at ON subscribers(created_at);
DROP INDEX IF EXISTS idx_subs_updated_at; CREATE INDEX idx_subs_updated_at ON subscribers(updated_at);
-- lists
DROP TABLE IF EXISTS lists CASCADE;
@ -38,6 +40,12 @@ CREATE TABLE lists (
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
DROP INDEX IF EXISTS idx_lists_type; CREATE INDEX idx_lists_type ON lists(type);
DROP INDEX IF EXISTS idx_lists_optin; CREATE INDEX idx_lists_optin ON lists(optin);
DROP INDEX IF EXISTS idx_lists_name; CREATE INDEX idx_lists_name ON lists(name);
DROP INDEX IF EXISTS idx_lists_created_at; CREATE INDEX idx_lists_created_at ON lists(created_at);
DROP INDEX IF EXISTS idx_lists_updated_at; CREATE INDEX idx_lists_updated_at ON lists(updated_at);
DROP TABLE IF EXISTS subscriber_lists CASCADE;
CREATE TABLE subscriber_lists (
@ -111,6 +119,11 @@ CREATE TABLE campaigns (
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
DROP INDEX IF EXISTS idx_camps_status; CREATE INDEX idx_camps_status ON campaigns(status);
DROP INDEX IF EXISTS idx_camps_name; CREATE INDEX idx_camps_name ON campaigns(name);
DROP INDEX IF EXISTS idx_camps_created_at; CREATE INDEX idx_camps_created_at ON campaigns(created_at);
DROP INDEX IF EXISTS idx_camps_updated_at; CREATE INDEX idx_camps_updated_at ON campaigns(updated_at);
DROP TABLE IF EXISTS campaign_lists CASCADE;
CREATE TABLE campaign_lists (
@ -212,6 +225,8 @@ INSERT INTO settings (key, value) VALUES
('app.message_sliding_window', 'false'),
('app.message_sliding_window_duration', '"1h"'),
('app.message_sliding_window_rate', '10000'),
('app.cache_slow_queries', 'false'),
('app.cache_slow_queries_interval', '"0 3 * * *"'),
('app.enable_public_archive', 'true'),
('app.enable_public_subscription_page', 'true'),
('app.enable_public_archive_rss_content', 'true'),
@ -279,3 +294,88 @@ DROP INDEX IF EXISTS idx_bounces_sub_id; CREATE INDEX idx_bounces_sub_id ON boun
DROP INDEX IF EXISTS idx_bounces_camp_id; CREATE INDEX idx_bounces_camp_id ON bounces(campaign_id);
DROP INDEX IF EXISTS idx_bounces_source; CREATE INDEX idx_bounces_source ON bounces(source);
DROP INDEX IF EXISTS idx_bounces_date; CREATE INDEX idx_bounces_date ON bounces((TIMEZONE('UTC', created_at)::DATE));
-- materialized views
-- dashboard stats
DROP MATERIALIZED VIEW IF EXISTS mat_dashboard_counts;
CREATE MATERIALIZED VIEW mat_dashboard_counts AS
WITH subs AS (
SELECT COUNT(*) AS num, status FROM subscribers GROUP BY status
)
SELECT NOW() AS updated_at,
JSON_BUILD_OBJECT(
'subscribers', JSON_BUILD_OBJECT(
'total', (SELECT SUM(num) FROM subs),
'blocklisted', (SELECT num FROM subs WHERE status='blocklisted'),
'orphans', (
SELECT COUNT(id) FROM subscribers
LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
WHERE subscriber_lists.subscriber_id IS NULL
)
),
'lists', JSON_BUILD_OBJECT(
'total', (SELECT COUNT(*) FROM lists),
'private', (SELECT COUNT(*) FROM lists WHERE type='private'),
'public', (SELECT COUNT(*) FROM lists WHERE type='public'),
'optin_single', (SELECT COUNT(*) FROM lists WHERE optin='single'),
'optin_double', (SELECT COUNT(*) FROM lists WHERE optin='double')
),
'campaigns', JSON_BUILD_OBJECT(
'total', (SELECT COUNT(*) FROM campaigns),
'by_status', (
SELECT JSON_OBJECT_AGG (status, num) FROM
(SELECT status, COUNT(*) AS num FROM campaigns GROUP BY status) r
)
),
'messages', (SELECT SUM(sent) AS messages FROM campaigns)
) AS data;
DROP INDEX IF EXISTS mat_dashboard_stats_idx; CREATE UNIQUE INDEX mat_dashboard_stats_idx ON mat_dashboard_counts (updated_at);
DROP MATERIALIZED VIEW IF EXISTS mat_dashboard_charts;
CREATE MATERIALIZED VIEW mat_dashboard_charts AS
WITH clicks AS (
SELECT JSON_AGG(ROW_TO_JSON(row))
FROM (
WITH viewDates AS (
SELECT TIMEZONE('UTC', created_at)::DATE AS to_date,
TIMEZONE('UTC', created_at)::DATE - INTERVAL '30 DAY' AS from_date
FROM link_clicks ORDER BY id DESC LIMIT 1
)
SELECT COUNT(*) AS count, created_at::DATE as date FROM link_clicks
-- use > between < to force the use of the date index.
WHERE TIMEZONE('UTC', created_at)::DATE BETWEEN (SELECT from_date FROM viewDates) AND (SELECT to_date FROM viewDates)
GROUP by date ORDER BY date
) row
),
views AS (
SELECT JSON_AGG(ROW_TO_JSON(row))
FROM (
WITH viewDates AS (
SELECT TIMEZONE('UTC', created_at)::DATE AS to_date,
TIMEZONE('UTC', created_at)::DATE - INTERVAL '30 DAY' AS from_date
FROM campaign_views ORDER BY id DESC LIMIT 1
)
SELECT COUNT(*) AS count, created_at::DATE as date FROM campaign_views
-- use > between < to force the use of the date index.
WHERE TIMEZONE('UTC', created_at)::DATE BETWEEN (SELECT from_date FROM viewDates) AND (SELECT to_date FROM viewDates)
GROUP by date ORDER BY date
) row
)
SELECT NOW() AS updated_at, JSON_BUILD_OBJECT('link_clicks', COALESCE((SELECT * FROM clicks), '[]'),
'campaign_views', COALESCE((SELECT * FROM views), '[]')
) AS data;
DROP INDEX IF EXISTS mat_dashboard_charts_idx; CREATE UNIQUE INDEX mat_dashboard_charts_idx ON mat_dashboard_charts (updated_at);
-- subscriber counts stats for lists
DROP MATERIALIZED VIEW IF EXISTS mat_list_subscriber_stats;
CREATE MATERIALIZED VIEW mat_list_subscriber_stats AS
SELECT NOW() AS updated_at, lists.id AS list_id, subscriber_lists.status, COUNT(*) AS subscriber_count FROM lists
LEFT JOIN subscriber_lists ON (subscriber_lists.list_id = lists.id)
GROUP BY lists.id, subscriber_lists.status
UNION ALL
SELECT NOW() AS updated_at, 0 AS list_id, NULL AS status, COUNT(*) AS subscriber_count FROM subscribers;
DROP INDEX IF EXISTS mat_list_subscriber_stats_idx; CREATE UNIQUE INDEX mat_list_subscriber_stats_idx ON mat_list_subscriber_stats (list_id, status);