diff --git a/analyser/analyser.py b/analyser/analyser.py index 44d26380..5043d62a 100644 --- a/analyser/analyser.py +++ b/analyser/analyser.py @@ -128,26 +128,25 @@ def preprocess_agents(self, agents, collector, events): ['dst_instid']].groupby('dst_instid').size().rename('hit_count') agents = agents.join(agents_that_get_hit_a_lot) agents.hit_count.fillna(0, inplace=True) - + + #Fix player parties + if (self.boss_info.force_single_party) | (not agents[(agents.prof >= 1) & (agents.prof <= 9) & (agents.party == 0)].empty): + agents.loc[(agents.prof >= 1) & (agents.prof <= 9), 'party'] = 1 + #identify specific ones we care about players = agents[(agents.prof >= 1) & (agents.prof <= 9)] - + if len(players) < 1: raise EvtcAnalysisException("No players found in this log") elif len(players) > 50: raise EvtcAnalysisException("Too many players found in this log: {0}".format(len(agents))) - if not players[players.party == 0].empty: - for player in players.index.values: - agents.loc[player, 'party'] = 1 - players = agents[(agents.prof >= 1) & (agents.prof <= 9)] - bosses = agents[(agents.prof.isin(self.boss_info.boss_ids)) | (self.boss_info.has_structure_boss & (agents.prof < 0) & (agents.hit_count >= 100))] final_bosses = agents[agents.prof == self.boss_info.boss_ids[-1]] - + #set up important preprocessed data self.subgroups = dict([(number, subgroup.index.values) for number, subgroup in players.groupby("party")]) @@ -157,7 +156,7 @@ def preprocess_agents(self, agents, collector, events): print(self.boss_instids) self.final_boss_instids = final_bosses.index.values collector.set_context_value(ContextType.AGENT_NAME, create_mapping(agents, 'name')) - return players, bosses, final_bosses + return agents, players, bosses, final_bosses def preprocess_events(self, events): #experimental phase calculations @@ -253,7 +252,7 @@ def __init__(self, encounter): agents = encounter.agents skills = encounter.skills - players, bosses, final_bosses = self.preprocess_agents(agents, collector, events) + agents, players, bosses, final_bosses = self.preprocess_agents(agents, collector, events) self.preprocess_skills(skills, collector) self.players = players diff --git a/analyser/bosses.py b/analyser/bosses.py index 2935c3ad..b5f69dad 100644 --- a/analyser/bosses.py +++ b/analyser/bosses.py @@ -47,7 +47,7 @@ def __repr__(self): return "%s (%s, %s)" % (self.name, self.data_type, self.desired) class Boss: - def __init__(self, name, kind, boss_ids, metrics=None, sub_boss_ids=None, key_npc_ids = None, phases=None, despawns_instead_of_dying = False, has_structure_boss = False, success_health_limit = None, cm_detector = no_cm): + def __init__(self, name, kind, boss_ids, metrics=None, sub_boss_ids=None, key_npc_ids = None, phases=None, despawns_instead_of_dying = False, has_structure_boss = False, success_health_limit = None, cm_detector = no_cm, force_single_party = False): self.name = name self.kind = kind self.boss_ids = boss_ids @@ -59,6 +59,7 @@ def __init__(self, name, kind, boss_ids, metrics=None, sub_boss_ids=None, key_np self.has_structure_boss = has_structure_boss self.success_health_limit = success_health_limit self.cm_detector = cm_detector + self.force_single_party = force_single_party class Phase: def __init__(self, name, important, @@ -270,14 +271,14 @@ def find_end_time(self, Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 15000), Phase("Second split", False, phase_end_damage_start = 15000), Phase("Phase 3", True, phase_end_health=1) - ], cm_detector = yes_cm), + ], cm_detector = yes_cm, force_single_party = True), Boss('Artsariiv (CM)', Kind.FRACTAL, [0x461d], despawns_instead_of_dying = True, success_health_limit = 3, phases = [ Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 10000), Phase("First split", False, phase_end_damage_start = 10000), Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 10000), Phase("Second split", False, phase_end_damage_start = 10000), Phase("Phase 3", True, phase_end_health=1) - ], cm_detector = yes_cm), + ], cm_detector = yes_cm, force_single_party = True), Boss('Arkk (CM)', Kind.FRACTAL,[0x455f], despawns_instead_of_dying = True, success_health_limit = 3, phases =[ Phase("100-80", True, phase_end_health = 80, phase_end_damage_stop = 10000), Phase("First orb", False, phase_end_damage_start = 10000), @@ -290,7 +291,7 @@ def find_end_time(self, Phase("40-30", True, phase_end_health = 30, phase_end_damage_stop = 10000), Phase("Third orb", False, phase_end_damage_start = 10000), Phase("30-0", True, phase_end_health = 1, phase_end_damage_stop = 10000) - ], cm_detector = yes_cm), + ], cm_detector = yes_cm, force_single_party = True), Boss('MAMA (CM)', Kind.FRACTAL, [0x427d], phases = [ Phase("Phase 1", True, phase_end_health = 75, phase_end_damage_stop = 3000), Phase("First split", False, phase_end_damage_start = 3000), @@ -299,14 +300,14 @@ def find_end_time(self, Phase("Phase 3", True, phase_end_health = 25, phase_end_damage_stop = 3000), Phase("Second split", False, phase_end_damage_start = 3000), Phase("Phase 4", True, phase_end_health=1) - ], cm_detector = yes_cm), + ], cm_detector = yes_cm, force_single_party = True), Boss('Siax (CM)', Kind.FRACTAL,[0x4284], phases = [ Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 15000), Phase("First split", False, phase_end_damage_start = 15000), Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 15000), Phase("Second split", False, phase_end_damage_start = 15000), Phase("Phase 3", True, phase_end_health=1) - ], cm_detector = yes_cm), + ], cm_detector = yes_cm, force_single_party = True), Boss('Ensolyss (CM)', Kind.FRACTAL,[0x4234], phases = [ Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 15000), Phase("First split", False, phase_end_damage_start = 15000), @@ -314,6 +315,6 @@ def find_end_time(self, Phase("Second split", False, phase_end_damage_start = 15000), Phase("Phase 3", True, phase_end_health=15), Phase("Phase 4", True, phase_end_health=1) - ], cm_detector = yes_cm) + ], cm_detector = yes_cm, force_single_party = True) ] BOSSES = {boss.boss_ids[0]: boss for boss in BOSS_ARRAY} diff --git a/raidar/management/commands/process_uploads.py b/raidar/management/commands/process_uploads.py index 82ce5cc8..19f6dca2 100644 --- a/raidar/management/commands/process_uploads.py +++ b/raidar/management/commands/process_uploads.py @@ -205,6 +205,9 @@ def analyse_upload(self, upload): started_at = dump['Category']['encounter']['start'] duration = dump['Category']['encounter']['duration'] success = dump['Category']['encounter']['success'] + upload_val = upload.val + category_id = upload_val.get('category_id', None) + tagstring = upload_val.get('tagstring', '') if duration < 60: raise EvtcAnalysisException('Encounter shorter than 60s') @@ -238,6 +241,8 @@ def analyse_upload(self, upload): encounter.started_at = started_at encounter.started_at_full = started_at_full encounter.started_at_half = started_at_half + encounter.category_id = category_id + encounter.tagstring = tagstring if not zipfile: encounter.filename += ".zip" encounter.save() @@ -248,8 +253,10 @@ def analyse_upload(self, upload): duration=duration, success=success, val=dump, area=area, era=era, started_at=started_at, started_at_full=started_at_full, started_at_half=started_at_half, + category_id=category_id, account_hash=account_hash ) + encounter.tagstring = tagstring file.close() file = None diff --git a/raidar/management/commands/restat.py b/raidar/management/commands/restat.py index 3ae2d957..26c8e042 100644 --- a/raidar/management/commands/restat.py +++ b/raidar/management/commands/restat.py @@ -1,5 +1,6 @@ from ._qsetiter import queryset_iterator from collections import defaultdict +from functools import partial from contextlib import contextmanager from django.core.management.base import BaseCommand, CommandError from django.db import transaction @@ -90,10 +91,7 @@ def bound_stats(output, name, value): if minprop not in output or value < output[minprop]: output[minprop] = value -def advanced_stats(maximum_percentile_samples): - return lambda a,b,c: advanced_stats_internal(maximum_percentile_samples, a, b, c) - -def advanced_stats_internal(maximum_percentile_samples, output, name, value): +def advanced_stats(maximum_percentile_samples, output, name, value): bound_stats(output, name, value) average_stats(output, name, value) l = output.get('values|' + name, []) @@ -155,11 +153,10 @@ def calculate_standard_stats(f, stats, main_stat_targets, incoming_buff_targets, incoming_buff_stats = _safe_get(lambda: stats['Metrics']['buffs']['From']['*All'], {}) for stat in ['dps','crit','seaweed','scholar','flanking']: - calculate(main_stat_targets, f, stat, _safe_get(lambda: stats_in_phase_to_all[stat])) - calculate(main_stat_targets, f, 'dps_boss', _safe_get(lambda: stats_in_phase_to_boss['dps'])) - calculate(main_stat_targets, f, 'dps_received', _safe_get(lambda: stats_in_phase_from_all['dps'])) - calculate(main_stat_targets, f, 'total_received', _safe_get(lambda: stats_in_phase_from_all['total'])) - calculate(main_stat_targets, f, 'total_shielded', _safe_get(lambda: shielded_in_phase_from_all['total'])) + calculate(main_stat_targets, f, stat, stats_in_phase_to_all.get(stat, 0)) + calculate(main_stat_targets, f, 'dps_boss', stats_in_phase_to_boss.get('dps', 0)) + calculate(main_stat_targets, f, 'dps_received', stats_in_phase_from_all.get('dps', 0)) + calculate(main_stat_targets, f, 'total_received', stats_in_phase_from_all.get('total', 0)) for buff, value in incoming_buff_stats.items(): calculate(incoming_buff_targets, f, buff, value) @@ -250,7 +247,7 @@ def calculate_stats(self, *args, **options): "area": {}, "user": {} } - era_queryset = era.encounters.all().order_by('?') + era_queryset = era.encounters.prefetch_related('participations__character', 'participations__character__account').all().order_by('?') totals_in_era = {} for encounter in queryset_iterator(era_queryset): boss = BOSSES[encounter.area_id] @@ -260,6 +257,7 @@ def calculate_stats(self, *args, **options): phases = data['Category']['combat']['Phase'] totals_in_area = navigate(totals['area'], encounter.area_id) + participations = encounter.participations.all() for phase, stats_in_phase in phases.items(): squad_stats = stats_in_phase['Subgroup']['*All'] phase_duration = data['Category']['encounter']['duration'] if phase == 'All' else _safe_get(lambda: data['Category']['encounter']['Phase'][phase]['duration']) @@ -274,12 +272,12 @@ def calculate_stats(self, *args, **options): if(encounter.success): calculate([group_totals, group_totals_era], - advanced_stats(options['percentile_samples']), + partial(advanced_stats, options['percentile_samples']), 'duration', phase_duration) calculate([group_totals, group_totals_era], count) calculate_standard_stats( - advanced_stats(options['percentile_samples']), + partial(advanced_stats, options['percentile_samples']), squad_stats, [group_totals, group_totals_era], [buffs_by_party, buffs_by_party_era], @@ -287,7 +285,6 @@ def calculate_stats(self, *args, **options): individual_totals = navigate(totals_in_area, phase, 'individual') individual_totals_era = navigate(totals_in_era, phase, 'individual') - participations = encounter.participations.select_related('character', 'character__account').all() for participation in participations: # XXX in case player did not actually participate (hopefully fix in analyser) if (participation.character.name not in stats_in_phase['Player']): @@ -315,7 +312,7 @@ def calculate_stats(self, *args, **options): calculate([totals_by_build, totals_by_archetype, totals_by_spec, individual_totals, totals_by_build_era, totals_by_archetype_era, totals_by_spec_era, individual_totals_era], count) calculate_standard_stats( - advanced_stats(options['percentile_samples']), + partial(advanced_stats, options['percentile_samples']), player_stats, [totals_by_build, totals_by_archetype, totals_by_spec, individual_totals, totals_by_build_era, totals_by_archetype_era, totals_by_spec_era, individual_totals_era], @@ -337,11 +334,11 @@ def calculate_stats(self, *args, **options): player_stats, profile_output.breakdown, [], - map(lambda a: navigate(a, 'outgoing'), profile_output.breakdown)) + [navigate(a, 'outgoing') for a in profile_output.breakdown]) - dead_percentage = 100 * _safe_get(lambda: stats_in_phase_events['dead_time']) / duration - down_percentage = 100 * _safe_get(lambda: stats_in_phase_events['down_time']) / duration - disconnect_percentage = 100 * _safe_get(lambda: stats_in_phase_events['disconnect_time']) / duration + dead_percentage = 100 * stats_in_phase_events.get('dead_time', 0) / duration + down_percentage = 100 * stats_in_phase_events.get('down_time', 0) / duration + disconnect_percentage = 100 * stats_in_phase_events.get('disconnect_time', 0) / duration calculate(profile_output.all, average_stats, 'dead_percentage', dead_percentage) calculate(profile_output.all, average_stats, 'down_percentage', down_percentage) diff --git a/raidar/migrations/0028_value_model.py b/raidar/migrations/0028_value_model.py new file mode 100644 index 00000000..75a2eaba --- /dev/null +++ b/raidar/migrations/0028_value_model.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-10-26 06:48 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0027_era_default_sort'), + ] + + operations = [ + migrations.AlterField( + model_name='encounter', + name='value', + field=models.TextField(default='{}', editable=False), + ), + migrations.AlterField( + model_name='era', + name='value', + field=models.TextField(default='{}', editable=False), + ), + migrations.AlterField( + model_name='eraareastore', + name='value', + field=models.TextField(default='{}', editable=False), + ), + migrations.AlterField( + model_name='erauserstore', + name='value', + field=models.TextField(default='{}', editable=False), + ), + migrations.AlterField( + model_name='notification', + name='value', + field=models.TextField(default='{}', editable=False), + ), + migrations.AlterField( + model_name='variable', + name='value', + field=models.TextField(default='{}', editable=False), + ), + ] diff --git a/raidar/migrations/0029_upload_value.py b/raidar/migrations/0029_upload_value.py new file mode 100644 index 00000000..6fce1325 --- /dev/null +++ b/raidar/migrations/0029_upload_value.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-10-26 06:50 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0028_value_model'), + ] + + operations = [ + migrations.AddField( + model_name='upload', + name='value', + field=models.TextField(default='{}', editable=False), + ), + ] diff --git a/raidar/models.py b/raidar/models.py index d51c608d..131aad98 100644 --- a/raidar/models.py +++ b/raidar/models.py @@ -43,6 +43,25 @@ User._meta.get_field('email')._unique = True + + +class ValueModel(models.Model): + value = models.TextField(default="{}", editable=False) + + @property + def val(self): + return json_loads(self.value) + + @val.setter + def val(self, value): + self.value = json_dumps(value) + + class Meta: + abstract = True + + + + class UserProfile(models.Model): PRIVATE = 1 SQUAD = 2 @@ -156,19 +175,10 @@ class Meta: ordering = ('name',) -class Era(models.Model): +class Era(ValueModel): started_at = models.IntegerField(db_index=True) name = models.CharField(max_length=255, null=True) description = models.TextField(null=True) - value = models.TextField(default="{}") - - @property - def val(self): - return json_loads(self.value) - - @val.setter - def val(self, value): - self.value = json_dumps(value) def __str__(self): return "%s (#%d)" % (self.name or "", self.id) @@ -191,7 +201,7 @@ class Meta: verbose_name_plural = "categories" -class Upload(models.Model): +class Upload(ValueModel): filename = models.CharField(max_length=255) uploaded_at = models.IntegerField(db_index=True) uploaded_by = models.ForeignKey(User, related_name='unprocessed_uploads') @@ -219,35 +229,17 @@ def _delete_upload_file(sender, instance, using, **kwargs): post_delete.connect(_delete_upload_file, sender=Upload) -class Notification(models.Model): +class Notification(ValueModel): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications') - value = models.TextField(default="{}") created_at = models.IntegerField(db_index=True, default=time) - @property - def val(self): - return json_loads(self.value) - - @val.setter - def val(self, value): - self.value = json_dumps(value) - -class Variable(models.Model): +class Variable(ValueModel): key = models.CharField(max_length=255, primary_key=True) - value = models.TextField(null=True) def __str__(self): return '%s=%s' % (self.key, self.val) - @property - def val(self): - return json_loads(self.value) - - @val.setter - def val(self, value): - self.value = json_dumps(value) - def get(name): return Variable.objects.get(key=name).val @@ -264,7 +256,7 @@ def _dictionary(): def _generate_url_id(size=5): return ''.join(w.capitalize() for w in random.sample(_dictionary(), size)) -class Encounter(models.Model): +class Encounter(ValueModel): url_id = models.TextField(max_length=255, editable=False, unique=True, default=_generate_url_id, verbose_name="URL ID") started_at = models.IntegerField(db_index=True) duration = models.FloatField() @@ -276,7 +268,6 @@ class Encounter(models.Model): era = models.ForeignKey(Era, on_delete=models.PROTECT, related_name='encounters') category = models.ForeignKey(Category, on_delete=models.SET_NULL, related_name='encounters', null=True) characters = models.ManyToManyField(Character, through='Participation', related_name='encounters') - value = models.TextField(editable=False) # hack to try to ensure uniqueness account_hash = models.CharField(max_length=32, editable=False) started_at_full = models.IntegerField(editable=False) @@ -286,14 +277,6 @@ class Encounter(models.Model): gdrive_url = models.CharField(max_length=255, editable=False, null=True) tags = TaggableManager(blank=True) - @property - def val(self): - return json_loads(self.value) - - @val.setter - def val(self, value): - self.value = json_dumps(value) - def __str__(self): return '%s (%s, %s, #%s)' % (self.area.name, self.filename, self.uploaded_by.username, self.id) @@ -395,29 +378,11 @@ class Meta: unique_together = ('encounter', 'character') -class EraAreaStore(models.Model): +class EraAreaStore(ValueModel): era = models.ForeignKey(Era, on_delete=models.CASCADE, related_name="era_area_stores") area = models.ForeignKey(Area, on_delete=models.CASCADE, related_name="era_area_stores") - value = models.TextField(default="{}") - - @property - def val(self): - return json_loads(self.value) - - @val.setter - def val(self, value): - self.value = json_dumps(value) -class EraUserStore(models.Model): +class EraUserStore(ValueModel): era = models.ForeignKey(Era, on_delete=models.CASCADE, related_name="era_user_stores") user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="era_user_stores") - value = models.TextField(default="{}") - - @property - def val(self): - return json_loads(self.value) - - @val.setter - def val(self, value): - self.value = json_dumps(value) diff --git a/raidar/static/raidar/script.js b/raidar/static/raidar/script.js index 5948cc78..84cefaef 100644 --- a/raidar/static/raidar/script.js +++ b/raidar/static/raidar/script.js @@ -64,7 +64,7 @@ }); $(document).ajaxError((evt, xhr, settings, err) => { console.error(err); - error("Error communicating to server") + error("Error communicating to server"); }) function f0X(x) { @@ -413,7 +413,7 @@ ${body} settings: { encounterSort: { prop: 'uploaded_at', dir: 'down', filters: false, filter: { success: null } }, }, - upload: [], + uploads: [], }; let lastNotificationId = window.raidar_data.last_notification_id; let storedSettingsJSON = localStorage.getItem('settings'); @@ -1078,7 +1078,7 @@ ${body} //if (evt.loaded == evt.total) { //} entry.progress = progress; - r.update('upload'); + r.update('uploads'); } let uploadProgressDone = (entry, data) => { if (data.error) { @@ -1088,7 +1088,7 @@ ${body} entry.upload_id = data.upload_id; } delete entry.file; - r.update('upload'); + r.update('uploads'); startUpload(true); } @@ -1107,14 +1107,14 @@ ${body} // entry.encounterId = data.id; // entry.success = true; // delete entry.file; - // r.update('upload'); + // r.update('uploads'); // startUpload(true); // } let uploadProgressFail = entry => { entry.success = false; delete entry.file; - r.update('upload'); + r.update('uploads'); startUpload(true); } @@ -1128,12 +1128,20 @@ ${body} function startUpload(previousIsFinished) { if (uploading && !previousIsFinished) return; - let entry = r.get('upload').find(entry => !("progress" in entry)); + let entry = r.get('uploads').find(entry => !("progress" in entry)); uploading = entry; if (!entry) return; let form = new FormData(); - form.append('file', entry.file); + form.set('file', entry.file); + let category = r.get('upload.category'); + if (category) { + form.set('category', category); + } + let tags = r.get('upload.tags'); + if (tags) { + form.set('tags', r.get('upload.tags')); + } return $.ajax({ url: 'upload.json', data: form, @@ -1149,7 +1157,7 @@ ${body} const notificationHandlers = { upload: notification => { //let entry = uploads.find(entry => entry.upload_id == notification.upload_id); - let entry = r.get('upload').find(entry => entry.name == notification.filename); + let entry = r.get('uploads').find(entry => entry.name == notification.filename); let newEntry = { name: notification.filename, progress: 100, @@ -1161,9 +1169,9 @@ ${body} }; if (entry) { Object.assign(entry, newEntry); - r.update('upload'); + r.update('uploads'); } else { - r.push('upload', newEntry); + r.push('uploads', newEntry); } let encounters = r.get('encounters'); @@ -1174,12 +1182,12 @@ ${body} updateRactiveFromResponse({ encounters: encounters }); }, upload_error: notification => { - let uploads = r.get('upload'); + let uploads = r.get('uploads'); let entry = uploads.find(entry => entry.upload_id == notification.upload_id); if (entry) { entry.success = false; entry.error = notification.error; - r.update('upload'); + r.update('uploads'); } }, }; @@ -1193,6 +1201,21 @@ ${body} handler(notification); } + function upgradeClient() { + notification('Server was upgraded, client will restart in s', { status: 'warning', timeout: 10000 }); + let count = 8; + let cdEl = document.getElementById('upgrade-countdown'); + let loop = () => { + cdEl.textContent = --count; + if (count) { + setTimeout(loop, 1000); + } else { + window.location.reload(true); + } + }; + setTimeout(loop, 1000); + } + const POLL_TIME = 10000; function pollNotifications() { if (r.get('username')) { @@ -1208,7 +1231,10 @@ ${body} lastNotificationId = data.last_id; } data.notifications.forEach(handleNotification); - }).then(() => { + if (data.version != r.get('data.version.id')) { + upgradeClient(); + } + }).always(() => { setTimeout(pollNotifications, POLL_TIME); }); } else { @@ -1237,14 +1263,14 @@ ${body} let jQuery_xhr_factory = $.ajaxSettings.xhr; Array.from(files).forEach(file => { if (!file.name.endsWith('.evtc') && !file.name.endsWith('.evtc.zip')) return; - let entry = r.get('upload').find(entry => entry.name == file.name); + let entry = r.get('uploads').find(entry => entry.name == file.name); if (entry) { delete entry.success; delete entry.progress; entry.file = file; - r.update('upload'); + r.update('uploads'); } else { - r.push('upload', { + r.push('uploads', { name: file.name, file: file, uploaded_by: r.get('username'), diff --git a/raidar/static/raidar/style.css b/raidar/static/raidar/style.css index ffdcbf9b..a091975f 100644 --- a/raidar/static/raidar/style.css +++ b/raidar/static/raidar/style.css @@ -105,9 +105,14 @@ input:invalid, textarea:invalid { display: inline-block; } +select[readonly] { + pointer-events: none; + background-image: none !important; +} input[readonly] + .tags-input { - border: none; pointer-events: none; + border: none; + box-shadow: none; width: inherit !important; } .tags-input.no-background { diff --git a/raidar/templates/raidar/encounter.html b/raidar/templates/raidar/encounter.html index 9606b540..0ee91048 100644 --- a/raidar/templates/raidar/encounter.html +++ b/raidar/templates/raidar/encounter.html @@ -26,7 +26,7 @@

- [[#each data.categories:id]] diff --git a/raidar/templates/raidar/info_releasenotes.html b/raidar/templates/raidar/info_releasenotes.html index e3a3594c..6ef5345d 100644 --- a/raidar/templates/raidar/info_releasenotes.html +++ b/raidar/templates/raidar/info_releasenotes.html @@ -1,10 +1,26 @@

Release Notes

+ +

As with any website, hosting and server costs are always at play. If you love this site, why not consider donating? A one off donation of a dollar or ten dollars is a huge help in providing this website for free and ad-free to the community.

+ + + + + +
+

Version [[data.version.id]] released on [[formatDate(data.version.timestamp)]]

+ +

Version 1.0.1