diff --git a/CardReaderService/CardReaderService/Service1.cs b/CardReaderService/CardReaderService/Service1.cs index 971ea0b..7039de9 100644 --- a/CardReaderService/CardReaderService/Service1.cs +++ b/CardReaderService/CardReaderService/Service1.cs @@ -12,10 +12,10 @@ namespace CardReaderService { private Thread _mainWorkThread; private bool _stopMainWorkerThread; - + private AutoResetEvent _mainWorkerTerminationSignal; private string _readerName = ""; - //private SCardReader _reader; private SCardMonitor _cardMonitor; + private bool _initialised=false; public Service1() { @@ -24,29 +24,30 @@ namespace CardReaderService public void Start() { - OnStart(new string[] {}); + OnStart(new string[] { }); } -// + protected override void OnStart(string[] args) { - Console.WriteLine("Starting.. Getting available readers"); - var ctxFactory = ContextFactory.Instance; - using(var context = ctxFactory.Establish(SCardScope.System)) - { - var readerNames = context.GetReaders(); - if (NoReaderAvailable(readerNames)) - { - Console.WriteLine("No Card Reader is available, Exiting.."); - return; - } - Console.WriteLine("Choosing first available reader: " + readerNames.First()); - _readerName = readerNames.First(); - _cardMonitor = new SCardMonitor(ctxFactory, SCardScope.System); - _cardMonitor.CardInserted += _cardMonitor_CardInserted; - _cardMonitor.Start(_readerName); + StartWorkerThread(); + } - StartWorkerThread(); + private bool WeHaveValidCardReader() + { + if (_cardMonitor == null) + { + return false; } + Console.WriteLine(_cardMonitor.GetCurrentState(0)); + if (_cardMonitor.GetCurrentState(0) == SCRState.Unknown + || _cardMonitor.GetCurrentState(0) == SCRState.Unavailable + || _cardMonitor.GetCurrentState(0) == (SCRState.Ignore | SCRState.Unavailable) + //|| _cardMonitor.GetCurrentState(0) == SCRState.Unaware //if we say this is an invalid state, we cause a memory leak where we create a duplicate card monitor, subscribe and overwrite. + ) + { + return false; + } + return true; } public void Stop() @@ -57,19 +58,16 @@ namespace CardReaderService protected override void OnStop() { _stopMainWorkerThread = true; - if (_mainWorkThread!= null && _mainWorkThread.IsAlive) + _mainWorkerTerminationSignal.Set(); + if (_mainWorkThread != null && _mainWorkThread.IsAlive) { - _mainWorkThread.Join(3000); - if (_mainWorkThread.IsAlive) - { - _mainWorkThread.Interrupt(); - } + _mainWorkThread.Interrupt(); } if (_cardMonitor != null) { _cardMonitor.Cancel(); _cardMonitor.Dispose(); - _cardMonitor = null; + _cardMonitor = null; } } @@ -81,6 +79,7 @@ namespace CardReaderService Name = "CardServiceMainThread", IsBackground = false }; + _mainWorkerTerminationSignal = new AutoResetEvent(false); _mainWorkThread.Start(); } @@ -99,7 +98,7 @@ namespace CardReaderService _stopMainWorkerThread = true; return; } - + var err = reader.Connect(_readerName, SCardShareMode.Shared, SCardProtocol.T0 | SCardProtocol.T1); if (err == SCardError.Success) @@ -113,10 +112,10 @@ namespace CardReaderService var atrString = ConvertByteUIDToString(e.Atr); Console.WriteLine("Card Inserted, ATR: " + atrString + ", and UID is: " + uid); - CardDataPost postObj = new CardDataPost {CardUId = uid}; + CardDataPost postObj = new CardDataPost { CardUId = uid }; DataCenterHelper.PostAsync(postObj, "/api/swipedata"); Console.WriteLine("Posted to Server"); - } + } } else { @@ -125,21 +124,53 @@ namespace CardReaderService } } - + private void MainWorkerThread() { while (!_stopMainWorkerThread) { - //dont actually need to do anything.. but cannot exit right away? - Thread.Sleep(3000); + if (!WeHaveValidCardReader()) + { //only do this if we don't have a valid card reader + if (_initialised) + { + //card reader no longer available, tidy up. + _cardMonitor.Cancel(); + _cardMonitor.CardInserted -= _cardMonitor_CardInserted; + _cardMonitor.Dispose(); + _cardMonitor = null; + _initialised = false; + } + Console.WriteLine("Starting.. Getting available readers"); + var ctxFactory = ContextFactory.Instance; + using (var context = ctxFactory.Establish(SCardScope.System)) + { + var readerNames = context.GetReaders(); + if (!NoReaderAvailable(readerNames)) + { + //we have a reader available, so initialise! + Console.WriteLine("Choosing first available reader: " + readerNames.First()); + _readerName = readerNames.First(); + _cardMonitor = new SCardMonitor(ctxFactory, SCardScope.System); + _cardMonitor.CardInserted += _cardMonitor_CardInserted; + _cardMonitor.Start(_readerName); + + _initialised = true; + } + else + { + Console.WriteLine("No Card Reader is available.."); + } + } + } + _mainWorkerTerminationSignal.WaitOne(3000); } } private string ConvertByteUIDToString(byte[] uid) { - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); - foreach(var b in uid) + foreach (var b in uid) { sb.AppendFormat("{0:X2}", b); } diff --git a/DataCenter_Windows/WindowsDataCenter/CardReaderService/CardReaderService.cs b/DataCenter_Windows/WindowsDataCenter/CardReaderService/CardReaderService.cs index 9422698..e5bf9ab 100644 --- a/DataCenter_Windows/WindowsDataCenter/CardReaderService/CardReaderService.cs +++ b/DataCenter_Windows/WindowsDataCenter/CardReaderService/CardReaderService.cs @@ -18,6 +18,9 @@ namespace CardReaderService private string _readerName = ""; private SCardMonitor _cardMonitor; + private bool _initialised = false; + private AutoResetEvent _mainWorkerTerminationSignal; + private ILogger _logger; public CardReaderService() @@ -32,45 +35,9 @@ namespace CardReaderService protected override void OnStart(string[] args) { - var configPath = string.Concat(System.Reflection.Assembly.GetEntryAssembly().Location, ".config"); - _logger = NinjectHelper.GetInstance().Get(); - _logger.Trace("Starting Service.. Getting available readers"); - var ctxFactory = ContextFactory.Instance; - using(var context = ctxFactory.Establish(SCardScope.System)) - { - var readerNames = context.GetReaders(); - if (NoReaderAvailable(readerNames)) - { - _logger.Trace("No Card Reader is available, Exiting.."); - throw new ApplicationException("A card reader must be provided in order to operate."); - } - foreach (var reader in readerNames) - { - _logger.Trace("Found reader: {0}", reader); - } - - var readerNameConfig = ConfigurationHandler.ConfigurationHandler.GetConfiguration("ReaderName"); - if (string.IsNullOrEmpty(readerNameConfig)) - { - if (!readerNames.Contains(readerNameConfig)) - { - _logger.Warn("No reader found with the name: {0}, defaulting to first available reader {1}", - readerNameConfig, readerNames.First()); - - readerNameConfig=readerNames.First(); - } - } - _logger.Trace("Choosing reader: {0}", readerNameConfig); - _readerName = readerNameConfig; - - _cardMonitor = new SCardMonitor(ctxFactory, SCardScope.System); - _cardMonitor.CardInserted += _cardMonitor_CardInserted; - _cardMonitor.Start(_readerName); - - StartWorkerThread(); - } + StartWorkerThread(); } public void StopService() @@ -81,6 +48,7 @@ namespace CardReaderService protected override void OnStop() { _stopMainWorkerThread = true; + _mainWorkerTerminationSignal.Set(); if (_mainWorkThread!= null && _mainWorkThread.IsAlive) { _mainWorkThread.Join(3000); @@ -104,6 +72,7 @@ namespace CardReaderService Name = "CardServiceMainThread", IsBackground = false }; + _mainWorkerTerminationSignal = new AutoResetEvent(false); _mainWorkThread.Start(); } @@ -137,7 +106,7 @@ namespace CardReaderService _logger.Trace("Card Inserted, ATR: " + atrString + ", and UID is: " + uid); var postObj = new CardDataPost {CardUId = uid}; - DataCenterHelper.PostAsync(postObj, "/api/swipedata"); + DataCenterHelper.PostAsync(_logger, postObj, "/api/swipedata"); _logger.Trace("Posted to Server"); } else @@ -153,7 +122,51 @@ namespace CardReaderService while (!_stopMainWorkerThread) { //dont actually need to do anything.. but cannot exit right away? - Thread.Sleep(3000); + if (!WeHaveValidCardReader()) + { + if (_initialised) + { + //card reader no longer available, tidy up. + _cardMonitor.Cancel(); + _cardMonitor.CardInserted -= _cardMonitor_CardInserted; + _cardMonitor.Dispose(); + _cardMonitor = null; + _initialised = false; + } + var ctxFactory = ContextFactory.Instance; + using (var context = ctxFactory.Establish(SCardScope.System)) + { + var readerNames = context.GetReaders(); + if (NoReaderAvailable(readerNames)) + { + _logger.Trace("No Card Reader is available.."); + } + else + { + foreach (var reader in readerNames) + { + _logger.Trace("Found reader: {0}", reader); + } + + var readerNameConfig = ConfigurationHandler.ConfigurationHandler.GetConfiguration("ReaderName"); + if (string.IsNullOrEmpty(readerNameConfig) || (!readerNames.Contains(readerNameConfig))) + { + _logger.Warn("No reader found with the name: {0}, defaulting to first available reader {1}", + readerNameConfig, readerNames.First()); + + readerNameConfig = readerNames.First(); + } + _logger.Trace("Choosing reader: {0}", readerNameConfig); + _readerName = readerNameConfig; + + _cardMonitor = new SCardMonitor(ctxFactory, SCardScope.System); + _cardMonitor.CardInserted += _cardMonitor_CardInserted; + _cardMonitor.Start(_readerName); + _initialised = true; + } + } + } + _mainWorkerTerminationSignal.WaitOne(3000); } } @@ -171,5 +184,23 @@ namespace CardReaderService { return readerNames == null || readerNames.Count < 1; } + + private bool WeHaveValidCardReader() + { + if (_cardMonitor == null) + { + return false; + } + _logger.Trace(_cardMonitor.GetCurrentState(0).ToString()); + if (_cardMonitor.GetCurrentState(0) == SCRState.Unknown + || _cardMonitor.GetCurrentState(0) == SCRState.Unavailable + || _cardMonitor.GetCurrentState(0) == (SCRState.Ignore | SCRState.Unavailable) + //|| _cardMonitor.GetCurrentState(0) == SCRState.Unaware //if we say this is an invalid state, we cause a memory leak where we create a duplicate card monitor, subscribe and overwrite. + ) + { + return false; + } + return true; + } } } diff --git a/DataCenter_Windows/WindowsDataCenter/CardReaderService/DataCenterHelper.cs b/DataCenter_Windows/WindowsDataCenter/CardReaderService/DataCenterHelper.cs index 6bb0452..24e61cb 100644 --- a/DataCenter_Windows/WindowsDataCenter/CardReaderService/DataCenterHelper.cs +++ b/DataCenter_Windows/WindowsDataCenter/CardReaderService/DataCenterHelper.cs @@ -5,13 +5,14 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using Interfaces; using Newtonsoft.Json; namespace CardReaderService { static class DataCenterHelper { - public static void Post(CardDataPost postObject, string url) + public static void Post(ILogger logger, CardDataPost postObject, string url) { var endpointConfig = ConfigurationHandler.ConfigurationHandler.GetConfiguration("DataCenterServiceEndpoint") ?? "http://localhost:8800"; @@ -22,10 +23,10 @@ namespace CardReaderService var content = new StringContent(jsonObject, Encoding.UTF8, "application/json"); try { - Console.WriteLine("Writing"); + logger.Trace("Writing"); var fullUrl = endpointConfig + url; var response = client.PostAsync(fullUrl, content).Result; - Console.WriteLine("Written"); + logger.Trace("Written"); } catch (Exception) { @@ -35,9 +36,9 @@ namespace CardReaderService } } - public static Task PostAsync(CardDataPost postObject, string url) + public static Task PostAsync(ILogger logger, CardDataPost postObject, string url) { - return Task.Run(() => Post(postObject, url)); + return Task.Run(() => Post(logger, postObject, url)); } } diff --git a/DataCenter_Windows/WindowsDataCenter/CardReaderService/Properties/AssemblyInfo.cs b/DataCenter_Windows/WindowsDataCenter/CardReaderService/Properties/AssemblyInfo.cs index 31ec865..cb59532 100644 --- a/DataCenter_Windows/WindowsDataCenter/CardReaderService/Properties/AssemblyInfo.cs +++ b/DataCenter_Windows/WindowsDataCenter/CardReaderService/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.5.0")] -[assembly: AssemblyFileVersion("0.1.5.0")] +[assembly: AssemblyVersion("0.2.1.0")] +[assembly: AssemblyFileVersion("0.2.1.0")] diff --git a/DataCenter_Windows/WindowsDataCenter/CardReaderServiceHost/Properties/AssemblyInfo.cs b/DataCenter_Windows/WindowsDataCenter/CardReaderServiceHost/Properties/AssemblyInfo.cs index be4e655..380976f 100644 --- a/DataCenter_Windows/WindowsDataCenter/CardReaderServiceHost/Properties/AssemblyInfo.cs +++ b/DataCenter_Windows/WindowsDataCenter/CardReaderServiceHost/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("0.2.1.0")] +[assembly: AssemblyFileVersion("0.2.1.0")] diff --git a/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/CardReaderServiceInstaller.wixproj b/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/CardReaderServiceInstaller.wixproj index a8847dc..c5bbea3 100644 --- a/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/CardReaderServiceInstaller.wixproj +++ b/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/CardReaderServiceInstaller.wixproj @@ -37,6 +37,14 @@ Binaries;Content;Satellites INSTALLFOLDER + + ConfigurationHandler + {115250f6-f8c4-4f9b-a15f-251ea258d963} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + Interfaces {b7347b72-e208-423a-9d99-723b558ea3d7} diff --git a/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/Common.wxs b/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/Common.wxs index 9bb6fef..e8a39b4 100644 --- a/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/Common.wxs +++ b/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/Common.wxs @@ -8,6 +8,12 @@ KeyPath="yes" Source="$(var.Interfaces.TargetDir)Interfaces.dll" /> + + + diff --git a/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/Product.wxs b/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/Product.wxs index d42319f..1e5b069 100644 --- a/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/Product.wxs +++ b/DataCenter_Windows/WindowsDataCenter/CardReaderServiceInstaller/Product.wxs @@ -3,7 +3,7 @@ diff --git a/DataCenter_Windows/WindowsDataCenter/ConfigurationHandler/ConfigurationHandler.cs b/DataCenter_Windows/WindowsDataCenter/ConfigurationHandler/ConfigurationHandler.cs index 0359e05..d10e35c 100644 --- a/DataCenter_Windows/WindowsDataCenter/ConfigurationHandler/ConfigurationHandler.cs +++ b/DataCenter_Windows/WindowsDataCenter/ConfigurationHandler/ConfigurationHandler.cs @@ -1,4 +1,5 @@ using System.Configuration; +using System.Linq; namespace ConfigurationHandler { @@ -7,7 +8,9 @@ namespace ConfigurationHandler public static string GetConfiguration(string keyName) { var appSettings = ConfigurationManager.OpenExeConfiguration(System.Reflection.Assembly.GetEntryAssembly().Location).AppSettings; - return appSettings.Settings[keyName].Value; + if(appSettings.Settings.AllKeys.Contains(keyName)) + return appSettings.Settings[keyName].Value; + return string.Empty; } } } diff --git a/DataCenter_Windows/WindowsDataCenter/ConfigurationHandler/Properties/AssemblyInfo.cs b/DataCenter_Windows/WindowsDataCenter/ConfigurationHandler/Properties/AssemblyInfo.cs index abe3c68..6e769e8 100644 --- a/DataCenter_Windows/WindowsDataCenter/ConfigurationHandler/Properties/AssemblyInfo.cs +++ b/DataCenter_Windows/WindowsDataCenter/ConfigurationHandler/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.5.0")] -[assembly: AssemblyFileVersion("0.1.5.0")] +[assembly: AssemblyVersion("0.2.1.0")] +[assembly: AssemblyFileVersion("0.2.1.0")] diff --git a/DataCenter_Windows/WindowsDataCenter/FlexiTimeSystemInstaller/Bundle.wxs b/DataCenter_Windows/WindowsDataCenter/FlexiTimeSystemInstaller/Bundle.wxs index 0d330d0..2d58ef2 100644 --- a/DataCenter_Windows/WindowsDataCenter/FlexiTimeSystemInstaller/Bundle.wxs +++ b/DataCenter_Windows/WindowsDataCenter/FlexiTimeSystemInstaller/Bundle.wxs @@ -2,7 +2,7 @@ diff --git a/DataCenter_Windows/WindowsDataCenter/Interfaces/IRepository.cs b/DataCenter_Windows/WindowsDataCenter/Interfaces/IRepository.cs index 30f8141..e5e4f59 100644 --- a/DataCenter_Windows/WindowsDataCenter/Interfaces/IRepository.cs +++ b/DataCenter_Windows/WindowsDataCenter/Interfaces/IRepository.cs @@ -66,6 +66,10 @@ namespace Interfaces /// with nested list /// IdentifierList GetUnassignedIdentifierList(); + /// + /// Remove all unassigned identifiers from the system. + /// + void ClearUnassignedIdentifiers(); /// /// Update a user in the system with the new values. diff --git a/DataCenter_Windows/WindowsDataCenter/Interfaces/Properties/AssemblyInfo.cs b/DataCenter_Windows/WindowsDataCenter/Interfaces/Properties/AssemblyInfo.cs index 6d4bbe6..e035cdd 100644 --- a/DataCenter_Windows/WindowsDataCenter/Interfaces/Properties/AssemblyInfo.cs +++ b/DataCenter_Windows/WindowsDataCenter/Interfaces/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.5.0")] -[assembly: AssemblyFileVersion("0.1.5.0")] +[assembly: AssemblyVersion("0.2.1.0")] +[assembly: AssemblyFileVersion("0.2.1.0")] diff --git a/DataCenter_Windows/WindowsDataCenter/NLogLogger/NLogConfig.xml b/DataCenter_Windows/WindowsDataCenter/NLogLogger/NLogConfig.xml index 819283f..9600876 100644 --- a/DataCenter_Windows/WindowsDataCenter/NLogLogger/NLogConfig.xml +++ b/DataCenter_Windows/WindowsDataCenter/NLogLogger/NLogConfig.xml @@ -4,8 +4,12 @@ autoReload="true"> + + \ No newline at end of file diff --git a/DataCenter_Windows/WindowsDataCenter/NLogLogger/NLogger.cs b/DataCenter_Windows/WindowsDataCenter/NLogLogger/NLogger.cs index 291b4a9..abbc712 100644 --- a/DataCenter_Windows/WindowsDataCenter/NLogLogger/NLogger.cs +++ b/DataCenter_Windows/WindowsDataCenter/NLogLogger/NLogger.cs @@ -9,14 +9,14 @@ namespace NLogLogger { public class NLogger:ILogger { - private NLog.Logger _logger; + private readonly Logger _logger; public NLogger() { var nlogConfigPathOption = ConfigurationHandler.ConfigurationHandler.GetConfiguration("NLogConfigFilePath"); if (nlogConfigPathOption == null) { - throw new ArgumentNullException("nlogConfigPath"); + throw new ArgumentNullException("nlogConfigPath", "NLogConfigFilePath missing from application config file."); } var nlogConfigPath = new Uri(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().CodeBase), nlogConfigPathOption)).LocalPath; LogManager.Configuration = new XmlLoggingConfiguration(nlogConfigPath); diff --git a/DataCenter_Windows/WindowsDataCenter/SQLiteProvider/Product.wxs b/DataCenter_Windows/WindowsDataCenter/SQLiteProvider/Product.wxs index 7f006ab..cc45214 100644 --- a/DataCenter_Windows/WindowsDataCenter/SQLiteProvider/Product.wxs +++ b/DataCenter_Windows/WindowsDataCenter/SQLiteProvider/Product.wxs @@ -4,7 +4,7 @@ diff --git a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/DBVersion.cs b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/DBVersion.cs new file mode 100644 index 0000000..b796f80 --- /dev/null +++ b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/DBVersion.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SQLiteRepository +{ + class DbVersion + { + public string VersionNumber { get; set; } + } +} diff --git a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/Properties/AssemblyInfo.cs b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/Properties/AssemblyInfo.cs index f820360..be2cc04 100644 --- a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/Properties/AssemblyInfo.cs +++ b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/Properties/AssemblyInfo.cs @@ -1,6 +1,6 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SQLiteRepository.Properties; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -32,5 +32,14 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.5.0")] -[assembly: AssemblyFileVersion("0.1.5.0")] +[assembly: AssemblyVersion(AssemblyInfo.ASSEMBLY_VERSION)] +[assembly: AssemblyFileVersion(AssemblyInfo.ASSEMBLY_FILE_VERSION)] + +namespace SQLiteRepository.Properties +{ + internal static class AssemblyInfo + { + internal const string ASSEMBLY_VERSION = "0.2.1.0"; + internal const string ASSEMBLY_FILE_VERSION = "0.2.1.0"; + } +} diff --git a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteProcedures.cs b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteProcedures.cs index b38588e..bcded4e 100644 --- a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteProcedures.cs +++ b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteProcedures.cs @@ -11,12 +11,37 @@ namespace SQLiteRepository nameof(TimeLogDb.CalendarWeek) + "=? and " + nameof(TimeLogDb.Year) + "=?)"; public const string GET_ALL_USERS = - "select * from " + nameof(UserIdentity) + " order by " + nameof(UserIdentity.LastName) + " collate nocase, " + - nameof(UserIdentity.FirstName) + " collate nocase"; + "select * from " + nameof(UserIdentity) + " ut " + + "where " + + "EXISTS( select " + nameof(GroupDb.GroupId) + + " from " + nameof(GroupDb) + + " where " + nameof(GroupDb.GroupName) + " = 'Archived') AND " + + "NOT EXISTS( select * from " + nameof(UserGroupJoinDb) + " ugp where " + + nameof(UserGroupJoinDb.UserId_FK) + " = ut.Id" + + " and " + nameof(UserGroupJoinDb.GroupId_FK) + " = ( " + + "select " + nameof(GroupDb.GroupId) + + " from " + nameof(GroupDb) + + " where " + nameof(GroupDb.GroupName) + " = 'Archived') )" + + "order by " + + nameof(UserIdentity.LastName) + " collate nocase, " + + nameof(UserIdentity.FirstName) + " collate nocase"; public const string GET_ALL_USERS_PAGINATE = - "select * from " + nameof(UserIdentity) + " order by " + nameof(UserIdentity.LastName) + " collate nocase, " + - nameof(UserIdentity.FirstName) + " collate nocase limit ? offset ?"; + "select * from " + nameof(UserIdentity) + " ut " + + "where " + + "EXISTS( select " + nameof(GroupDb.GroupId) + + " from " + nameof(GroupDb) + + " where " + nameof(GroupDb.GroupName) + " = 'Archived') AND " + + "NOT EXISTS( select * from " + nameof(UserGroupJoinDb) + " ugp where " + + nameof(UserGroupJoinDb.UserId_FK) + " = ut.Id" + + " and " + nameof(UserGroupJoinDb.GroupId_FK) + " = ( " + + "select " + nameof(GroupDb.GroupId) + + " from " + nameof(GroupDb) + + " where " + nameof(GroupDb.GroupName) + " = 'Archived') )" + + "order by " + + nameof(UserIdentity.LastName) + " collate nocase, " + + nameof(UserIdentity.FirstName) + " collate nocase " + + "limit ? offset ?"; public const string GET_ALL_USERS_BY_GROUP = "select u." + nameof(UserIdentity.Id) + ", u." + nameof(UserIdentity.FirstName) + ", u." + @@ -38,6 +63,9 @@ namespace SQLiteRepository public const string GET_UNASSIGNED_CARD_LIST = "select * from " + nameof(CardUniqueId) + " where " + nameof(CardUniqueId.UserId_FK) + "=?"; + public const string CLEAR_UNASSIGNED_CARDS = + "delete from " + nameof(CardUniqueId) + " where " + nameof(CardUniqueId.UserId_FK) + "=?"; + public const string UPDATE_CARD_USER_ID = "update " + nameof(CardUniqueId) + " set " + nameof(CardUniqueId.UserId_FK) + "=? where " + nameof(CardUniqueId.Id) + "=?"; diff --git a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteRepository.cs b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteRepository.cs index 1d29754..89c510b 100644 --- a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteRepository.cs +++ b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteRepository.cs @@ -8,6 +8,7 @@ using System.Reflection; using Interfaces; using SQLite.Net; using SQLite.Net.Platform.Win32; +using SQLiteRepository.Properties; namespace SQLiteRepository { @@ -32,7 +33,68 @@ namespace SQLiteRepository _connection.CreateTable(); _connection.CreateTable(); _connection.CreateTable(); + _connection.CreateTable(); _logger.Trace("Initialised SQLite Repository"); + _logger.Trace("Checking For Upgrades"); + CheckForDbUpgrade(); + } + + private void CheckForDbUpgrade() + { + var data = _connection.Query("select * from DbVersion"); + if (!data.Any()) + { + //Pre-Upgrade database, need upgrading + _logger.Trace("Pre version 0.2 RC database found, performing update.."); + ExecuteUpgradeFromVersion("0.1"); //execute 0.2 upgrade scripts onwards. + } + else + { + var installedVersion = new Version(data.First().VersionNumber); + var currentVersion = new Version(AssemblyInfo.ASSEMBLY_VERSION); + if (currentVersion.CompareTo(installedVersion) > 0) //greater than 0 - current version is newer + { + _logger.Trace("Installed Database Version: {0} is older than current version {1}"); + ExecuteUpgradeFromVersion(installedVersion.ToString()); + } + } + } + + private void ExecuteUpgradeFromVersion(string installedVersion) + { + const string PREFIX = "SQLiteRepository.UpgradeScripts."; + var instVers = new Version(installedVersion); + //so we have established that each script for upgrade will be .sql, so now start at lowest version and work up! + var assembly = Assembly.GetExecutingAssembly(); + var upgradeScripts = assembly.GetManifestResourceNames() + .Where(x=>x.StartsWith(PREFIX)) + .OrderBy(x=>new Version(Path.GetFileNameWithoutExtension(x.Replace(PREFIX, "")))) + .Where(x => + { + var scriptVersion = new Version(Path.GetFileNameWithoutExtension(x.Replace(PREFIX, ""))).CompareTo(instVers); + return scriptVersion > 0; + } + ) + .ToList(); + //now have an ordered list of upgrade script files, so execute in order! + foreach (var upgradeScript in upgradeScripts) + { + using (var stream = assembly.GetManifestResourceStream(upgradeScript)) + using(var str = new StreamReader(stream)) + { + var script = str.ReadToEnd(); + _logger.Trace("Executing upgrade script with name: {0}", upgradeScript); + _connection.Execute(script); + } + } + SetDbVersion(AssemblyInfo.ASSEMBLY_VERSION); + } + + private void SetDbVersion(string vers) + { + _connection.DeleteAll(); + _connection.Insert(new DbVersion {VersionNumber = vers}); + _logger.Trace("Set Database version to: {0}", vers); } public UserList GetUsers(int pageNumber = -1, int pageSize = -1, int groupId = -1) @@ -247,6 +309,13 @@ namespace SQLiteRepository return ret; } + public void ClearUnassignedIdentifiers() + { + _connection.Execute( + SQLiteProcedures.CLEAR_UNASSIGNED_CARDS, + Constants.UNASSIGNED_CARD_USER_ID); + } + //TODO: Check time logs table on update to ensure associated cards/unique identifiers are removed/added as appropriate. public OperationResponse UpdateUser(User user, out int userIdResult) { @@ -487,7 +556,7 @@ namespace SQLiteRepository { //TODO: I would probably prefer to do this manually.... var resp = _connection.Query("update GroupDb set GroupName=? where GroupId=?", group.Name, group.Id); - //var resp = _connection.Update(groupDb); + return OperationResponse.UPDATED; } @@ -505,6 +574,8 @@ namespace SQLiteRepository if (!query.Any()) return OperationResponse.FAILED; + UpdateExistingLogDirections(log); + _connection.ExecuteScalar("delete from TimeLogDb where Id=?", log.Id); return OperationResponse.DELETED; @@ -514,6 +585,8 @@ namespace SQLiteRepository { var calendarWeek = GetIso8601CalendarWeek(log.EventTime.UtcDateTime); var year = log.EventTime.Year; + log.CalendarWeek = calendarWeek; + log.Year = year; var dbLog = new TimeLogDb { SwipeEventDateTime = log.EventTime, @@ -524,6 +597,10 @@ namespace SQLiteRepository UserId_FK = log.UserId, IdentifierId = ConvertSourceToIdentifierId(log.Source), }; + #region update in/out directions for manual logs. + UpdateExistingLogDirections(log); + #endregion + //and now insert the new log. _connection.Insert(dbLog); return OperationResponse.CREATED; } @@ -720,10 +797,7 @@ namespace SQLiteRepository else { // we have a time log from today already, so just do the opposite of what we last did! - if (lastEntry.Direction == LogDirectionDb.IN) - logDirection = LogDirectionDb.OUT; - else if (lastEntry.Direction == LogDirectionDb.OUT) - logDirection = LogDirectionDb.IN; + logDirection = InvertLogDirectionDb(lastEntry.Direction); } } else @@ -809,5 +883,47 @@ namespace SQLiteRepository IsContractor = user.IsContractor }; } + + private void UpdateExistingLogDirections(TimeLog log) + { + //need to make this generic so that both create and delete will update the log directions.. but ARGH. + //if you want to use this as a delete, you would delete and call this, if you want to create, call this then create. + //should look at how to improve this method so that it will compensate for if the log hasnt been deleted yet, but ott? + var weekLogs = GetTimeLogList(log.UserId, log.CalendarWeek, log.Year); + + //Get the same logs of the day that the log has been entered for + var todaysLogs = weekLogs.FirstOrDefault(x => x.Day == log.EventTime.DayOfWeek); + if (todaysLogs != null) + { + //Get the days logs that are after the manually created date + var logs = todaysLogs.Logs.Where(x => x.EventTime.CompareTo(log.EventTime) >= 0).OrderBy(x => x.EventTime).ToList(); + //Update each log with the inverse progressively + var currentlogDirection = log.Direction; + for (var i = 0; i < logs.Count; i++) + { + logs[i].Direction = InvertLogDirection(currentlogDirection); + UpdateLog(logs[i]); + currentlogDirection = logs[i].Direction; + } + } + } + + private LogDirectionDb InvertLogDirectionDb(LogDirectionDb direction) + { + return (LogDirectionDb) (int) InvertLogDirection((LogDirection) (int) direction); + } + + private LogDirection InvertLogDirection(LogDirection direction) + { + switch (direction) + { + case LogDirection.IN: + return LogDirection.OUT; + case LogDirection.OUT: + return LogDirection.IN; + default: + return LogDirection.UNKNOWN; + } + } } } diff --git a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteRepository.csproj b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteRepository.csproj index 4ca4ebd..46517fc 100644 --- a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteRepository.csproj +++ b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/SQLiteRepository.csproj @@ -78,6 +78,7 @@ + @@ -102,6 +103,9 @@ + + + diff --git a/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/UpgradeScripts/0.2.sql b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/UpgradeScripts/0.2.sql new file mode 100644 index 0000000..5eeccd7 --- /dev/null +++ b/DataCenter_Windows/WindowsDataCenter/SQLiteRepository/UpgradeScripts/0.2.sql @@ -0,0 +1 @@ +insert into GroupDb values ((select max(groupId) from GroupDb)+1,'Archived',0) \ No newline at end of file diff --git a/DataCenter_Windows/WindowsDataCenter/WebApiServerHostInstaller/Product.wxs b/DataCenter_Windows/WindowsDataCenter/WebApiServerHostInstaller/Product.wxs index 9d69092..bd0e345 100644 --- a/DataCenter_Windows/WindowsDataCenter/WebApiServerHostInstaller/Product.wxs +++ b/DataCenter_Windows/WindowsDataCenter/WebApiServerHostInstaller/Product.wxs @@ -3,7 +3,7 @@ diff --git a/DataCenter_Windows/WindowsDataCenter/WebApiServerHostInstaller/WebPages.wxs b/DataCenter_Windows/WindowsDataCenter/WebApiServerHostInstaller/WebPages.wxs index 294e388..0d47858 100644 --- a/DataCenter_Windows/WindowsDataCenter/WebApiServerHostInstaller/WebPages.wxs +++ b/DataCenter_Windows/WindowsDataCenter/WebApiServerHostInstaller/WebPages.wxs @@ -93,6 +93,12 @@ KeyPath="yes" Checksum="yes"/> + + + @@ -177,6 +183,12 @@ KeyPath="yes" Checksum="yes"/> + + + diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/App.config b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/App.config index 7d38816..ebf5f25 100644 --- a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/App.config +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/App.config @@ -5,7 +5,7 @@ - + diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/Controllers/CardsController.cs b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/Controllers/CardsController.cs index 8908259..ee9d5b5 100644 --- a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/Controllers/CardsController.cs +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/Controllers/CardsController.cs @@ -27,5 +27,15 @@ namespace WindowsDataCenter _logger.Trace("Call to GetUnassignedCards, returning {0} items", unassignedCards.data.Count); return Ok(unassignedCards); } + + [HttpDelete] + [Route("unassigned")] + [CacheControl(MaxAge = 0)] + public IHttpActionResult ClearUnassignedCards() + { + _repo.ClearUnassignedIdentifiers(); + _logger.Trace("Call to ClearUnassignedCards, removed all identifiers."); + return Ok(); + } } } \ No newline at end of file diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/Properties/AssemblyInfo.cs b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/Properties/AssemblyInfo.cs index d8f7760..0a37b05 100644 --- a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/Properties/AssemblyInfo.cs +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.5.0")] -[assembly: AssemblyFileVersion("0.1.5.0")] +[assembly: AssemblyVersion("0.2.1.0")] +[assembly: AssemblyFileVersion("0.2.1.0")] diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/Admin.html b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/Admin.html index c4fcdb6..b6667d0 100644 --- a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/Admin.html +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/Admin.html @@ -77,7 +77,33 @@ +
+
+

Unassigned Cards

+ +
+
+
+ + + + + + + + + + + + + +
Card Unique IdLast Used Date
+
+
+
- + \ No newline at end of file diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/admin.js b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/admin.js index 964fd63..b912835 100644 --- a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/admin.js +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/admin.js @@ -2,6 +2,7 @@ var self = this; self.groupsList = ko.observable(null); self.groupEditItem = ko.observable(null); + self.unassignedCardList = ko.observable(null); self.helpers = new Helpers(); self.uiPages = { overview: "overview", @@ -11,7 +12,9 @@ self.apiEndpoints = { deleteGroups:"/api/groups/delete", getGroups: "/api/groups", - editGroup: "/api/groups/edit" + editGroup: "/api/groups/edit", + getUnassignedCards: "/api/cards/unassigned", + clearUnassignedCards: "/api/cards/unassigned" }; self.clearGroupForm = function () { self.helpers.goToMenuOption(self.uiPages.group); @@ -69,10 +72,48 @@ var errorObj = self.helpers.processRequestFailure(resp, status, error); }); }; + self.getUnassignedCardData = function() { + var url = self.helpers.createRequestUrl(self.apiEndpoints.getUnassignedCards, null, false); + $.getJSON(url, + function(res) { + self.unassignedCardList(res); + }).fail(function(resp, status, error) { + console.log("error - getUnassignedCards"); + var errorObj = self.helpers.processRequestFailure(resp, status, error); + }); + }; + self.clearUnassignedCards = function() { + var url = self.helpers.createRequestUrl(self.apiEndpoints.clearUnassignedCards, null, false); + $.ajax({ + type: "DELETE", + url: url, + data: "", + success: function() { + self.getUnassignedCardData(); //update + }, + error: function(jqxhr, status, error) { + console.log("error - clearUnassignedCards"); + var errorObj = self.helpers.processRequestFailure(resp, status, error); + } + }); + }; + self.padNumber = function (number) { + return (number < 10 ? "0" : "") + number; + }; + self.convertToDisplayDateTime = function (dateValue) { + var date = new Date(dateValue); // dd MM YY HH:mm:ss e.g.: 01 Mar 17 17:34:02 + return date.getDate() + " " + + date.toLocaleString("en-us", { month: "long" }) + " " + + (date.getYear()-100) + " " + + self.padNumber(date.getHours()) + ":" + + self.padNumber(date.getMinutes()) + ":" + + self.padNumber(date.getSeconds()); + }; Sammy(function () { this.disable_push_state = true; this.get("#overview", function () { self.getGroups(); + self.getUnassignedCardData(); }); this.post("#editgroup", function () { self.submitGroupEdit(self.groupEditItem()); diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/admin.min.js b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/admin.min.js index f6a459f..6a6123e 100644 --- a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/admin.min.js +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/admin.min.js @@ -1 +1 @@ -function AdminVM(){var n=this;n.groupsList=ko.observable(null);n.groupEditItem=ko.observable(null);n.helpers=new Helpers;n.uiPages={overview:"overview",group:"groups",home:function(){return this.overview}};n.apiEndpoints={deleteGroups:"/api/groups/delete",getGroups:"/api/groups",editGroup:"/api/groups/edit"};n.clearGroupForm=function(){n.helpers.goToMenuOption(n.uiPages.group);n.groupEditItem(null)};n.hideGroupForm=function(){n.groupEditItem(null)};n.newGroupForm=function(){n.groupEditItem({Id:-1,Name:""});n.helpers.goToMenuOption(n.uiPages.group)};n.groupFormHidden=ko.computed(function(){return n.groupEditItem()==null},n);n.editGroupClick=function(t){n.helpers.goToMenuOption(n.uiPages.group);n.groupEditItem(t)};n.getGroups=function(){var t=n.helpers.createRequestUrl(n.apiEndpoints.getGroups,null,!1);$.getJSON(t,function(t){n.groupsList(t)}).fail(function(t,i,r){console.log("error - getGroups");var u=n.helpers.processRequestFailure(t,i,r)})};n.deleteGroup=function(t){var i=n.helpers.createRequestUrl(n.apiEndpoints.deleteGroups,[{key:"groupId",value:t}],!1,!1);$.ajax({url:i,type:"DELETE",success:function(){console.log("deleted "+t);n.hideGroupForm();n.helpers.goToMenuOption(n.uiPages.home())}});console.log("delete: "+t)};n.submitGroupEdit=function(t){var i=n.helpers.createRequestUrl(n.apiEndpoints.editGroup,null,!1);$.post(i,t,function(){},"json").done(function(){n.groupEditItem(null);n.helpers.goToMenuOption(n.uiPages.home())}).fail(function(t,i,r){n.helpers.goToMenuOption(n.uiPages.home());var u=n.helpers.processRequestFailure(t,i,r)})};Sammy(function(){this.disable_push_state=!0;this.get("#overview",function(){n.getGroups()});this.post("#editgroup",function(){return n.submitGroupEdit(n.groupEditItem()),!1});this.get("",function(){this.app.runRoute("get","#"+n.uiPages.home())})}).run()}ko.applyBindings(new AdminVM) \ No newline at end of file +function AdminVM(){var n=this;n.groupsList=ko.observable(null);n.groupEditItem=ko.observable(null);n.unassignedCardList=ko.observable(null);n.helpers=new Helpers;n.uiPages={overview:"overview",group:"groups",home:function(){return this.overview}};n.apiEndpoints={deleteGroups:"/api/groups/delete",getGroups:"/api/groups",editGroup:"/api/groups/edit",getUnassignedCards:"/api/cards/unassigned",clearUnassignedCards:"/api/cards/unassigned"};n.clearGroupForm=function(){n.helpers.goToMenuOption(n.uiPages.group);n.groupEditItem(null)};n.hideGroupForm=function(){n.groupEditItem(null)};n.newGroupForm=function(){n.groupEditItem({Id:-1,Name:""});n.helpers.goToMenuOption(n.uiPages.group)};n.groupFormHidden=ko.computed(function(){return n.groupEditItem()==null},n);n.editGroupClick=function(t){n.helpers.goToMenuOption(n.uiPages.group);n.groupEditItem(t)};n.getGroups=function(){var t=n.helpers.createRequestUrl(n.apiEndpoints.getGroups,null,!1);$.getJSON(t,function(t){n.groupsList(t)}).fail(function(t,i,r){console.log("error - getGroups");var u=n.helpers.processRequestFailure(t,i,r)})};n.deleteGroup=function(t){var i=n.helpers.createRequestUrl(n.apiEndpoints.deleteGroups,[{key:"groupId",value:t}],!1,!1);$.ajax({url:i,type:"DELETE",success:function(){console.log("deleted "+t);n.hideGroupForm();n.helpers.goToMenuOption(n.uiPages.home())}});console.log("delete: "+t)};n.submitGroupEdit=function(t){var i=n.helpers.createRequestUrl(n.apiEndpoints.editGroup,null,!1);$.post(i,t,function(){},"json").done(function(){n.groupEditItem(null);n.helpers.goToMenuOption(n.uiPages.home())}).fail(function(t,i,r){n.helpers.goToMenuOption(n.uiPages.home());var u=n.helpers.processRequestFailure(t,i,r)})};n.getUnassignedCardData=function(){var t=n.helpers.createRequestUrl(n.apiEndpoints.getUnassignedCards,null,!1);$.getJSON(t,function(t){n.unassignedCardList(t)}).fail(function(t,i,r){console.log("error - getUnassignedCards");var u=n.helpers.processRequestFailure(t,i,r)})};n.clearUnassignedCards=function(){var t=n.helpers.createRequestUrl(n.apiEndpoints.clearUnassignedCards,null,!1);$.ajax({type:"DELETE",url:t,data:"",success:function(){n.getUnassignedCardData()},error:function(t,i,r){console.log("error - clearUnassignedCards");var u=n.helpers.processRequestFailure(resp,i,r)}})};n.padNumber=function(n){return(n<10?"0":"")+n};n.convertToDisplayDateTime=function(t){var i=new Date(t);return i.getDate()+" "+i.toLocaleString("en-us",{month:"long"})+" "+(i.getYear()-100)+" "+n.padNumber(i.getHours())+":"+n.padNumber(i.getMinutes())+":"+n.padNumber(i.getSeconds())};Sammy(function(){this.disable_push_state=!0;this.get("#overview",function(){n.getGroups();n.getUnassignedCardData()});this.post("#editgroup",function(){return n.submitGroupEdit(n.groupEditItem()),!1});this.get("",function(){this.app.runRoute("get","#"+n.uiPages.home())})}).run()}ko.applyBindings(new AdminVM) \ No newline at end of file diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/index.html b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/index.html index 0b8e067..c003136 100644 --- a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/index.html +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/index.html @@ -10,14 +10,14 @@ - + - - + + - +